Errors
221 Pay error format, field errors, rate limit, stable codes and what to do for each one.
Every error of the 221 API (221 Pay, data routes, project routes, including
404 and 405) has the same envelope: a stable code to test in code, a
readable message, a request_id to pass to support, fields when fields are
rejected, and members specific to the code (limit, reset_at,
retry_after_s…), at the same level.
{
"code": "INVALID_REQUEST",
"message": "Some fields are invalid.",
"request_id": "2f6c9a0e-5b1d-4e7f-8c3a-9d0b1e2f3a4c",
"fields": {
"customer_phone": "Invalid mobile number for this payment method's country.",
"offer_code": "offer_code must hold 4 to 16 letters or digits."
}
}- Test
code, nevermessageor the HTTP status: the text can change, and the same code can carry a different status depending on the route. fieldsgroups one message per rejected field, so each error can be shown next to its field.messageis in French by default; in English withAccept-Language: enor the?lang=enparameter.request_idis also returned in theX-Request-Idheader of every response.
Rate limit
600 requests per minute per project. Beyond that: 429 RATE_LIMITED, with the
Retry-After header giving the wait in seconds.
HTTP/1.1 429 Too Many Requests
Retry-After: 12This is the only limit on 221 Pay routes: the daily quota (429 QUOTA_EXCEEDED)
covers data routes only. See Authentication,
section "Daily quota".
Method not allowed
A /v1 route called with another HTTP method answers 405 with an Allow
header, the list of accepted methods, and the same envelope as the other
errors, with the code METHOD_NOT_ALLOWED. The message follows
Accept-Language (French by default, English when the value starts with en).
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
Content-Type: application/json
{"code": "METHOD_NOT_ALLOWED", "message": "Method not allowed. Allowed methods: GET, POST.", "request_id": "2f6c9a0e-5b1d-4e7f-8c3a-9d0b1e2f3a4c"}Codes
| HTTP | code | Cause | What to do |
|---|---|---|---|
| 400 | INVALID_REQUEST | Invalid body, parameter or header. fields gives one message per rejected field, including a malformed offer_code, a field unknown to the body of POST /v1/payments (fields.<field name>), a missing Idempotency-Key on POST /v1/payments, /v1/refunds or /v1/payouts (key Idempotency-Key), or a refund above the refundable remainder (fields.amount). | Fix the listed fields. |
| 400 | OFFER_INVALID | Promo code unknown, expired or outside the order's conditions. | Remove the promo code or use another one. |
| 400 | PROJECT_REQUIRED | Dashboard session without a designated project. | Send the X-221-External-Ref header, or call with the project key. |
| 400 | BELOW_MINIMUM | Bank transfer below the minimum. Fields min_amount and max_amount, and fields.amount. | Send at least min_amount. See Payouts. |
| 400 | ABOVE_MAXIMUM | Bank transfer above the maximum per payout. Fields min_amount and max_amount, and fields.amount. | Lower the amount or make several payouts. |
| 400 | INVALID_CODE | Wrong verification code for a payout account. Field attempts_left. | Enter the code received by e-mail. |
| 400 | MISSING_CAPTURES | Identity verification submitted without all the photos (/v1/kyc/...). Field missing, the list of photos still to send. | Send the missing photos, then submit the verification again. |
| 401 | INVALID_API_KEY | Key malformed, unknown or revoked. | Check the key. See Authentication. |
| 401 | UNAUTHENTICATED | Neither key nor session, or key of another project. | Send the project key. |
| 403 | MISSING_SCOPE | Key without the scope the route (payments, data) or the webhook event requires. Member scope. | Create a key with that scope. |
| 403 | LIVE_NOT_ENABLED | Call in live mode (sk_221_pay_live_… key) on a project whose live mode is not activated. | Have a project administrator activate live mode (see Authentication), or call with a test key. |
| 403 | FORBIDDEN | Role insufficient for the action. | Act with an administrator account or an API key. |
| 403 | KYC_REQUIRED | Payout or payout number before identity verification. Field kyc_status. | Complete verification. See Payouts. |
| 403 | MERCHANT_SUSPENDED | Merchant account suspended or closed. Field merchant_status. | Contact 221. Reads keep working. |
| 403 | MERCHANT_LIMIT_EXCEEDED | Daily or monthly limit reached. Fields limit_type (daily or monthly) and limit. | Wait for the next period or ask for a higher limit. |
| 403 | WITHDRAWAL_LIMIT_EXCEEDED | Payout limit reached. Fields limit_type (per_withdrawal, daily_count, daily_amount or monthly_amount) and limit. | Lower the amount, or wait for the reset (midnight, Dakar time). |
| 403 | MONTHLY_LIMIT_REACHED | Monthly total of bank transfers reached. Field remaining, the amount still possible this month. | Lower the amount to remaining at most, or wait for the 1st of next month. |
| 404 | NOT_FOUND | Object missing or belonging to another project. | Check the id. |
| 404 | FEATURE_DISABLED | Feature switched off by 221 (promo codes, for example). | Call without that feature. |
| 409 | DUPLICATE_MERCHANT_REFERENCE | merchant_reference already used in the project. | Replay with the original Idempotency-Key to get the payment back, or choose a new reference for a new order. |
| 409 | IDEMPOTENCY_CONFLICT | Idempotency-Key already used with another body. | Replay the original body. |
| 409 | QUOTE_EXPIRED | Refund or payout quote expired (2 minutes). fields.quote_hash carries the message. | Request a new quote. |
| 409 | QUOTE_INVALID | quote_hash unknown, already used, belonging to another merchant account, or not matching the request (other amount, other fee_payer). fields.quote_hash carries the message. | Request a new quote. |
| 409 | DESTINATION_NOT_VERIFIED | Quote or payout to a number or bank account whose verification code has not been entered. The message says "number" or "bank account" depending on the destination. | Enter the code, then retry. See Payouts. |
| 409 | INSUFFICIENT_FUNDS | Available balance too low. | Wait for new collections or lower the amount. |
| 409 | INVALID_STATE | The object is not in a state that allows the action: dispute already open on the payment, or dispute that can no longer be accepted, challenged or completed; payout number already verified (/resend). The message names the current state. | Read the object again and act according to its state. |
| 409 | KYC_ALREADY_SUBMITTED | Identity verification already sent for review (/v1/kyc/...). | Do not send it again: follow GET /v1/kyc/status. |
| 409 | OFFER_CODE_TAKEN | POST /v1/offers with a promo code that already exists in the project. | Choose another code. |
| 410 | CODE_EXPIRED | Verification code of a payout account expired (10 minutes). | Request a new code with POST /v1/payout-destinations/{id}/resend. |
| 413 | REQUEST_TOO_LARGE | Request body over 1 MiB on a 221 Pay route (uploading a dispute document has its own limit), over 64 KiB on a data route. | Reduce the body. |
| 429 | TOO_MANY_ATTEMPTS | 5 wrong attempts on the verification code. | Request a new code. |
| 429 | RATE_LIMITED | Rate limit reached. Data routes add the member retry_after_s. | Wait Retry-After seconds. |
| 500 | INTERNAL_ERROR | Unexpected internal error in 221 Pay. | Replay with the same Idempotency-Key and the same body; if it persists, send the request_id to 221. |
| 503 | CHANNEL_UNAVAILABLE | Rail not activated on the project, or temporarily closed. For a payout, reserved funds are returned before the response. | Offer another rail or retry later. |
| 503 | PAYMENTS_UNAVAILABLE | 221 Pay temporarily unavailable on this server. | Retry later, with the same Idempotency-Key. |
| 503 | NOT_CONFIGURED | Feature not set up on this server: identity verification (/v1/kyc/...) or the public address of payment links (POST /v1/payment-links). Unlike CHANNEL_UNAVAILABLE, there is nothing to retry. | Contact 221 with the request_id. |
| 507 | STORAGE_FULL | Storage for identity verification photos temporarily full (/v1/kyc/...). | Retry later. |
Data and project route codes
These codes, upper case like the others, come from the data routes, keys,
webhooks and a project's team. The members specific to the code sit at the top
level of the body, next to message. The HTTP status can vary by route: test
code.
| HTTP | code | Cause | Members |
|---|---|---|---|
| 400 | BAD_REQUEST | Unreadable or missing JSON body on a data or project route (no fields). A field of the wrong type gives INVALID_REQUEST with fields instead. | |
| 401 | API_KEY_EXPIRED | Key expired. | expires_at |
| 401 | API_KEY_REVOKED | Key revoked. | revoked_at |
| 403 | API_KEY_DISABLED | Key disabled by 221 for abuse. | disabled_at |
| 403 | FORBIDDEN_ROLE | Project role insufficient for the action. | role, required |
| 429 | QUOTA_EXCEEDED | Daily quota of the data routes reached. | limit, reset_at |
| 400 | INVALID_URL | Webhook URL refused (https to a public host). | fields.url |
| 409 | SUBSCRIPTION_EXISTS | This URL already has an endpoint. | url |
| 409 | SUBSCRIPTION_LIMIT | 5 endpoints per project reached. | limit |
| 409 | KEY_LIMIT_REACHED | 5 active keys per project reached. | limit |
| 409 | PROJECT_LIMIT_REACHED | Project limit reached. | limit |
| 404 | PROJECT_NOT_FOUND | Project missing or not the caller's. | |
| 409 | LAST_ADMIN | The project must keep an administrator. | |
| 403 | OWNER_PROTECTED | Only the owner changes their role or leaves the project. | |
| 409 | OWNER_MUST_STAY | The owner stays administrator. | |
| 409 | ALREADY_MEMBER, ALREADY_INVITED | Person already a member, or invitation already pending. | email |
| 404 | INVITATION_NOT_FOUND | Invitation unknown or withdrawn. | |
| 410 | INVITATION_EXPIRED | Invitation expired. | expires_at |
| 410 | INVITATION_USED | Invitation already used. | |
| 403 | INVITATION_EMAIL_MISMATCH | Invitation addressed to another address. | |
| 409 | EMAIL_NOT_VERIFIED | E-mail address not confirmed. | |
| 403 | REAUTHENTICATION_REQUIRED | Sign-in less than 10 minutes old required. | |
| 403 | INVALID_PASSWORD | Wrong password. | |
| 409 | BALANCE_NOT_EMPTY | Project cannot be deleted while funds or a payout are in progress. | reason |
| 503 | DATASET_UNAVAILABLE | Dataset not published yet. | dataset |
| 503 | SERVER_BUSY | Service busy: retry in a few seconds. |
The /auth routes (sign-in, password) also answer with a flat code,
message and request_id shape (also in the X-Request-Id header). Their
codes are those of the sign-in service, for example INVALID_EMAIL,
INVALID_EMAIL_OR_PASSWORD or VALIDATION_ERROR (unreadable body, with a
single generic message). TOO_MANY_REQUESTS adds retry_after_s and the
X-Retry-After header.
Retry safely
| Situation | Retry |
|---|---|
400, 401, 403, 404, 409 | No: fix the request first. |
429 | Yes, after Retry-After seconds. |
500, 503, timeout, network cut | Yes, with the same Idempotency-Key and the same body. |
5xx on a quote | Yes: a quote moves no money. |
POST /v1/payments, POST /v1/refunds and POST /v1/payouts require
Idempotency-Key: a retry never creates a second operation. See
Payments, section "Unknown outcome".
Report an incident
Send 221 the request_id, the time of the call and the route called. Never
the API key or the webhook secret.