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

# Dispute webhooks

> Get notified when a dispute is opened on your payment and when its status changes: registration, payload, statuses, and delivery.

Dispute webhooks notify your endpoint about <a href="/pay-in-4-custom-integration/disputes">disputes</a> raised on your payments — when a customer opens a dispute, when you challenge it, and when it is resolved. This lets you react to disputes without polling the <a href="/api-reference/disputes/get-disputes-list">Disputes API</a>.

<Note>
  Disputes have **no test mode**: dispute webhooks are registered with a **live** secret key and are sent for live payments only (the same as the Disputes API).
</Note>

## How They Work

<Steps>
  <Step title="Register an endpoint">
    <a href="/api-reference/dispute-webhooks/register-a-dispute-webhook">Register a dispute webhook</a> for each `merchant_code` — the merchant code is passed in the **`X-Merchant-Code` header**, which is required on every dispute-webhook request:

    ```bash theme={"dark"}
    curl -X POST https://api.tabby.ai/api/v1/dispute-webhooks \
      -H "Authorization: Bearer sk_..." \
      -H "X-Merchant-Code: your_merchant_code" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-store.com/tabby/dispute-webhook",
        "header": { "title": "X-Auth-Key", "value": "your_random_signing_secret" }
      }'
    ```

    KSA merchants call `https://api.tabby.sa/api/v1/dispute-webhooks`. Each `merchant_code` can have up to **4 dispute webhooks** — the limit is separate from payment webhooks. The optional `header` is added to every notification so you can verify its origin.

    <Note>
      **Registration is self-service.** The same operations as for payment webhooks — register, list, retrieve, update and remove — are available under `/api/v1/dispute-webhooks`, see the <a href="/api-reference/dispute-webhooks/register-a-dispute-webhook">API reference</a>. A dispute webhook receives **all** dispute events of the merchant; there is no event filter.
    </Note>
  </Step>

  <Step title="Receive notifications">
    Tabby sends a POST request to your URL whenever a dispute is opened on one of your payments or its <a href="/pay-in-4-custom-integration/dispute-webhooks#dispute-statuses">status changes</a>.
  </Step>

  <Step title="Acknowledge with 200">
    Respond with a `200` HTTP status code to confirm the reception, and check the auth header to verify the request. Any other response (or no response) counts as a delivery error and triggers <a href="/pay-in-4-custom-integration/webhooks#retry-attempts">retries</a>.
  </Step>
</Steps>

### Differences From Payment Webhooks

| | <a href="/pay-in-4-custom-integration/webhooks">Payment webhooks</a> | Dispute webhooks |
| - | - | - |
| Endpoints | `/api/v1/webhooks` | `/api/v1/dispute-webhooks` |
| Secret key | `sk_...` or `sk_test_...` — the key sets the environment | live `sk_...` only — a test key gets `403 disputes have no test mode` |
| `X-Merchant-Code` | required (omission is tolerated for a key that maps to a single merchant) | required on every request, single-merchant keys included — otherwise `400 code is required` |
| Limit | 4 per `merchant_code` + key pair | 4 per `merchant_code`, counted separately |
| `header.value` in responses | returned as registered | masked: `****` followed by the last 4 characters (`****` alone for values of 8 characters or fewer) |
| Events | payment status changes | all dispute events, no filter |

A few rules to keep in mind when managing dispute webhooks:

* **URLs must be publicly reachable.** `localhost`, raw IP addresses and host names that do not resolve are rejected with `400 invalid webhook url`. Use HTTPS — the secret in `header` travels with every notification.
* **URLs are normalised and unique per merchant.** The scheme and host are lower-cased, a default port and a trailing slash are dropped before the URL is stored — `https://Store.com/hook/` and `https://store.com/hook` are the same webhook, and registering it twice returns `400 webhook already exists`. The path itself is case-sensitive.
* **`PUT` replaces the whole object.** Send both `url` and `header` when updating; a request without `header` removes the header.
* **The header value is a secret.** Tabby stores it in full but returns it masked in every response — keep your own copy of the value your endpoint validates against.

## Payload

A dispute webhook is a POST request with a JSON body that links the dispute to the affected payment:

```JSON theme={"dark"}
{
  "status": "pending",
  "dispute_id": "string",
  "payment_id": "string",
  "currency": "SAR",
  "created_at": "2026-06-15T13:08:54Z"
}
```

| Field | Description |
| - | - |
| `status` | The dispute event — see [Dispute statuses](#dispute-statuses) below. |
| `dispute_id` | ID of the dispute. Use it with the <a href="/api-reference/disputes/get-dispute-by-id">Get dispute by ID</a> endpoint to fetch full details. |
| `payment_id` | ID of the payment the dispute was raised against. Use it to correlate the dispute with the order in your system. |
| `currency` | ISO currency code of the payment (`SAR`, `AED`). The disputed amount is not part of the payload — fetch it with <a href="/api-reference/disputes/get-dispute-by-id">Get dispute by ID</a>. |
| `created_at` | When the dispute was created, in UTC, ISO 8601 datetime format. |

## Dispute statuses

The `status` field tells you what happened to the dispute:

| `status` | Meaning |
| - | - |
| `pending` | The customer opened a dispute on your payment. |
| `arbitration` | You challenged the dispute (for example, the amount is wrong) or provided the requested evidence, and it moved to arbitration. |
| `evidence_merchant` | Tabby support requested supporting evidence from you. |
| `approved` | The dispute was approved and the amount was refunded to the customer. |
| `declined` | The dispute was declined by Tabby support. |
| `cancelled` | The dispute was cancelled by the customer. |

## Delivery

Dispute webhooks are delivered to the URLs you registered under `/api/v1/dispute-webhooks` (separate from your payment webhooks), using the **same delivery mechanism as payment webhooks** — optional authentication header, retry policy, and server IPs. In particular:

* Acknowledge each delivery with `200` and process it asynchronously — see <a href="/pay-in-4-custom-integration/webhooks#best-practices">Best Practices</a>.
* Delivery order is not guaranteed and a notification may occasionally be delivered twice — deduplicate by `dispute_id` + `status`.
* Failed deliveries are retried — see <a href="/pay-in-4-custom-integration/webhooks#retry-attempts">Retry Attempts</a>.


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