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

# Custom Payment Links Integration

## Integration Overview

This integration allows customers to pay using Tabby by receiving a payment link via SMS/Push. The flow involves:

1. Creating a payment session
2. Sending payment link to customer
3. Customer completes payment on their device
4. Merchant receiving payment confirmation

<Note>
  To implement sending payment link via SMS/Push from Tabby side to a customer a POST request to an API endpoint `/api/v2/checkout/{id of session}/send_hpp_link` should be triggered from your side first. Instructions on how and when to implement it can be found below.
</Note>

## Quick Reference

| **API Endpoint**                                 | **Purpose**                    | **Method** |
| ------------------------------------------------ | ------------------------------ | ---------- |
| `/api/v2/checkout`                               | Create session and payment     | POST       |
| `/api/v2/checkout/{id of session}/send_hpp_link` | Send payment link via SMS/Push | POST       |
| `/api/v2/payments/{payment.id}`                  | Retrieve payment status        | GET        |
| `/api/v2/checkout/{id of session}/cancel`        | Cancel session                 | POST       |

| **Key Status Codes**            | **Description**                            |
| ------------------------------- | ------------------------------------------ |
| `CREATED`                       | Payment initiated, waiting for completion  |
| `AUTHORIZED`                    | Payment approved, not yet captured         |
| `CLOSED` with "captures" object | Payment approved and captured successfully |
| `REJECTED`                      | Payment declined                           |
| `EXPIRED`                       | Session/payment expired or cancelled       |

## Steps to Integrate Tabby using Custom Payment Links

<Steps>
  <Step
    stepNumber={1}
    title={(
  <span>
    <a href="https://merchant.tabby.ai/">
      Register with Tabby
    </a>
    <span style={{ fontWeight: 'normal', fontSize: '0.9em' }}>
      &nbsp;(KSA: <a href="https://merchant.tabby.sa/">merchant.tabby.sa</a>)
    </span>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;and finish the application
    </span>
  </span>
)}
  />

  <Step
    stepNumber={2}
    title={(
  <span style={{ fontWeight: 'normal' }}>
    Collect the Test API Keys and Merchant codes from Tabby Merchant Dashboard or your Tabby Account manager
  </span>
)}
  />

  <Step
    stepNumber={3}
    title={(
  <span>
    <span style={{ fontWeight: 'normal' }}>
      Set up the&nbsp;
    </span>
    <a href="/offline-payment-methods/custom-payment-links#create-session-and-payment-using-checkout-api">
      Tabby session creation from your terminal
    </a>
  </span>
)}
  />

  <Step
    stepNumber={4}
    title={(
  <span>
    <span style={{ fontWeight: 'normal' }}>
      Make sure a payment link is successfully&nbsp;
    </span>
    <a href="/offline-payment-methods/custom-payment-links#customer-payment-options">
      sent as an SMS to the customer
    </a>
  </span>
)}
  />

  <Step
    stepNumber={5}
    title={(
  <span>
    <span style={{ fontWeight: 'normal' }}>
      Set up&nbsp;
    </span>
    <a href="/offline-payment-methods/custom-payment-links#payment-processing">
      Payment Processing
    </a>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;on your Backend
    </span>
  </span>
)}
  />

  <Step
    stepNumber={6}
    title={(
  <span>
    <span style={{ fontWeight: 'normal' }}>
      Once the payment is complete -&nbsp;
    </span>
    <a href="/offline-payment-methods/custom-payment-links#print-a-receipt">
      print the receipt for the customer
    </a>
  </span>
)}
  />

  <Step
    stepNumber={7}
    title={(
  <span>
    <a href="/offline-payment-methods/custom-payment-links#testing-scenarios">
      Test your Integration,
    </a>
    <span style={{ fontWeight: 'normal' }}>
      &nbsp;contact Tabby Integrations Team in the Integration email thread to complete the testing process
    </span>
  </span>
)}
  />

  <Step
    stepNumber={8}
    title={(
  <span style={{ fontWeight: 'normal' }}>
    After successful testing passed receive the Live API keys and deploy to production
  </span>
)}
  />
</Steps>

## Integration Flow

```mermaid theme={"dark"}
sequenceDiagram
    autonumber
    participant Customer
    participant POS as POS Terminal
    participant Provider as POS Backend
    participant Checkout as Tabby Checkout
    participant TabbyAPI as Tabby API

    Customer ->>+ POS: Request to pay with Tabby
    POS ->>+ Provider: Initiate payment
    Provider ->>+ TabbyAPI: POST /api/v2/checkout<br/>{ amount, buyer.phone, ... }
    TabbyAPI -->>- Provider: Response<br/>{ id of session, status of session, payment.id, web_url }

    alt "status" of session == "created"
        Provider ->>+ TabbyAPI: POST /api/v2/checkout/{id of session}/send_hpp_link
        TabbyAPI -->>- Customer: Send checkout link via SMS/Push
        Provider -->> POS: Display "Waiting<br/>for customer payment"
        Note right of Customer: Customer receives<br/>and opens the link
    else "status" of session == "rejected"
        Provider -->> POS: Return "Payment rejected"
        POS -->> Customer: Show failure screen
        Note right of Customer: A different payment method <br />should be selected
    end

    Customer ->>+ Checkout: Open Tabby Checkout and go through payment steps
    loop Tabby checkout steps
    Checkout -->> Customer: Guide through required steps
    end

    opt
        alt Cashier cancels
          POS -->> Provider: Cancel request
          Provider -->> TabbyAPI: POST /api/v2/checkout/{id of session}/cancel
        else Customer cancels
          Customer -->> Checkout: Clicks 'Cancel' button on Tabby Checkout Page
        end
    end

    Provider ->>+ TabbyAPI: Check payment status<br/>(GET /api/v2/payments/{payment.id})
    TabbyAPI -->>- Provider: { payment.status: <br />AUTHORIZED | CLOSED | REJECTED | EXPIRED }
    Note over Provider,TabbyAPI: Or receive webhook with the same status

    alt payment.status == <br />"AUTHORIZED" OR "CLOSED"
        Provider -->> POS: Payment successful
        POS -->> Customer: Show success screen, Print receipt
    else payment.status == <br />"REJECTED" or "EXPIRED"
        Provider -->> POS: Payment failed
        POS -->> Customer: Show failure screen
    end
```

## Create Session and Payment Using Checkout API

Call the <a href="https://docs.tabby.ai/api-reference/checkout/create-a-session" rel="noopener noreferrer" target="_blank">Create a session API</a>. The required payload parameters for the session:

```JSON theme={"dark"}
{
  "payment": {
    "amount": "string", // Up to 2 decimals for AED and SAR, 3 decimals for KWD, e.g. 100.00
    "currency": "string", // Use the ISO 4217 standard for defining currencies: AED, SAR, KWD
    "buyer": {
      "phone": "string" // Required for sending Payment link
    },
    "order": {
      "reference_id": "string", // Merchant's Order Number to match the order with the payment.id
      "items": [
        {                    
          "title": "string", // Name of the product.
          "quantity": 1, // Quantity of the product ordered. Should be >= 1
          "unit_price": "0.00", // Price per unit of the product. Should be positive or zero.
          "category": "string" // Required as name of high-level category (Clothes, Electronics,etc.)
        }
      ]
    },
    "attachment": {
            "body": "{\"latitude\": latitude of the terminal as float,\"longitude\": longitude of the terminal as float,\"timestamp\": \"timestamp of the purchase in UTC, displayed in ISO 8601 datetime format\"}", // Example: "\"latitude\":24.4763,\"longitude\":54.3209,\"timestamp\": \"2026-02-27T14:35:10.123Z\""
            "content_type": "application/vnd.tabby.v1+json"
        },
  },
  "merchant_code": "string" // Merchant's branch code or MID
}
```

Even though other parameters are technically optional for offline integration, we highly recommend sharing other data marked as <code className="text-blue-600 dark:text-blue-300">required</code> in the <a href="https://docs.tabby.ai/api-reference/checkout/create-a-session" rel="noopener noreferrer" target="_blank">API Docs</a>, as additional data allows Tabby to increase the AOVs and conversion approval rates.

### Eligibility Check

As a response you receive one of the two session statuses - <code className="text-blue-600 dark:text-blue-300">"created"</code> or <code className="text-blue-600 dark:text-blue-300">"rejected"</code>:

* if the session status is <code className="text-blue-600 dark:text-blue-300">"created"</code> - save the **id of the session** (will be required for cancellation step) and **payment.id** (will be required for payment status check and refund steps) received in the response:

```
"status": "created"
"id": "string" // ID of the session
"payment"."id":"string" // ID of the payment
```

* if the session status is <code className="text-blue-600 dark:text-blue-300">"rejected"</code> - show the Payment failure screen and offer the customer an alternative payment method.

<Note>
  Please, do not proceed with any further steps with Tabby. The rejection might be related to order amount being too high, disabled branch code, or other reasons.
</Note>

The response payload will contain the following:

```
"status": "rejected",
"configuration"."products"."installments"."rejection_reason": "string" // reason for rejection
```

The <code className="text-blue-600 dark:text-blue-300">"rejection\_reason"</code> field can take the following values, you may optionally add human readable messages for cashier:

| Reason                  | English                                                                                                                    | Arabic                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `not_available`         | Sorry, Tabby is unable to approve this purchase. Please use an alternative payment method for your order.                  | نأسف، تابي غير قادرة على الموافقة على هذه العملية. الرجاء استخدام طريقة دفع أخرى.                           |
| `order_amount_too_high` | This purchase is above your current spending limit with Tabby, try a smaller cart or use another payment method            | قيمة الطلب تفوق الحد الأقصى المسموح به حاليًا مع تابي. يُرجى تخفيض قيمة السلة أو استخدام وسيلة دفع أخرى.    |
| `order_amount_too_low`  | The purchase amount is below the minimum amount required to use Tabby, try adding more items or use another payment method | قيمة الطلب أقل من الحد الأدنى المطلوب لاستخدام خدمة تابي. يُرجى زيادة قيمة الطلب أو استخدام وسيلة دفع أخرى. |

## Customer payment options

* **First option: Send the Payment Link** to the customer via SMS using **send\_hpp\_link API** (provided in a Postman Collection). You can use this method only if you receive a <code className="text-blue-600 dark:text-blue-300">"created"</code> status in the response to the previous request.
* **Second option: use the <a href="/offline-payment-methods/pos-integration" rel="noopener noreferrer" target="_blank">POS QR Code integration</a>** as a fallback option.

## Payment Processing

Verify the payment status using:

* <a href="/pay-in-4-custom-integration/webhooks" rel="noopener noreferrer" target="_blank">Webhooks</a>
* or <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a>

### Webhooks

* Tabby sends you a notification payment status update. The initial payment status is <code className="text-blue-600 dark:text-blue-300">CREATED</code>.
* If the Webhook with the <code className="text-blue-600 dark:text-blue-300">authorized</code> or <code className="text-blue-600 dark:text-blue-300">closed</code> status is received - mark the order as successful in your OMS. You can ignore other Webhooks received for this <code className="text-blue-600 dark:text-blue-300">payment.id</code>.
* If the Webhook returns a <code className="text-blue-600 dark:text-blue-300">rejected</code> status - mark the payment as unsuccessful and ask the customer to pay with another payment method.
* If no status is received - the cashier should have an option to cancel the payment.<br />
  **Optional**: You can also add a cancel button using the <a href="/offline-payment-methods/custom-payment-links#cancel-a-payment">Cancel Session API</a> (provided in a Postman Collection) when you want to expire the Tabby session if a customer asks to pay with another payment method or start a new Tabby session.

### Retrieve Request

An alternative way to verify a payment status is by polling status with the <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a>. You can call Retrieve Request by cron or by cashier's action (add button **Check status** to the POS). The following statuses can be received:

* <code className="text-blue-600 dark:text-blue-300">CREATED</code> - the payment has not been completed yet, wait for it to change to one of the terminal statuses.
* <code className="text-blue-600 dark:text-blue-300">AUTHORIZED</code> or <code className="text-blue-600 dark:text-blue-300">CLOSED</code> - a payment was placed successfully, mark orders as successful and proceed with the order on your POS/OMS.
* <code className="text-blue-600 dark:text-blue-300">REJECTED</code> or <code className="text-blue-600 dark:text-blue-300">EXPIRED</code> - a payment is not successful. Ask the customer to pay with a different payment method.

<Note>
  **You can use both** <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a> and <a href="/pay-in-4-custom-integration/webhooks" rel="noopener noreferrer" target="_blank">Webhooks</a> methods for speed and reliability.
</Note>

<Tip>
  It is an expected behaviour that webhooks return payment status in lower case - e.g., <code className="text-blue-600 dark:text-blue-300">authorized</code>, while <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Request</a> - in upper case: <code className="text-blue-600 dark:text-blue-300">AUTHORIZED</code>.
</Tip>

### Cancel a Payment

A request to cancel a payment is available in the Postman collection. The payment can only be canceled if its status is <code className="text-blue-600 dark:text-blue-300">CREATED</code>. Once canceled - the status will change to <code className="text-blue-600 dark:text-blue-300">EXPIRED</code>.

If the payment has already been authorized, attempting to cancel it will return the following error: <code className="text-blue-600 dark:text-blue-300">400 Bad Request</code>

```JSON theme={"dark"}
{
  "status": "error",
  "errorType": "bad_data",
  "error": "session is finalized"
}
```

In this case check the payment status via the <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a> and verify the status is <code className="text-blue-600 dark:text-blue-300">AUTHORIZED</code> or <code className="text-blue-600 dark:text-blue-300">CLOSED</code>. Then show a success screen, print a receipt and proceed with the order.

<Note>
  The Cancel API does not refund payments and can only be used to expire not finalised sessions. Once the payment receives one of the terminal statuses - <code className="text-blue-600 dark:text-blue-300">AUTHORIZED</code>, <code className="text-blue-600 dark:text-blue-300">CLOSED</code>, <code className="text-blue-600 dark:text-blue-300">REJECTED</code> or <code className="text-blue-600 dark:text-blue-300">EXPIRED</code> - the session cannot be cancelled.
</Note>

### Refund a Payment

You can process <a href="/pay-in-4-custom-integration/payment-processing#payment-refund" rel="noopener noreferrer" target="_blank">a Full or Partial Refund</a>. Call <a href="https://docs.tabby.ai/api-reference/payments/refund-a-payment" rel="noopener noreferrer" target="_blank">Refund API</a> for a specific payment.id with the desired amount. You can find the <code className="text-blue-600 dark:text-blue-300">payment.id</code> by matched <code className="text-blue-600 dark:text-blue-300">payment.order.reference\_id</code> in your OMS.

You can also process a refund from the Tabby Merchant Dashboard.

<Note>
  Only payment in status <code className="text-blue-600 dark:text-blue-300">CLOSED</code> with a captured amount present in the <code className="text-blue-600 dark:text-blue-300">"captures":\[]</code> array of objects can be refunded.<br />
  *On Merchant Dashboard such payment will have status <code className="text-blue-600 dark:text-blue-300">CAPTURED</code>.*
</Note>

## Print a Receipt

Show a success screen and print a receipt. The receipt data can be used to identify the order and payment, and (optionally) initiate a refund if your POS system provides this functionality.

| Receipt data template           |
| ------------------------------- |
| Merchant Order / Transaction ID |
| Date and Time                   |
| Tabby logo                      |
| Tabby Payment ID (optional)     |
| Merchant name (optional)        |

## Testing Scenarios

Kindly verify that your integration can handle all listed below scenarios.

### 1. Payment Success

**Testing Steps:**

1. From a Cashier's POS choose Tabby.
2. Enter payment amount and a real phone number to receive the real payment link.

<Note>
  If your phone number is not eligible for Tabby and the session is rejected, use another phone number or contact Tabby Integrations Team.
</Note>

3. Open received payment link.
4. On Tabby Checkout page enter credentials:

```
Positive flow:
UAE: otp.success@tabby.ai, phone: +971500000001
KSA: otp.success@tabby.ai, phone: +966500000001
Kuwait: otp.success@tabby.ai, phone: +96590000001
```

5. Complete the payment using <code className="text-blue-600 dark:text-blue-300">OTP:8888</code> on Tabby Checkout Page.
6. Verify that the successful payment status is received.

**Expected Results:**

1. Session creation response has status <code className="text-blue-600 dark:text-blue-300">"created"</code> - the customer is eligible to use Tabby.
2. A payment link is successfully sent as an SMS to the customer and Tabby Checkout Page opened.
3. Credentials are entered.
4. The success Tabby screen appears.
5. Payment is successful and captured:
   * on Merchant Dashboard payment status is <code className="text-blue-600 dark:text-blue-300">CAPTURED</code>
   * via a <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a> response Payment status is <code className="text-blue-600 dark:text-blue-300">CLOSED</code>, captured amount is present in the <code className="text-blue-600 dark:text-blue-300">"captures":\[]</code> array of objects.

<Warning>
  If a payment status remains <code className="text-blue-600 dark:text-blue-300">NEW</code> on the Merchant Dashboard or <code className="text-blue-600 dark:text-blue-300">AUTHORIZED</code> via Retrieve Payment API call - kindly contact your Tabby Account manager or `partner@tabby.ai` / `partner@tabby.sa` to update auto-capture settings.
</Warning>

### 2. Eligibility Check Reject

**Testing Steps:**

1. From a Cashier's POS choose Tabby.
2. The session should be created with the following phone number:

```
Eligibility Check Reject flow:
UAE: +971500000002
KSA: +966500000002
Kuwait: +96590000002
```

**Expected Results:**

1. Session creation response has status <code className="text-blue-600 dark:text-blue-300">"rejected"</code> - the customer is **not** eligible to use Tabby.
   * **Optionally**: one of the rejection reasons can be shown to cashier.

### 3. Payment Cancellation

**Testing Steps:**

1. From a Cashier's POS choose Tabby.
2. Enter payment amount and a real phone number to receive the real payment link.

<Note>
  If your phone number is not eligible for Tabby and the session is rejected, use another phone number or contact Tabby Integrations Team.
</Note>

3. Open received payment link.
4. Click 'Cancel' button on Tabby Checkout Page or cancel the payment from a Cashier's POS.
5. Verify the payment status via Retrieve Payment API.

**Expected Results:**

1. Session creation response has status <code className="text-blue-600 dark:text-blue-300">"created"</code> - the customer is eligible to use Tabby.
2. A payment link is successfully sent as an SMS to the customer, Tabby Checkout Page opens.
3. A session is cancelled.
4. On checking Payment Status via <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a> it should be <code className="text-blue-600 dark:text-blue-300">EXPIRED</code>.

<Note>
  By default, Tabby session expires after **20 minutes** since creation and customer is not able to continue the session. This **session expiry timeout** can be reduced by the request from the Merchant side to your assigned business manager in the Integrations thread.

  A payment status may change to <code className="text-blue-600 dark:text-blue-300">"EXPIRED"</code> after **session expiry timeout + 5 minutes** (20 + 5 by default). After that the payment will remain in status <code className="text-blue-600 dark:text-blue-300">"EXPIRED"</code>, no need to check it further.
</Note>

### 4. Payment Failure

**Testing Steps:**

1. From a Cashier's POS choose Tabby.
2. Enter payment amount and a real phone number to receive the real payment link.

<Note>
  If your phone number is not eligible for Tabby and the session is rejected, use another phone number or contact Tabby Integrations Team.
</Note>

3. Open received payment link.
4. On Tabby Checkout page enter credentials:

```
Negative flow:
UAE: otp.rejected@tabby.ai, phone: +971500000001
KSA: otp.rejected@tabby.ai, phone: +966500000001
Kuwait: otp.rejected@tabby.ai, phone: +96590000001
```

5. Finish the payment using <code className="text-blue-600 dark:text-blue-300">OTP:8888</code> on Tabby Checkout Page.
6. Verify the payment status via Retrieve Payment API.

**Expected Results:**

1. Session creation response has status <code className="text-blue-600 dark:text-blue-300">"created"</code> - the customer is eligible to use Tabby.
2. A payment link is successfully sent as an SMS to the customer.
3. Tabby Checkout Page opens, credentials are entered.
4. The rejection screen with the message 'We can't approve this purchase' appears.
5. On checking Payment Status via <a href="https://docs.tabby.ai/api-reference/payments/retrieve-a-payment" rel="noopener noreferrer" target="_blank">Retrieve Payment API call</a> it should be <code className="text-blue-600 dark:text-blue-300">REJECTED</code>.

## Postman Collection

1. Download the JSON file and import it into Postman.
2. Set your <code className="text-blue-600 dark:text-blue-300">base\_url</code>, <code className="text-blue-600 dark:text-blue-300">secret\_key</code>, <code className="text-blue-600 dark:text-blue-300">merchant\_code</code>, and <code className="text-blue-600 dark:text-blue-300">currency</code> in Collection Variables. See <a href="/api-reference/overview#base-urls">Base URLs</a> for regional domains.

<Card horizontal color="#00A462" href="/custom-pl-pos.json" icon="file-arrow-down" iconType="light" rel="noopener noreferrer" target="_blank">
  <span style={{ textDecoration: 'none', fontWeight: 'bold' }}>
    Payment Links / POS Collection
  </span>
</Card>

<Note>
  This API collection is used for both POS Integration and Custom Payment Links integration and includes all the integration steps.
</Note>
