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

# Payment webhooks

> Get notified about payment status changes: registration, payload, supported events, delivery order, and retries.

<a href="/api-reference/webhooks">Tabby Webhooks</a> are HTTPS callbacks that notify you about payment-related and token-related events. You register a URL once, and Tabby sends a POST request to it whenever an event related to your account occurs — even when the customer never returns to your site. This makes webhooks the most reliable way to <a href="/pay-in-4-custom-integration/payment-processing#dont-miss-authorized-payments">catch authorized payments</a>.

## How They Work

<Steps>
  <Step title="Register an endpoint">
    <a href="/api-reference/webhooks/register-a-webhook">Register a webhook</a> for each `merchant_code` + secret key pair — the merchant code is passed in the **`X-Merchant-Code` header** (required):

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

    The environment is determined by the key you register with: a production key (`sk_...`) registers webhooks for production payments, a test key (`sk_test_...`) — for test payments. Each pair can have up to **4 webhooks**. The optional `header` signs the requests so you can verify their authenticity.

    <Note>
      **Webhook registration is self-service.** `POST /api/v1/webhooks` is a public endpoint — you don't need to ask Tabby to register, update or remove webhooks for you. Call `https://api.tabby.ai/api/v1/webhooks` with your secret key and the `X-Merchant-Code` header, as shown above; KSA merchants use `https://api.tabby.sa/api/v1/webhooks`. All webhook endpoints are listed in the <a href="/api-reference/webhooks/register-a-webhook">API reference</a>.
    </Note>
  </Step>

  <Step title="Receive notifications">
    Tabby sends a POST request to your URL whenever the <a href="/pay-in-4-custom-integration/webhooks#supported-events">payment 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>

## Payload

Webhooks are POST requests with a JSON body:

```JSON theme={"dark"}
{
  "id": "string",
  "created_at": "2021-09-14T13:08:54Z",
  "expires_at": "2022-09-14T13:08:54Z",
  "closed_at": "2021-09-14T13:09:45Z",
  "status": "closed",
  "is_test": false,
  "is_expired": false,
  "amount": "100",
  "currency": "SAR",
  "order": {
    "reference_id": "string"
  },
  "captures": [
    {
      "id": "string",
      "amount": "100",
      "created_at": "2021-09-14T13:09:45Z",
      "reference_id": "string"
    }
  ],
  "refunds": [
    {
      "id": "string",
      "amount": "100",
      "created_at": "2021-09-14T14:14:02Z",
      "reference_id": "string",
      "reason": "string"
    }
  ],
  "meta": {
    "order_id": null,
    "customer": null
  },
  "token": "string"
}
```

<Tip>
  Webhook payloads use lowercase statuses (`"authorized"`), while the <a href="/api-reference/payments/retrieve-a-payment">Retrieve Request</a> returns uppercase (`"AUTHORIZED"`) — this is expected. See <a href="/pay-in-4-custom-integration/payment-statuses#statuses-in-api-responses-vs-webhooks">Payment Statuses</a>.
</Tip>

## Supported Events

The payload content depends on the event:

| Event | Webhook payment status | Payload update |
| - | :-: | - |
| Authorize | authorized | "status": "authorized" |
| Capture | authorized | capture info is added to captures.\[] array |
| Close | closed | "status": "closed" and "closed\_at" updated |
| Reject | rejected | "status": "rejected" |
| Expire (Optional) | expired | "status": "expired", "expired\_at" and "is\_expired" updated |
| Refund | closed | refund info is added to refunds.\[] array |
| Update | the same as before the Update Request | order.reference\_id updated |

The "expire" event is optional — ask the Tabby team to enable it for your store if you want notifications when a payment is cancelled by the customer or expires.

## A Typical Payment

For a regular successful order you will receive three notifications:

1. **Payment authorized** — the payload status is `authorized`. Check the order and process it in your OMS if it wasn't processed yet, then send the <a href="/api-reference/payments/capture-a-payment">Capture Request</a>.
2. **Payment captured** — the payload status is still `authorized`, with your capture added to the `captures` array. No action is required.
3. **Payment closed** — the payload status is `closed`: the payment is completed and confirmed from both sides. No action is required.

<Note>
  Looking for notifications about disputes raised on your payments? See <a href="/pay-in-4-custom-integration/dispute-webhooks">Dispute webhooks</a> — a separate webhook family with its own endpoints under `/api/v1/dispute-webhooks`, registered with a live secret key.
</Note>

## Best Practices

* **Respond fast.** Acknowledge the webhook with `200` right away and process it asynchronously, instead of holding the response until processing is done.
* **Expect disorder and duplicates.** Webhooks are asynchronous: the delivery order is not guaranteed and the same event may occasionally arrive twice — ignore a notification you have already processed, and see <a href="/pay-in-4-custom-integration/webhooks#handling-edge-cases">Handling Edge Cases</a> below.
* **Filter events.** You receive notifications for all payment events — process only the ones you need.
* **Allowlist Tabby server IPs:**

  ```
  34.166.36.90
  34.166.35.211
  34.166.34.222
  34.166.37.207
  34.93.76.191
  34.166.128.182
  34.166.170.3
  34.166.249.7
  ```

To test and debug webhooks, use a tool like <a href="https://webhook.site/">Webhook.site</a> to inspect the payload and headers Tabby sends to your endpoint.

## Handling Edge Cases

These three situations occur in every production integration. Handling them wrong is the most common cause of "lost" payments and duplicate captures.

### The webhook can arrive before your own order is saved

Tabby sends the `authorized` webhook as soon as the customer completes the payment — which can be **before your checkout code has committed the order to your database**. If your handler looks up the order, finds nothing, and still acknowledges with `200`, that event is gone for good and the payment sits unprocessed.

**Do this instead:** when the webhook references a payment you cannot match yet, respond with a non-`200` status (e.g. `404`). Tabby treats it as a delivery error and <a href="/pay-in-4-custom-integration/webhooks#retry-attempts">retries</a> — by the next attempt your transaction has landed and the event processes normally. Acknowledge with `200` only when you either processed the event or deliberately chose to ignore it. It also pays to store the raw event before processing — an audit log of received webhooks makes every "where did the payment go" investigation trivial.

### Statuses only move forward

A `closed` event can arrive before the `authorized` one. Treat payment status as a one-way street — `authorized` → `closed` — and never downgrade: if your record already says `closed`, ignore a late `authorized` event instead of overwriting.

### A capture confirmation is not a request to capture

The second notification in <a href="/pay-in-4-custom-integration/webhooks#a-typical-payment">A Typical Payment</a> — `authorized` with your capture in the `captures` array — confirms your own capture. Before triggering a capture from a webhook, check that `captures` is empty; if it isn't, the payment is already being settled and no action is needed.

## Retry Attempts

A webhook request times out after **1 minute**. If it times out or gets any response other than `200`, Tabby resends it up to **4 more times** with an exponential interval between attempts (1–4 minutes). Retries don't block other notifications — Tabby keeps sending webhooks for other payment events as they occur.


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