> ## 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**.

<Note>
  On `api.tabby.ai` a missing or malformed `Authorization` header and an unknown secret key are rejected at the edge with an **empty body** (`400` for a missing header or a non-Bearer scheme, `401` for an unknown key). The JSON bodies below are returned once the request reaches the service (today every such request on `api.tabby.sa` reaches the service). Do not rely on a response body for `400`/`401` authentication errors; branch on the status code.
</Note>

<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>

***

## Dispute webhooks — `/api/v1/dispute-webhooks`

Applies to `POST /api/v1/dispute-webhooks`, `GET /api/v1/dispute-webhooks`, `GET /api/v1/dispute-webhooks/{id}`, `PUT /api/v1/dispute-webhooks/{id}`, `DELETE /api/v1/dispute-webhooks/{id}`. Authenticate with the **live Secret Key**; the `X-Merchant-Code` header is required on every request. See [Dispute webhooks](/pay-in-4-custom-integration/dispute-webhooks) for how they work.

<Note>
  On `api.tabby.ai` a missing or malformed `Authorization` header and an unknown secret key are rejected at the edge with an **empty body** (`400` for a missing header or a non-Bearer scheme, `401` for an unknown key); a merchant code that does not belong to the key may also be answered at the edge. The JSON bodies below are returned once the request reaches the service (today every such request on `api.tabby.sa` reaches the service). Do not rely on a response body for `400`/`401` authentication errors; branch on the status code. Always call the base URL of your merchant's region.
</Note>

<AccordionGroup>
  <Accordion title="400 Bad Request — code is required">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "code is required" }
    ```

    The `X-Merchant-Code` header is missing. It is required on every dispute-webhook request, even when your secret key maps to a single merchant.
  </Accordion>

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

    The `url` points to `localhost`, to a raw IP address, or to a host name that does not resolve in DNS. Use a publicly reachable host name (HTTPS recommended).
  </Accordion>

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

    A dispute webhook with the same URL is already registered for this merchant (on `PUT`: the new URL belongs to another of your dispute webhooks). URLs are compared after normalisation (lower-case scheme and host, default port and trailing slash removed). List the registered webhooks with `GET /api/v1/dispute-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": "max number of webhooks reached" }
    ```

    The merchant already has 4 dispute webhooks (the limit is separate from payment webhooks). Remove stale ones with `DELETE /api/v1/dispute-webhooks/{id}` before adding new.
  </Accordion>

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

    The request body is empty or not valid JSON, or `url` is missing, empty or not an absolute URL. Send a JSON object with a `url`, as in [Register a dispute webhook](/api-reference/dispute-webhooks/register-a-dispute-webhook).
  </Accordion>

  <Accordion title="400 Bad Request — invalid header">
    ```json theme={"dark"}
    { "status": "error", "errorType": "bad_data", "error": "bad_request", "errors": [ { "code": "invalid_value", "field": "$.header.title", "message": "empty value" } ] }
    ```

    `header` is present but `title` or `value` is missing, empty or invalid: `title` must be a valid HTTP header name (no spaces, up to 128 characters), `value` up to 1024 characters with no control characters. The `errors` array points at the offending field. Omit `header` entirely if you do not want one.
  </Accordion>

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

    The `Authorization` header is missing or wrong (`missing Authorization header`, `missing Bearer scheme`, `api key not found`, `invalid secret key` — the last one when a public `pk_` key is sent), or the `X-Merchant-Code` does not belong to the key you authorized with (`no accessible merchants found` — also returned for an unknown code). Send `Authorization: Bearer {secret_key}` together with one of the merchant codes of that key.
  </Accordion>

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

    The request was authorized with a test secret key (`sk_test_...`). Dispute webhooks exist for live payments only — use the live secret key (`sk_...`).
  </Accordion>

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

    No dispute webhook with this `id` exists for your merchant (including one that was already removed or belongs to another merchant), or the `id` is not a valid UUID. List the registered webhooks with `GET /api/v1/dispute-webhooks`.
  </Accordion>

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

    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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.