Errors
Speedmap uses conventional HTTP status codes and one JSON error shape with a stable, machine-readable code.
Successful requests return 2xx. 4xx means the request cannot succeed as sent; 5xx means a temporary problem on our side or upstream, and the request is safe to retry.
Every error has the same body. Branch on error.code, never on message, which is written for people and can change.
error.codestringrequiredStable, machine-readable code from the table below.
error.messagestringrequiredHuman-readable explanation.
error.reference_idstringPresent when a verification was created before the failure. Use it with support.
error.detailsarray of objectsFor validation errors:
{ path, message }per field.pathis dotted, e.g.match.date_of_birth.
400 Bad Request
{
"error": {
"code": "invalid_request",
"message": "Request validation failed",
"details": [
{
"path": "nin",
"message": "NIN must be 11 digits"
},
{
"path": "match.date_of_birth",
"message": "Use YYYY-MM-DD"
}
]
}
}| Status | Code | Meaning | What to do |
|---|---|---|---|
400 | invalid_request | The body or query failed validation. details has one entry per field. | Fix the fields listed in details. Do not retry unchanged. |
400 | invalid_idempotency_key | Idempotency-Key is shorter than 8 or longer than 128 characters. | Use a UUID. |
400 | invalid_categories | Consent categories is not a 4-letter Y/N mask, or is NNNN. | Send a mask with at least one Y, e.g. YYNN. |
400 | email_required | Consent channel email was chosen without an email. | Add email, or use another channel. |
400 | too_many_items | A bulk job has more than 10,000 items. | Split the job. |
400 | invalid_amount | A wallet top-up is below ₦1,000 or above ₦10,000,000. | Choose an amount in range. |
400 | phone_required | A top-up was started for a workspace with no contact phone number. | Add a phone number in the top-up dialog (saved to the workspace profile). |
400 | invalid_token | An email verification, password reset or invitation link is malformed or was already used. | Request a new link from the dashboard or sign-in page. |
400 | token_expired | The link has expired (verification 24 h, reset 30 min, invitation 7 days). | Request a new link. |
401 | unauthorized | The API key is missing, malformed or revoked. | Send Authorization: Bearer sk_… with an active key. |
401 | key_expired | The API key passed its expires_at. | Create or rotate a key in the dashboard and deploy it. |
401 | invalid_code | The two-factor or emailed sign-in code is wrong. details.attempts_left counts down; five failures end the attempt. | Enter the current code from your authenticator, or a backup code. |
401 | invalid_otp | The one-time code on the hosted consent page is wrong. | Re-enter it. Five failures lock the request. |
402 | insufficient_balance | Live only: the wallet balance is below the price of the check. | Top up the wallet in the dashboard, then retry. |
403 | account_suspended | The workspace is suspended. | Contact support. |
403 | forbidden | Your role is not allowed to perform this action. | Ask a workspace owner or admin. |
403 | email_unverified | A live key or go-live application needs a verified email address. | Open the verification link we emailed, or resend it from the dashboard banner. |
403 | mfa_required | Two-factor authentication is on for this account and this sign-in has not completed it. | Complete the second step with POST /auth/mfa/verify. |
403 | mfa_required_by_org | The workspace requires two-factor authentication and this member has not enabled it. Dashboard writes are blocked; reads still work. | Enable two-factor authentication under Settings → Security. |
403 | key_scope | The API key is not scoped to this product or is read-only. | Use a key with the product in its scopes, or create one in the dashboard. |
403 | ip_not_allowed | The request came from an IP address outside the key’s allowlist. | Call from an allowed address, or edit the key’s allowlist. |
403 | live_access_required | A live action before go-live was approved. | Complete go-live in the dashboard, or use a sandbox key. |
403 | not_entitled | The workspace is not enabled for the product or method (e.g. raw NIN lookup). | Use a permitted method, or request access in the dashboard. |
403 | product_not_live | The product is in preview and answers in sandbox only. | Use a sandbox key until the product is live. |
403 | sandbox_only | A sandbox endpoint (such as consent approve/deny) was called with a live key. | Use a sandbox key. |
404 | not_found | No object with that id in this mode. | Check the id and that the key mode (sandbox or live) matches. |
404 | consent_not_found | The consent_id on a check does not exist in this mode. | Create a consent request first. |
404 | unknown_product | The product does not exist. | Check the endpoint path. |
409 | idempotency_key_reused | The Idempotency-Key was already used with a different body. | Use a new key for a new request. |
409 | consent_not_granted | Revoke was called on a consent that is not granted. | Only granted consents can be revoked. |
409 | consent_not_pending | A decision was made on a consent that is no longer pending. | Read the consent to see its status. |
409 | payment_not_pending | A top-up was checked or simulated after it was already paid, expired or failed. | Read the payment; start a new top-up if needed. |
412 | consent_not_granted | A check used a consent that is pending, denied, expired or revoked. | Wait for consent.updated, or create a new request. |
412 | consent_expired | The consent grant is older than 24 hours. | Create a new consent request. |
412 | consent_subject_mismatch | The consent was granted for a different identifier. | Use the consent for this identifier. |
422 | provider_rejected_request | The upstream registry rejected the request as invalid. | Check the identifier and fields. reference_id identifies the attempt. |
429 | rate_limited | The API key exceeded its per-minute budget. | Wait for Retry-After seconds, then retry with back-off. |
429 | too_many_attempts | Too many attempts on a sign-in or consent flow. | Wait a few minutes. |
502 | consent_hub_error | NIBSS Consent Hub rejected or failed the consent request. | Retry later; check the identifier. |
503 | provider_unavailable | The upstream for this product is temporarily switched off. | Retry later. Safe with the same Idempotency-Key. |
503 | verification_unavailable | The check could not be completed (provider outage or a Speedily-side issue). | Retry later with the same or a new Idempotency-Key. You are not charged. |
503 | payments_unavailable | The payment provider (FintavaPay) could not open a one-time account or confirm a transfer. | Retry in a few minutes. Nothing was charged. |
Retrying safely
- Retry
429,502and503with exponential back-off. On429, honourRetry-After. - Always send an
Idempotency-KeyonPOSTrequests and reuse it on retry: a retried check returns the original result and is never billed twice. A key whose first attempt failed with503is released, so the retry runs again. - Do not retry
4xxresponses unchanged (except429); they will fail the same way.
Statuses that are not errors
202 Acceptedwithstatus: "pending": the provider has not answered yet. See pending verifications.200withstatus: "not_found": the check ran and no record exists.
Questions? Email developers@speedmap.ng. This page is also available as Markdown.