Errors
A failed request answers with an HTTP status of 400 or more and this body:
{ "error": { "message": "Some of the information provided is not valid.", "code": "VALIDATION_FAILED", "title": "Validation failed", "details": [ { "field": "amount", "code": "invalid", "message": "Send a positive amount in minor units (500000 = ₦5,000.00)." }, { "field": "currency", "code": "unsupported", "message": "Only NGN is supported." }, { "field": "customer.email", "code": "format", "message": "Enter a valid email address." } ] }, "meta": { "request_id": "01a0fbe8-71b1-76ff-bd46-8017a630b882" }}| Field | |
|---|---|
error.code | What went wrong, as a stable code. Branch on this. |
error.message | A sentence for a person. Safe to show to your user; do not parse it, it may change. When exactly one field is wrong, it is that field’s message. |
error.title | The code in words. |
error.details | For validation errors: one entry per field, with the field’s path (customer.email), a short code (required, invalid, format, length, unknown, type, unsupported) and a message. |
error.meta | Extra context for some codes, such as retry_after_seconds. |
meta.request_id | This request’s id, also in the X-Request-Id header. Log it, and quote it when you contact us. |
You may send your own X-Request-Id (up to 64 printable characters) to tie
our logs to yours; we echo it back.
Error codes
Section titled “Error codes”| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | A field is missing or wrong, an unknown field was sent, a query parameter is invalid, or Idempotency-Key is missing. | Fix the request using details. Do not retry unchanged. |
| 400 | MALFORMED_REQUEST | The body is not valid JSON, or is empty. | Fix the body. |
| 401 | UNAUTHENTICATED | No Authorization: Bearer header. | Send your secret key. |
| 401 | INVALID_API_KEY | The key is unknown, mistyped or revoked. | Check the key, or roll it. |
| 403 | SECRET_KEY_REQUIRED | A publishable key (pk_…) was sent. | Use your secret key, from your server. |
| 403 | LIVE_MODE_NOT_ENABLED | A live key before your business is verified. | Use test keys until you are live. |
| 403 | ACCOUNT_RESTRICTED | Your account is suspended. | Contact us. |
| 403 | TEST_MODE_ONLY | A test helper was called in live mode. | Use a test key or a test checkout. |
| 404 | NOT_FOUND | No such object in this mode, or no such endpoint. Objects of other businesses are also “not found”. | Check the id and that you use the key of the right mode. |
| 409 | DUPLICATE_REFERENCE | You already used this reference in this mode. | Use a new reference for a new payment, or fetch the existing one. |
| 409 | IDEMPOTENCY_KEY_REUSED | This Idempotency-Key was used with a different body. | Use a new key for a different request. |
| 409 | REQUEST_IN_PROGRESS | The first request with this Idempotency-Key is still running. | Retry the same request in a second or two. |
| 409 | INVALID_STATE_TRANSITION | The action does not fit the object’s state, such as resending a webhook for a payment that has not finished. | Wait until it can apply. |
| 409 | CONFLICT | Something else is in the way; the message says what (for example, no webhook endpoint is subscribed). | Read the message. |
| 413 | PAYLOAD_TOO_LARGE | The body is over 2 MB. | Send less. Metadata is limited to 8 KB anyway. |
| 415 | UNSUPPORTED_MEDIA_TYPE | The body is not sent as application/json. | Set Content-Type: application/json. |
| 422 | VALIDATION_FAILED | Account-name enquiry found no such account. | Check the bank and number. |
| 429 | RATE_LIMITED | Too many requests. | Wait Retry-After seconds. See Rate limits. |
| 500 | INTERNAL_ERROR | Our fault. | Retry with backoff. Safe for POST /v1/payments thanks to the Idempotency-Key. |
| 503 | SERVICE_UNAVAILABLE | A bank or rail we depend on did not answer. | Retry shortly. |
Errors your customers may see at checkout
Section titled “Errors your customers may see at checkout”The hosted checkout handles these itself and shows the customer what to do. Your server never receives them, but you may see them in your dashboard or hear them from a customer:
code | Meaning |
|---|---|
CHECKOUT_EXPIRED | The checkout passed its expires_at. Start a new payment. |
CHECKOUT_CLOSED | The payment is already complete, or closed. |
PAYMENT_IN_PROGRESS | An attempt is still running; the customer must wait for it before trying again. |
CHANNEL_NOT_ENABLED | That method is not offered for this payment. |
INVALID_OTP | Wrong one-time code, with tries left. |
PAYER_NOT_FOUND | No Nablr account with that username or phone. |
ATTEMPT_FAILED | The method was refused outright; the customer can try another. |
LINK_INACTIVE | The payment link has been turned off. |
Retrying safely
Section titled “Retrying safely”- Retry
429,500,503and network errors, with exponential backoff (for example 1 s, 2 s, 4 s, then give up and alert). - Never retry
400,401,403,404or409unchanged: the answer will not change. POST /v1/paymentswith the sameIdempotency-Keyis always safe to retry: see Idempotency.