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

> What to do after the customer completes the Tabby checkout: verify the payment, capture it, and process refunds when needed.

Once the customer completes the Tabby checkout, three server-side steps remain:

<Steps>
  <Step
    stepNumber={1}
    title={(
  <span>
    <a href="/pay-in-4-custom-integration/payment-processing#payment-verification">
      Verify
    </a>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;the payment status server-to-server
    </span>
  </span>
)}
  />

  <Step
    stepNumber={2}
    title={(
  <span>
    <a href="/pay-in-4-custom-integration/payment-processing#payment-capture">
      Capture
    </a>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;the payment — only captured payments are settled to you
    </span>
  </span>
)}
  />

  <Step
    stepNumber={3}
    title={(
  <span>
    <a href="/pay-in-4-custom-integration/payment-processing#payment-refund">
      Refund
    </a>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;the payment when needed
    </span>
  </span>
)}
  />
</Steps>

## Payment Verification

Never rely on the redirect alone — always verify the payment server-to-server before processing the order.

When the customer lands on one of your redirect URLs, call the <a href="/api-reference/payments/retrieve-a-payment">Retrieve Request</a> with the `payment_id` you received at <a href="/pay-in-4-custom-integration/checkout-flow#checkout-session-initiation">session creation</a> and check the `status` field:

| Customer redirected to | Expected payment `status` |
| - | - |
| Success URL | `AUTHORIZED` |
| Cancel URL | `EXPIRED` |
| Failure URL | `REJECTED` |

What to do next depends on the status (see <a href="/pay-in-4-custom-integration/payment-statuses">Payment Statuses</a> for the full lifecycle):

| Payment `status` | Your action |
| - | - |
| `AUTHORIZED` | Process the order in your OMS, <a href="/pay-in-4-custom-integration/payment-processing#payment-capture">capture the payment</a>, and notify the customer that the order is confirmed (Tabby also sends its own payment confirmation) |
| `REJECTED` / `EXPIRED` | Cancel or delete the order in your OMS |
| `CREATED` | The checkout is not finished yet — keep checking until the status changes or the session expires |

### Don't Miss Authorized Payments

A payment can become `AUTHORIZED` without a successful redirection — for example, the customer pays and then closes the browser page or mobile WebView before being redirected, or a network issue interrupts the redirect. Catch such payments in one of two ways:

* **Webhooks (recommended).** <a href="/pay-in-4-custom-integration/webhooks">Webhooks</a> are sent on every status change regardless of redirection. When you receive a webhook with the `authorized` status: if the order is still pending, process it in your OMS and capture the payment; if the order was already processed after the redirect, no action is needed.
* **Polling (fallback).** Set up a cron job calling the Retrieve Request every couple of minutes until the payment reaches `AUTHORIZED`, `REJECTED`, or `EXPIRED`.

<Tip>
  Webhooks use lowercase statuses (`"authorized"`), while the Retrieve Request 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>

### Session Expiration

By default, a Tabby checkout session expires **20 minutes** after creation, and the payment status changes to `EXPIRED` about **10 minutes** after that — roughly 30 minutes since creation in total. Once the payment is `EXPIRED`, it is terminal: stop checking it.

The session expiry timeout can be reduced on request — contact your assigned business manager in the Integrations thread.

## Payment Capture

After you verify that the payment is `AUTHORIZED`, send a <a href="/api-reference/payments/capture-a-payment">Capture Request</a> from your OMS to confirm the order to Tabby. Capturing does not charge the customer anything extra — it confirms the order from your end. Only captured payments are settled to you, and customer disputes are possible only for captured payments.

Capture the **full payment amount** — Tabby verifies that the amount in the Capture Request matches the payment amount. A successful capture returns `200` with the payment status `CLOSED`; if you get any other response, investigate before retrying. Partial captures are supported, but the best-practice flow is a full capture right after verification.

<Warning>
  Only an `AUTHORIZED` payment can be captured. Capture attempts in `CREATED`, `EXPIRED`, `CLOSED`, or `REJECTED` return a `400` error.
</Warning>

### Missing Captures

Tabby tracks non-captured payments. For the first **21 days** after authorization the payment is left untouched — you can capture or cancel it at any moment. After 21 days, Tabby may capture the remaining amount in full on its side, as a missing capture is usually caused by a technical issue. Tabby also notifies merchants when we observe regular capture issues.

You can find all `AUTHORIZED` payments on Merchant Dashboard by filtering orders by the `NEW` status (or by exporting payments as a CSV file) — resolve them by capturing or cancelling.

## Payment Refund

Use a refund to return a captured amount to the customer. Refunds can be made with the <a href="/api-reference/payments/refund-a-payment">Refund Request</a> or on Merchant Dashboard (<a href="https://merchant.tabby.ai/">merchant.tabby.ai</a> / <a href="https://merchant.tabby.sa/">merchant.tabby.sa</a> for KSA).

The request is the same for full and partial refunds — provide the `payment_id` and the amount:

* **Full refund** returns the total captured amount and can be performed only once.
* **Partial refund** returns less than the captured amount. A payment can be refunded multiple times, but the total cannot exceed the captured amount.

Refund constraints:

* Only a `CLOSED` payment can be refunded.
* Refunds are processed in the same currency as the captured payment.
* Refunds can be initiated within **180 days** from the payment creation.
* A processed refund cannot be cancelled.

### Refunds Troubleshooting

If your refund request didn't respond with `200`, check the following:

* Is the payment status `CLOSED`?
* Is the refund amount higher than the captured amount?
* Has the payment already been fully refunded?
* Does the amount format follow the <a href="/introduction/technical-requirements#amount">requirements</a>?

## Idempotent Requests

Capture and Refund requests support idempotency, so you can safely retry them after a connection error without performing the same operation twice. Pass your own unique key in the `reference_id` parameter of the request:

```JSON theme={"dark"}
{
  "reference_id": "some_key"
}
```

A retry with the same `reference_id` will not create a second capture or refund.

<Tip>
  **Derive the key from your order, not from the clock.** A `reference_id` like `capture-{your_order_id}` stays the same no matter which code path retries the capture; a timestamp-based key defeats idempotency — every retry becomes a "new" capture.
</Tip>

### Avoiding Duplicate Captures

In a typical integration **two code paths can try to capture the same payment**: the success-redirect handler (customer came back to your site) and the `authorized` webhook handler (fires regardless of the redirect). If both run, you get duplicate capture attempts — harmless with a shared `reference_id`, noisy `400 already closed` errors without one.

Guard the capture with both layers:

1. **Local claim** — before calling capture, atomically mark the payment as capture-in-progress in your database (e.g. an `UPDATE ... WHERE status = 'authorized'` that only one caller can win). The losing path skips the capture. If the capture API call fails, release the claim so a later retry can proceed.
2. **Shared `reference_id`** — both paths derive the same key from the order id, so even if the local guard misses, Tabby deduplicates the capture on its side.

The webhook-side check is described in <a href="/pay-in-4-custom-integration/webhooks#a-capture-confirmation-is-not-a-request-to-capture">Handling Edge Cases</a>: a webhook whose `captures` array is already non-empty is a confirmation, not a trigger.


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