Skip to content

Errors, limits, idempotency ​

Error shape ​

Open API errors look like this:

json
{
  "error": {
    "type": "permission",
    "code": "consent_revoked",
    "message": "The person has revoked this business's access to their data",
    "param": null
  }
}
  • code is stable. Branch on it.
  • message is for humans and may change.
  • param names the field at fault, when there is one.
  • type groups codes by HTTP status: invalid_request (400/413/422), authentication (401), permission (403), not_found (404), conflict (409), gone (410), rate_limit (429), provider (502/503).

Two responses use a different shape:

  • 422 validation errors (missing field, wrong type, bad format) come back as a list:
    json
    { "detail": [{ "type": "missing", "loc": ["body", "consent"], "msg": "Field required" }] }
  • 429 rate limit: {"error": "Rate limit exceeded: 120 per 1 minute"}.

The token endpoint uses OAuth's format instead: {"error": "invalid_grant", "error_description": "…"}. See Continue with FOID: errors.

In the SDKs, every non-2xx response raises FoidError with status, code, message (Python) or description (Node), and the parsed body. For the two shapes above, code is http_error and the details are in body.

Error codes ​

HTTPcodeMeaningWhat to do
400invalid_nin, invalid_bvnThe number isn't exactly 11 digitsFix the input
400consent_requiredDocument check sent without consent_obtained=trueCollect and send consent
400unsupported_file_typeDocument isn't JPEG, PNG or WebPRe-upload
400invalid_webhook_urlThe webhook URL isn't allowed. See allowed URLs.Use a public https URL
401missing_api_keyNo Authorization: Bearer … headerSend the key
401invalid_api_keyUnknown, malformed or revoked keyCheck which key you deployed
403live_mode_disabledLive mode isn't enabled yet (KYB)Use a test key, or go live
403ip_not_allowedLive key used from an IP outside your allow-listCall from an allow-listed server
403organisation_suspendedThe organisation isn't activeContact FOID
403consent_revokedThe person revoked your accessStop and delete, see revocation
403scope_not_grantedYou don't hold the scope this read needsAsk for it in a new verification or sign-in
404subject_not_foundNo such subject in this environmentCheck the ID and the key's environment
404verification_not_foundNo such verification in this environmentCheck the ID and the key's environment
404credential_not_foundThe person has no FOID credential yet (trust report)—
409verification_closedThe verification is already completed or cancelled—
409idempotency_key_reusedSame Idempotency-Key, different request bodyUse a new key per distinct request
409no_face_on_filereverify for a subject without an earlier liveness faceUse liveness first
409no_foid_accountTrust report for someone without a FOID account—
413file_too_largeDocument image over 8 MBRe-upload smaller
422(validation)Body failed validation. See detail.Fix the request
429(rate limit)Too many requestsBack off and retry

The hosted page and the sign-in popup show their own errors to the person (for example too_many_attempts after 3 tries on one step, or verification_expired). Your server sees the outcome on the verification instead.

Idempotency ​

Network retries shouldn't create a second verification or pay for a second check. POST /verifications, POST /checks/nin and POST /checks/bvn accept an Idempotency-Key header:

  • Same key + same body: you get the original response back, with the header Idempotent-Replayed: true. Nothing new is created or billed.
  • Same key + different body: 409 idempotency_key_reused.
  • Keys are remembered for 24 hours, per organisation and environment.

Use something unique to the action, such as onboard-<your user id> or a UUID you store with the request.

bash
curl https://api.foid.ng/api/open/v1/verifications \
  -H "Authorization: Bearer $FOID_API_KEY" \
  -H "Idempotency-Key: onboard-cust_1001" \
  -H "Content-Type: application/json" \
  -d '{"checks": ["nin"], "purpose": "Open a wallet", "customer_ref": "cust_1001"}'
python
v = foid.create_verification(checks=["nin"], purpose="Open a wallet", customer_ref="cust_1001",
                             idempotency_key="onboard-cust_1001")
js
const v = await foid.createVerification({ checks: ['nin'], purpose: 'Open a wallet', customer_ref: 'cust_1001',
  idempotencyKey: 'onboard-cust_1001' })

The SDKs' check_nin / checkNin don't take an idempotency key. For idempotent checks, call request("POST", "/checks/nin", json=…, idempotency_key=…) (Python) or request('POST', '/checks/nin', { json, idempotencyKey }) (Node).

Rate limits ​

EndpointsLimit
POST /verifications, POST /checks/nin, POST /checks/bvn120 requests per minute per API key
POST /checks/nin-document30 per minute
POST /oauth/token60 per minute

Over the limit you get 429. Wait and retry with exponential backoff.

Request IDs ​

Every Open API response has a FOID-Request-Id header. The same ID appears in the console under Request logs. Log it with your own request, and quote it when you contact support.

FOID — Fintech Open Identity. Test mode never touches real identity data.