Errors
Read Payment Labs error responses, tell a fixable request from a state conflict, and decide when it is safe to retry.
Payment Labs uses HTTP status codes to say what kind of failure happened, and a code in the body to say exactly which one. This page explains both and tells you what to do with each.
Before you start
You should be able to call the API with a token. See Environments and Authentication.
The error body
Most errors return a JSON object with two fields:
{
"code": "NOT_EDITABLE",
"message": "Payment P-01JBX7S9QK3M8T2N6R4W0Y5ZDE is not in draft or failed status."
}code: a machine-readable error code. Branch on it.message: a human-readable explanation. Log it, show it to an operator, never parse it. Its wording can change.
Handle the status code first, then the code when there is one. A few responses carry only message (for example some 409 responses), and a few come from the gateway as {"message": "Unauthorized"}. Your client must not fail when code is absent.
Validation errors
When one or more fields are missing or invalid, the response is 400 with code VALIDATION_ERROR and a details object. Each key is a field name (a dotted path for nested fields, such as buyer.email), and each value is the list of rules that field broke:
{
"code": "VALIDATION_ERROR",
"message": "There have been validation errors",
"details": {
"sourceAmount": [
{ "valid": false, "fieldName": "sourceAmount", "code": "MISSING_REQUIRED_FIELD", "message": "Field is required" }
],
"nickname": [
{ "valid": false, "fieldName": "nickname", "code": "NOT_ALLOWED_FIELD", "message": "Field is not allowed" }
]
}
}Common rule codes:
Rule code | Meaning |
|---|---|
MISSING_REQUIRED_FIELD | A required field is missing or empty. |
NOT_ALLOWED_FIELD | The body has a field the operation does not accept. Every operation rejects unknown fields, so check spelling and remove fields from other operations. |
INVALID_FIELD_TYPE | Wrong JSON type, for example a number where a string is expected. |
INVALID_FIELD_ENUM_TYPE | The value is not one of the allowed values. constraints lists them. |
INVALID_EMAIL, INVALID_URL, INVALID_COUNTRY, INVALID_CURRENCY | The value is not a valid email, URL, ISO 3166 country code or ISO 4217 currency code. |
INVALID_FIELD_TOO_LOW, INVALID_FIELD_TOO_HIGH, INVALID_FIELD_LENGTH_TOO_SHORT, INVALID_FIELD_LENGTH_TOO_LONG | The value is outside the allowed range or length. constraints gives the limit. |
Fix every field listed in details before you send the request again. Sending the same body again returns the same error.
Status codes
| Status | Meaning | What to do | Retry? |
|---|---|---|---|
200, 201 | Success. The body is the resulting object. | ||
202 | Accepted. The work continues in the background, for example submitting or cancelling a payment. | Follow the result with webhooks or by reading the object. A 202 does not mean the money has moved. | |
204 | Success with no body. | ||
400 | The request breaks a rule. Either VALIDATION_ERROR with details, or a business-rule code listed on the operation's reference page. | Fix the request. | No |
401 | The token is missing, invalid or expired. | Get a new token, then retry once. See Authentication errors. | Once, with a new token |
403 | The token is valid but not allowed to do this. | Use a credential with the right role. See Permissions. | No |
404 | NOT_FOUND. The object does not exist, or it belongs to another account. Payment Labs does not say which. | Check the IDs in the path. | No |
409 | Conflict with the current state, for example submitting a payment that is already submitted, or a reference ID already used with different terms. | Read the object to see its current state, then decide. | Only after the state changes |
422 | INVALID_BODY. The body is not valid JSON. | Send a JSON object with Content-Type: application/json. | No |
423 | NOT_EDITABLE or another state code. The object is in a state that does not allow this change, for example a payment that is no longer a draft or a batch that is archived. Some operations use 423 for other state codes, such as INSUFFICIENT_BALANCE. | Read the object; act on its current state. | Only after the state changes |
500 | Unexpected server error. The body may be {"error": "Internal Server Error"} or {"message": "..."}. | Retry with backoff. For operations that create something, check first whether it was created. | Yes, with care |
503 | A dependency was unavailable, for example AUTH_UNAVAILABLE. | Retry with backoff. | Yes |
409 versus 423: both mean "not now". 409 usually means another request already did the thing, or a value clashes with an existing record. 423 means the object's state does not allow the change. In both cases, read the object before doing anything else.
Errors that need action outside the API
code | Status | Meaning | What to do |
|---|---|---|---|
INSUFFICIENT_BALANCE | 423 | The ledger account balance is lower than the payment. | Fund the ledger account, then submit again. |
CUSTOMER_FROZEN | 409 | Your wallet program is frozen. Payment submission, wallet credits, charges, load refunds and dispute resolution are refused while it is. | Contact Payment Labs. |
FORBIDDEN_INSUFFICIENT_TAGS | 403 | The credential's role does not allow this operation. | Ask Payment Labs for a credential with the role the operation needs. See Permissions. |
Each operation's reference page lists the codes specific to it.
Search errors
Search operations return 400 with a validation details entry for an invalid filter (for example INVALID_FIELD_NOT_ALLOWED when you filter on a field the operation does not support), and 400 INVALID_REQUEST when the search itself fails. See Search, Exports and Pagination.
Retrying safely
- Retry only
401(once, with a new token),429,500,503and network timeouts. - Wait before each retry and double the wait each time, starting at 1 second, with a small random jitter. Stop after 5 attempts.
- Before retrying an operation that creates something (a batch, a payment, a checkout, a refund), check whether the first attempt succeeded. Most create operations are not idempotent. See Idempotency and Retries.
async function withRetry<T>(call: () => Promise<Response>, parse: (r: Response) => Promise<T>): Promise<T> {
const retryable = new Set([429, 500, 502, 503, 504]);
for (let attempt = 0; ; attempt++) {
const response = await call();
if (response.ok) return parse(response);
if (!retryable.has(response.status) || attempt === 4) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const delayMs = 1000 * 2 ** attempt + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}Rate limits
Payment Labs does not publish per-account request quotas. If you receive 429 Too Many Requests, slow down and retry with the backoff above. Spread large jobs over time instead of sending them in parallel bursts, and use bulk operations and search instead of one request per object.
Next steps
Updated about 24 hours ago
What’s Next
Next: Idempotency and Retries.
