> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tabby.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API Error Codes

> Errors returned by the Tabby API — status codes, response bodies, and how to handle each one.

Check the usual errors the Tabby API returns and what to do about each.

## How errors work

When a request fails, Tabby responds with a `4xx`/`5xx` HTTP status code and a JSON error body:

```json theme={"dark"}
{ "status": "error", "errorType": "bad_data", "error": "could not decode request" }
```

For field-level validation failures the body carries an `errors` array instead of a single `error` string:

```json theme={"dark"}
{
  "status": "error",
  "errorType": "bad_data",
  "errors": [
    { "field": "$.payment.currency", "code": "invalid_value", "message": "" }
  ]
}
```

* Branch your handling on the **HTTP status code** and **`errorType`**. Possible `errorType` values: `bad_data`, `not_authorized`, `no_permission`, `not_found`, `conflict`; a `500` carries `internal`.
* `error` is a human-readable English message to aid debugging — log it, but don't parse it and never show it to customers.
* On `500` or a network failure, retry per your retry policy; for [captures](#post-apiv2paymentsidcaptures) and [refunds](#post-apiv2paymentsidrefunds) always retry with the **same idempotency key** — your own unique `reference_id` in the request body, see [Idempotent requests](/pay-in-4-custom-integration/payment-processing#idempotent-requests) — so retries can't duplicate money movement.
* Use the base URL that matches the merchant's region: `api.tabby.ai` (UAE) or `api.tabby.sa` (KSA) — see [Base URLs](/api-reference/overview#base-urls). Calling the wrong region returns `403` (see below).
* A pre-scoring **rejection is not an error**: `POST /api/v2/checkout` returns `200 OK` with `status: "rejected"`. For handling and the customer-facing rejection messages, see [Show rejection](/pay-in-4-custom-integration/checkout-flow#possible-rejection_reason-values).

## Quick reference

| HTTP status               | Error code (`errorType`)                                                                        |
| ------------------------- | ----------------------------------------------------------------------------------------------- |
| 400 Bad Request           | `bad_data`                                                                                      |
| 401 Unauthorized          | `not_authorized`                                                                                |
| 403 Forbidden             | `no_permission`                                                                                 |
| 404 Not Found             | `not_found`                                                                                     |
| 409 Conflict              | `conflict`                                                                                      |
| 429 Too Many Requests     | — (rate limit reached, see [Rate limit](/introduction/technical-requirements#rate-limit))       |
| 500 Internal Server Error | `internal` (Tabby-side error — retry, monitor the [status page](https://www.tabby-status.com/)) |

***

## `POST /api/v2/checkout`

Creates a checkout session. Authenticate with the **Secret Key**: `Authorization: Bearer {secret_key}`.

<AccordionGroup>
  <Accordion title="400 Bad Request — could not decode request">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not decode request" }
    ```

    The JSON is malformed or a parameter has the wrong type. Validate the payload against the [Session payload model](/api-reference/checkout/session-payload-model).
  </Accordion>

  <Accordion title="400 Bad Request — currency / merchant_code mismatch">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not create payment: only '{{}}' is supported, but got '{{}}'" }
    ```

    The `currency` in the payload does not match the country assigned to your `merchant_code` (e.g. a UAE `merchant_code` used with `SAR`). Use the correct currency for the country.
  </Accordion>

  <Accordion title="400 Bad Request — field validation failed">
    ```json theme={"dark"}
    {
      "status": "error",
      "errorType": "bad_data",
      "errors": [
        { "field": "$.payment.currency", "code": "invalid_value", "message": "" }
      ]
    }
    ```

    A field value is invalid — `field` points at the offending path. The same envelope covers unsupported `currency`, wrong formats, and similar per-field issues.
  </Accordion>

  <Accordion title="400 Bad Request — payment validation failed">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not create payment: should be positive" }
    ```

    Other validation failures on the payment object (amounts and quantities must be positive, etc.) use the same envelope with a human-readable `error`. Read the `error` field for the specific cause.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized", "error": "merchant is null" }
    ```

    Two possible causes:

    1. The `Authorization` header is missing or wrong — it must be sent as `Authorization: Bearer {secret_key}`.
    2. The `merchant_code` in the payload doesn't match what the API key is bound to. Check the `merchant_code` you're passing and use the correct one for the environment (live vs sandbox).
  </Accordion>

  <Accordion title="403 Forbidden — no permission">
    ```json theme={"dark"}
    { "status": "error", "errorType": "no_permission" }
    ```

    Two possible causes:

    1. You are passing the `token` parameter, but it is not enabled for your integration. Remove `token` from the payload.
    2. The request hit the wrong regional base URL — the body then also carries `"error": "merchant region does not match cluster region"`. Use the base URL that matches the merchant's region: `api.tabby.ai` (UAE) or `api.tabby.sa` (KSA).
  </Accordion>

  <Accordion title="404 Not Found">
    ```
    404 page not found
    ```

    The URL path is incorrect. Verify it against the [API reference](/api-reference/checkout/create-a-session). Unsupported HTTP methods also return `404`.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    Retry the request per your retry policy. If all requests keep returning `500`, contact the Tabby Integrations team and consider hiding Tabby temporarily. Follow [tabby-status.com](https://www.tabby-status.com/) for maintenance and incident updates.
  </Accordion>
</AccordionGroup>

***

## `GET /api/v2/payments/{id}`

Retrieves the current state of a payment. Successful responses and payment statuses are covered in [Payment statuses](/pay-in-4-custom-integration/payment-statuses).

<AccordionGroup>
  <Accordion title="400 Bad Request — invalid payment id">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "invalid payment id" }
    ```

    The `payment_id` is not a valid UUID. Fix the format.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized" }
    ```

    The `Authorization` header is missing or wrong. It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — no such payment">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "no such payment" }
    ```

    No payment with this `payment_id` is visible to your API key — either the id is wrong, or the payment belongs to a different store / API key. Check the `payment_id` and that you query with the same key that created the session.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    Retry per your retry policy. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>

***

## `GET /api/v2/payments`

Retrieves a list of payments filtered by query parameters. Errors use the same envelopes as `GET /api/v2/payments/{id}`:

* `400 Bad Request` (`bad_data`) — an invalid query-parameter value: `"error": "failed to decode request"`.
* `401 Unauthorized` (`not_authorized`) — missing/wrong `Authorization` header.
* `500 Internal Server Error` (`internal`) — retry per your retry policy.

***

## `PUT /api/v2/payments/{id}`

Updates a payment (e.g. `reference_id`). Errors use the same envelopes as the other Payments endpoints:

* `400 Bad Request` (`bad_data`) — malformed body: `"error": "failed to decode request"`.
* `401 Unauthorized` (`not_authorized`) — missing/wrong `Authorization` header.
* `404 Not Found` (`not_found`) — `"error": "no such payment"`: wrong `payment_id`, or a payment of a different store / API key.
* `500 Internal Server Error` (`internal`) — retry per your retry policy.

***

## `POST /api/v2/payments/{id}/captures`

Captures funds from an `AUTHORIZED` payment.

<Tip>
  Always send an idempotency key — your own unique `reference_id` in the request body (see [Idempotent requests](/pay-in-4-custom-integration/payment-processing#idempotent-requests)) — so that retries after a `500` or `409` do not produce duplicate captures.
</Tip>

<AccordionGroup>
  <Accordion title="400 Bad Request — invalid amount or body">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "Errors: [invalid_value($.amount):{{}}" }
    ```

    The `amount` has a wrong value — negative, too many decimal places, etc. An empty or truncated request body returns the same envelope with `"error": "EOF"`.
  </Accordion>

  <Accordion title="400 Bad Request — amount exceeds remaining">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not capture: could not capture more than not captured yet: {{}}" }
    ```

    The amount is higher than the remaining uncaptured amount. Examples:

    1. Total 100, capture request 150.
    2. Total 100, one successful capture of 50, another capture for 100.
    3. Total 100, one successful capture of 100, another capture for 100.

    Case 3 is usually an unsafe retry — use an idempotency key instead of re-sending the capture.
  </Accordion>

  <Accordion title="400 Bad Request — payment not authorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not capture not authorized payments" }
    ```

    The payment status is not `AUTHORIZED`, so captures cannot be made. Check the payment status via `GET /api/v2/payments/{id}` and act accordingly.
  </Accordion>

  <Accordion title="400 Bad Request — already closed">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "already closed" }
    ```

    The payment is already in a terminal `CLOSED` state — usually a duplicate capture of an already fully-captured payment, or a capture after `close`. Check the payment status; if your first capture actually succeeded, no action is needed.
  </Accordion>

  <Accordion title="400 Bad Request — invalid payment id">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "invalid payment id" }
    ```

    The `payment_id` is not a valid UUID. Fix the format.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized" }
    ```

    The `Authorization` header is missing or wrong. It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — no such payment">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "no such payment" }
    ```

    No payment with this `payment_id` is visible to your API key — wrong id, or a payment of a different store / API key.
  </Accordion>

  <Accordion title="409 Conflict — concurrent request">
    ```json theme={"dark"}
    { "status": "error", "errorType": "conflict", "error": "could not acquire lock: payment is locked" }
    ```

    Another concurrent request for this payment is being processed. Retry shortly with the same idempotency key.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    If you send an idempotency key, retry safely per your retry policy. Without one, call `GET /api/v2/payments/{id}` first and check already-applied captures before retrying. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>

***

## `POST /api/v2/payments/{id}/refunds`

Refunds a captured (`CLOSED`) payment.

<Tip>
  Always send an idempotency key — your own unique `reference_id` in the request body (see [Idempotent requests](/pay-in-4-custom-integration/payment-processing#idempotent-requests)) — to make refund retries safe.
</Tip>

<AccordionGroup>
  <Accordion title="400 Bad Request — invalid amount format">
    ```json theme={"dark"}
    {
      "status": "error",
      "errorType": "bad_data",
      "errors": [
        { "field": "$.amount", "code": "invalid_value", "message": "money supports only 3 decimal places, but got: 5" }
      ]
    }
    ```

    The `amount` has a wrong value — too many decimal places for the currency, zero or negative (`"error": "refund amount is less then or equals zero"`), or not a number at all (`"error": "error decoding string 'NaN': can't convert NaN to decimal"`).
  </Accordion>

  <Accordion title="400 Bad Request — amount exceeds refundable">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "refund amount should be less or equal than billed amount" }
    ```

    The refund amount exceeds the still-refundable amount. Examples:

    1. Captured 100, refund request 150.
    2. Captured 100, one successful refund of 50, another refund for 100.
    3. Captured 100, one successful refund of 100, another refund for 100.

    Case 3 is usually an unsafe retry — use an idempotency key instead of re-sending the refund.
  </Accordion>

  <Accordion title="400 Bad Request — cannot refund payment">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "can not refund payment that has no captures and not closed" }
    ```

    The payment is not `CLOSED` or has no captures. Check the payment status and act accordingly.
  </Accordion>

  <Accordion title="400 Bad Request — already closed">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "already closed" }
    ```

    The payment is in a state that no longer accepts this refund (e.g. it was closed without captures). Check the payment status via `GET /api/v2/payments/{id}`.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized" }
    ```

    The `Authorization` header is missing or wrong. It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — no such payment">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "no such payment" }
    ```

    No payment with this `payment_id` is visible to your API key — wrong id, or a payment of a different store / API key.
  </Accordion>

  <Accordion title="409 Conflict">
    ```json theme={"dark"}
    { "status": "error", "error": "payment is disputed" }
    ```

    Two possible causes:

    1. **Active dispute on the payment** — the body carries `"error": "payment is disputed"`. The refund is blocked until the dispute is resolved; see [Disputes](/pay-in-4-custom-integration/disputes).
    2. **Concurrent refunds on the same payment** — the body carries `"error": "could not acquire lock: payment is locked"`. Another request is being processed; retry shortly with the same idempotency key.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    If you send an idempotency key, retry safely per your retry policy. Without one, call `GET /api/v2/payments/{id}` first and check already-applied refunds before retrying. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>

***

## `POST /api/v2/payments/{id}/close`

Closes an `AUTHORIZED` payment (e.g. when the order is cancelled before capture).

<AccordionGroup>
  <Accordion title="400 Bad Request — payment not authorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "only authorized payments can be closed" }
    ```

    Only `AUTHORIZED` payments can be closed. Check the payment status and act accordingly.
  </Accordion>

  <Accordion title="400 Bad Request — payment already expired">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "could not cancel payment: payment was already expired" }
    ```

    The payment was `AUTHORIZED` earlier, but the authorization expired before `close` was called. No action is needed — the customer is not charged; treat the payment as expired.
  </Accordion>

  <Accordion title="400 Bad Request — already closed">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "already closed" }
    ```

    The payment is already `CLOSED` — usually a duplicate `close` call. No action is needed.
  </Accordion>

  <Accordion title="400 Bad Request — invalid payment id">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "invalid payment id" }
    ```

    The `payment_id` is not a valid UUID. Fix the format.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized" }
    ```

    The `Authorization` header is missing or wrong. It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — no such payment">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "no such payment" }
    ```

    No payment with this `payment_id` is visible to your API key — wrong id, or a payment of a different store / API key.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    Call `GET /api/v2/payments/{id}` to check the current status before retrying. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>

***

## Webhooks — `/api/v1/webhooks`

Applies to `POST /api/v1/webhooks`, `GET /api/v1/webhooks`, `GET /api/v1/webhooks/{id}`, `PUT /api/v1/webhooks/{id}`, `DELETE /api/v1/webhooks/{id}`. Authenticate with the **Secret Key**.

<AccordionGroup>
  <Accordion title="400 Bad Request — invalid webhook url">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "create payment webhook: invalid webhook url" }
    ```

    The `url` is not a valid, publicly reachable HTTPS URL. Fix the URL.
  </Accordion>

  <Accordion title="400 Bad Request — webhook already exists">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "create payment webhook: webhook already exists" }
    ```

    A webhook with the same URL is already registered for this merchant. List the registered webhooks with `GET /api/v1/webhooks` instead of re-creating.
  </Accordion>

  <Accordion title="400 Bad Request — max number of webhooks reached">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "create payment webhook: max number of webhooks reached" }
    ```

    The merchant already has the maximum number of registered webhooks. Remove stale ones with `DELETE /api/v1/webhooks/{id}` before adding new.
  </Accordion>

  <Accordion title="400 Bad Request — malformed request">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "json decode: EOF" }
    ```

    The request body is empty or not valid JSON, or the webhook `id` in the path is not a valid UUID.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized", "error": "invalid secret key" }
    ```

    The `Authorization` header is missing or carries a wrong key (`api key not found` / `invalid secret key`). It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — no such webhook">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "no such webhook" }
    ```

    No webhook with this `id` exists for your merchant. List the registered webhooks with `GET /api/v1/webhooks`.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    Retry per your retry policy. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>

***

## Disputes — `/api/v1/disputes`

Applies to `GET /api/v1/disputes`, `GET /api/v1/disputes/{disputeId}`, `POST /api/v1/disputes/{disputeId}/provide-evidence`, `POST /api/v1/disputes/approve`, `POST /api/v1/disputes/challenge`, `POST /api/v1/disputes/attachments/upload`. Authenticate with the **Secret Key**. See [Disputes](/pay-in-4-custom-integration/disputes) for the flow itself.

<AccordionGroup>
  <Accordion title="400 Bad Request">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "failed to decode request" }
    ```

    The request body is malformed or a parameter is invalid (e.g. wrong `dispute_id` format, missing required fields). Read the `error` field for the specific cause.
  </Accordion>

  <Accordion title="401 Unauthorized">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_authorized" }
    ```

    The `Authorization` header is missing or wrong. It must be sent as `Authorization: Bearer {secret_key}`.
  </Accordion>

  <Accordion title="404 Not Found — dispute not found">
    ```json theme={"dark"}
    { "status": "error", "errorType": "not_found", "error": "dispute not found" }
    ```

    No dispute with this `disputeId` is visible to your API key. Check the id against `GET /api/v1/disputes`.
  </Accordion>

  <Accordion title="409 Conflict (provide-evidence)">
    ```json theme={"dark"}
    { "status": "error", "errorType": "conflict" }
    ```

    The dispute is not in a state that accepts this operation — e.g. evidence was already submitted or the dispute has been resolved. Re-fetch it with `GET /api/v1/disputes/{disputeId}` and check its status.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    ```json theme={"dark"}
    { "status": "error", "errorType": "internal", "error": "Internal Server Error" }
    ```

    Retry per your retry policy. If `500` persists, contact the Tabby Integrations team and check [tabby-status.com](https://www.tabby-status.com/).
  </Accordion>
</AccordionGroup>
