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 codeMeaning
MISSING_REQUIRED_FIELDA required field is missing or empty.
NOT_ALLOWED_FIELDThe 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_TYPEWrong JSON type, for example a number where a string is expected.
INVALID_FIELD_ENUM_TYPEThe value is not one of the allowed values. constraints lists them.
INVALID_EMAIL, INVALID_URL, INVALID_COUNTRY, INVALID_CURRENCYThe 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_LONGThe 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

StatusMeaningWhat to doRetry?
200, 201Success. The body is the resulting object.
202Accepted. 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.
204Success with no body.
400The 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
401The token is missing, invalid or expired.Get a new token, then retry once. See Authentication errors.Once, with a new token
403The token is valid but not allowed to do this.Use a credential with the right role. See Permissions.No
404NOT_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
409Conflict 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
422INVALID_BODY. The body is not valid JSON.Send a JSON object with Content-Type: application/json.No
423NOT_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
500Unexpected 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
503A 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

codeStatusMeaningWhat to do
INSUFFICIENT_BALANCE423The ledger account balance is lower than the payment.Fund the ledger account, then submit again.
CUSTOMER_FROZEN409Your wallet program is frozen. Payment submission, wallet credits, charges, load refunds and dispute resolution are refused while it is.Contact Payment Labs.
FORBIDDEN_INSUFFICIENT_TAGS403The 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

  1. Retry only 401 (once, with a new token), 429, 500, 503 and network timeouts.
  2. Wait before each retry and double the wait each time, starting at 1 second, with a small random jitter. Stop after 5 attempts.
  3. 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


What’s Next

Did this page help you?