Errors and rate limits

Public read RPCs use the native Connect error envelope. Account-scoped REST endpoints use RFC 7807 problem details with Flan extensions (code, details).

Connect error format

Public read RPCs return JSON like:

{
  "code": "invalid_argument",
  "message": "work_mode must be remote, hybrid, or onsite"
}

The X-Request-ID response header always carries the request id — useful when contacting support.

REST problem format

Account-scoped REST endpoints return Content-Type: application/problem+json with a body of:

{
  "title": "Invalid Request",
  "status": 400,
  "detail": "unknown field \"foo\" in request body",
  "instance": "/v1/reports",
  "code": "invalid_request",
  "details": { "field": "foo" },
  "request_id": "req_a1b2c3d4e5f6..."
}

| Field | Always present | Description | | ------------ | -------------- | -------------------------------------------------------------- | | title | yes | Short human-readable title. | | status | yes | HTTP status code. | | detail | yes | Specific explanation. | | instance | no | The path that errored. | | code | yes | Stable machine-readable code (see below). | | details | no | Free-form object. For validation errors: { "field": "..." }. | | request_id | no | Echoed from X-Request-ID if provided, else generated. |

Status codes

| Status | Code | Meaning | | ------ | --------------------- | ------------------------------------------------------ | | 400 | invalid_request | Malformed query/body, unknown fields, bad enum. | | 401 | unauthorized | Missing or invalid credentials. | | 403 | forbidden | Authenticated but not allowed (e.g. unverified email). | | 404 | not_found | Resource does not exist. | | 405 | method_not_allowed | Wrong HTTP method. Allow header is set. | | 409 | conflict | Unique constraint violation. | | 413 | payload_too_large | Request body exceeds 64 KiB. | | 429 | rate_limited | Too many attempts. See Rate limits below. | | 500 | internal_error | Unexpected backend failure. | | 501 | not_implemented | Endpoint exists but is not yet available. | | 503 | service_unavailable | Maintenance or capacity mode. |

Rate limits

Rate limiting is being rolled out in stages. Today:

  • Auth flows (/v1/auth/login, /v1/auth/password/forgot) are throttled per-email and per-user to prevent brute force. Exceeding returns 429 rate_limited with the message "too many authentication attempts".
  • Public read RPCs (JobsService, CompaniesService, MetaService) are not rate-limited during beta.
  • API-key rate limits (per the pricing page) will be enforced once API-key authentication is wired through. Until then, treat the limits documented there as targets, not hard caps.

When limits activate, 429 rate_limited responses will include a Retry-After header (seconds).

Validation details

For request body validation, the server uses strict decoding: unknown fields cause 400 invalid_request with details.field set to the offending field. For nested objects, the field path is dot-separated (e.g. filters.work_mode).

{
  "status": 400,
  "code": "invalid_request",
  "detail": "unknown field \"foo\" in request body",
  "details": { "field": "foo" }
}

Reporting bad data

If you find a job whose status appears wrong, or whose company/location is incorrect, please report it:

curl -X POST https://api.useflan.app/v1/reports \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": "018f3b21-...",
    "type": "expired",
    "message": "URL returns 404"
  }'

Valid type values: expired, broken_apply, wrong_company, wrong_location, duplicate, spam, bad_format, other. Requires a cookie session (use the report form in the UI).