> For the complete documentation index, see [llms.txt](https://docs.akinon.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.akinon.com/technical-guides/project-zero/next.js/plugins/tabby-payment-gateway-1.md).

# Tabby Payment Gateway

The `TabbyPaymentGateway` component provides seamless integration with Tabby, enabling installment and deferred payment options directly within your checkout experience. Built for compatibility with the Akinon ProjectZero platform, this extension securely manages customer data and transaction flows while maintaining flexibility for localized currency and language support.

With minimal setup, the component generates the necessary context—including order, user, and historical data—to initiate secure transactions via Tabby. The extension also handles hash-based request validation and supports easy deployment through environment-based configuration.

A single storefront may serve several countries, and Tabby issues a **separate merchant installation—its own extension URL and its own hash key—per country**. This guide covers both cases: a single installation, and several installations selected by currency. If you operate in more than one country, read [Multiple Tabby Installations](#why-this-is-needed) before you deploy; a single-installation setup will appear to work in staging and then fail for every currency except the default one.

This guide covers installation, configuration, usage, and internal mechanics to help you implement Tabby in your checkout flow with confidence.

## <mark style="color:red;">Installation Method</mark>

You can use the following command to install the extension with the latest plugins:

```bash
npx @akinon/projectzero@latest --plugins
```

### <mark style="color:red;">Props</mark>

| Prop         | Type   | Required | Description                                                                                                                                                    |
| ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| sessionId    | string | Yes      | The session identifier received from Tabby.                                                                                                                    |
| currency     | string | Yes      | Currency code used in the transaction (e.g., AED, SAR).                                                                                                        |
| locale       | string | Yes      | Language/locale code used for translations (e.g., en, ar).                                                                                                     |
| extensionUrl | string | Yes      | The base URL for the Tabby extension server. The component does **not** read this from the environment—the page must resolve it and pass it in.                |
| hashKey      | string | Yes      | Secret hash key used to generate a secure transaction hash. The component does **not** read this from the environment—the page must resolve it and pass it in. |

{% hint style="warning" %}
If `sessionId`, `currency` or `locale` is missing, the component renders nothing and the checkout silently stops. Verify all three before debugging further.
{% endhint %}

### <mark style="color:red;">Environment Variables</mark>

#### <mark style="color:red;">Single Installation</mark>

One country, one Tabby merchant. Add the following variables to your `.env` file:

```bash
TABBY_EXTENSION_URL=<your_extension_url>
TABBY_HASH_KEY=<your_hash_key>
```

#### <mark style="color:red;">Multiple Installations</mark>

One storefront, several countries. Append the **uppercase** currency code to each variable name:

```bash
TABBY_EXTENSION_URL_<CURRENCY>=<your_extension_url>
TABBY_HASH_KEY_<CURRENCY>=<your_hash_key>
```

{% hint style="warning" %}
Both variables are read on the server only. Do not prefix them with `NEXT_PUBLIC_`—the hash key is a secret and must never reach the browser.
{% endhint %}

## <mark style="color:red;">Usage Example</mark>

Create a file in the path matching your routing mode:

| Routing mode                   | Page path                                                               |
| ------------------------------ | ----------------------------------------------------------------------- |
| `usePzSegment: true` (default) | `src/app/[pz]/payment-gateway/tabby/page.tsx`                           |
| Legacy (`usePzSegment: false`) | `src/app/[commerce]/[locale]/[currency]/payment-gateway/tabby/page.tsx` |

For a **single installation**, pass the unsuffixed variables straight through:

```tsx
import { TabbyPaymentGateway } from '@akinon/pz-tabby-extension';
import { withSegmentDefaults } from '@akinon/next/hocs/server';
import { ResolvedPageProps } from '@akinon/next/types';
import { parsePzParams } from '@akinon/next/utils/pz-segments';
import settings from '@theme/settings';

const TabbyGateway = async ({ params, searchParams }: ResolvedPageProps) => {
  const { locale, currency } = parsePzParams(params, settings);

  return (
    <TabbyPaymentGateway
      sessionId={searchParams.get('sessionId')}
      currency={currency}
      locale={locale}
      extensionUrl={process.env.TABBY_EXTENSION_URL}
      hashKey={process.env.TABBY_HASH_KEY}
    />
  );
};

export default withSegmentDefaults(TabbyGateway, { segmentType: 'page' });
```

Two pieces of the platform do the work here:

`withSegmentDefaults` resolves the Next.js `params`/`searchParams` promises and normalises `searchParams` into a `URLSearchParams` instance. Without it, `searchParams.get` is not available.

`parsePzParams` reads the locale and currency for the request. In pz-segment mode it decodes them out of the `pz` route segment; in legacy mode it reads `params.locale` and `params.currency` directly, falling back to the defaults in `settings.js`. Because it covers both, **the page above is identical in either routing mode**—only the file path changes.

## <mark style="color:red;">Multiple Tabby Installations (Country / Currency Based)</mark>

### <mark style="color:red;">Why This Is Needed?</mark>

Tabby onboards merchants per country. A storefront selling in both the UAE and Saudi Arabia holds **two** Tabby merchant accounts, each with its own extension URL and its own hash key. Because the extension resolves configuration from environment variables, those variables have to be distinguishable per country—otherwise every currency is sent to whichever single installation happens to be configured.

The currency is used as the discriminator, because it is already carried through the request by the platform.

### <mark style="color:red;">Naming Convention</mark>

Append the uppercase currency code to the base variable name:

| Country              | Currency | Variables                                                                            |
| -------------------- | -------- | ------------------------------------------------------------------------------------ |
| United Arab Emirates | AED      | <p><code>TABBY\_EXTENSION\_URL\_AED</code><br><code>TABBY\_HASH\_KEY\_AED</code></p> |
| Saudi Arabia         | SAR      | <p><code>TABBY\_EXTENSION\_URL\_SAR</code><br><code>TABBY\_HASH\_KEY\_SAR</code></p> |
| Kuwait               | KWD      | <p><code>TABBY\_EXTENSION\_URL\_KWD</code><br><code>TABBY\_HASH\_KEY\_KWD</code></p> |

The suffix is always uppercase, regardless of how the currency is written in `settings.js` or in the URL. The extension uppercases the active currency before building the variable name, so a currency configured as `aed` resolves to `TABBY_EXTENSION_URL_AED`.

### <mark style="color:red;">Resolution Rules</mark>

The currency-specific variable is preferred; the unsuffixed variable is the fallback:

| Configured                                 | Result for currency AED                |
| ------------------------------------------ | -------------------------------------- |
| `TABBY_HASH_KEY_AED` only                  | Uses `TABBY_HASH_KEY_AED`              |
| `TABBY_HASH_KEY` only                      | Uses `TABBY_HASH_KEY`                  |
| Both                                       | Uses `TABBY_HASH_KEY_AED`              |
| `TABBY_HASH_KEY_AED` set to an empty value | Falls back to `TABBY_HASH_KEY`         |
| Neither                                    | Availability endpoint returns HTTP 500 |

The extension URL follows the same rules. The two are resolved independently, so it is possible—and is a common misconfiguration—to end up with the URL of one installation and the hash key of another.

### <mark style="color:red;">What Resolves Automatically vs. What Doesn’t</mark>

This is the single most important distinction in this page:

| Part                            | Resolves currency-specific env? | Notes                                                                                      |
| ------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------ |
| Check Availability API route    | Yes, automatically              | Reads the `pz-currency` cookie, uppercases it, and falls back to the unsuffixed variables. |
| `TabbyPaymentGateway` component | **No**                          | Receives `extensionUrl` and `hashKey` as props. Your page must perform the selection.      |

{% hint style="warning" %}
If you configure the suffixed variables but leave the gateway page passing `process.env.TABBY_EXTENSION_URL`, the availability check will correctly consult the per-country installation while the payment form posts to the fallback one. Availability then looks healthy and the payment itself fails—which is exactly the symptom that is hard to trace without knowing this asymmetry exists.
{% endhint %}

### <mark style="color:red;">Resolving the Configuration on Your Page</mark>

Add a small helper to your application. It applies the same rules the API route applies—uppercase the currency, prefer the suffixed variable, fall back to the unsuffixed one:

```tsx
export const resolveTabbyConfig = (currency: string) => {
  const code = currency.toUpperCase();

  return {
    extensionUrl:
      process.env[`TABBY_EXTENSION_URL_${code}`] ||
      process.env.TABBY_EXTENSION_URL,
    hashKey:
      process.env[`TABBY_HASH_KEY_${code}`] || process.env.TABBY_HASH_KEY
  };
};
```

Then use it in the gateway page, taking the currency from `parsePzParams`:

```tsx
import { TabbyPaymentGateway } from '@akinon/pz-tabby-extension';
import { withSegmentDefaults } from '@akinon/next/hocs/server';
import { ResolvedPageProps } from '@akinon/next/types';
import { parsePzParams } from '@akinon/next/utils/pz-segments';
import settings from '@theme/settings';
import { resolveTabbyConfig } from '@theme/utils/tabby';

const TabbyGateway = async ({ params, searchParams }: ResolvedPageProps) => {
  const { locale, currency } = parsePzParams(params, settings);
  const { extensionUrl, hashKey } = resolveTabbyConfig(currency);

  return (
    <TabbyPaymentGateway
      sessionId={searchParams.get('sessionId')}
      currency={currency}
      locale={locale}
      extensionUrl={extensionUrl}
      hashKey={hashKey}
    />
  );
};

export default withSegmentDefaults(TabbyGateway, { segmentType: 'page' });
```

The currency `parsePzParams` returns and the `pz-currency` cookie the API route reads are written from the same resolved value in the same middleware pass, so the page and the availability endpoint always select the same installation. Keeping the fallback operator as `||`—not `??`—matches the route exactly: an empty value falls through to the unsuffixed variable in both.

### <mark style="color:red;">Example Environment Files</mark>

#### <mark style="color:red;">Single Installation — United Arab Emirates Only</mark>

```bash
TABBY_EXTENSION_URL=https://tabby.example.com/extension
TABBY_HASH_KEY=8f2a1c9e4b7d6035
```

#### <mark style="color:red;">Multiple Installations — UAE & Saudi Arabia</mark>

```bash
TABBY_EXTENSION_URL_AED=https://tabby-ae.example.com/extension
TABBY_HASH_KEY_AED=8f2a1c9e4b7d6035

TABBY_EXTENSION_URL_SAR=https://tabby-sa.example.com/extension
TABBY_HASH_KEY_SAR=5b3e7a2f9c1d4806
```

#### <mark style="color:red;">Multiple Installations with a Default</mark>

Currencies without their own pair fall back to the unsuffixed values. Use this only when the fallback installation is genuinely correct for every remaining currency:

```bash
TABBY_EXTENSION_URL=https://tabby-ae.example.com/extension
TABBY_HASH_KEY=8f2a1c9e4b7d6035

TABBY_EXTENSION_URL_SAR=https://tabby-sa.example.com/extension
TABBY_HASH_KEY_SAR=5b3e7a2f9c1d4806
```

{% hint style="warning" %}
Define each installation's URL and hash key **as a pair**. Setting `TABBY_EXTENSION_URL_SAR` without `TABBY_HASH_KEY_SAR` sends SAR traffic to the Saudi installation while signing it with the fallback key, and every request is rejected.
{% endhint %}

## <mark style="color:red;">API Routes</mark>

### <mark style="color:red;">Check Availability API</mark>

To enable Tabby payment availability checks, create an API route at `src/app/api/tabby-check-availability/route.ts`:

```typescript
import { POST } from '@akinon/pz-tabby-extension/src/pages/api/check-availability';

export { POST };
```

This endpoint checks whether Tabby is available for a given order amount, email, phone number and currency. It reads the currency from the `pz-currency` cookie—set by the platform middleware—and selects the matching installation as described above. It then validates both the outgoing request and the incoming response with hash-based security measures.

**Responses:**

| Status | Body                                                  | Meaning                                                          |
| ------ | ----------------------------------------------------- | ---------------------------------------------------------------- |
| 200    | `{ salt, hash, is_available }`                        | Availability resolved and the response hash verified.            |
| 400    | `Currency not found in cookies`                       | The `pz-currency` cookie is absent.                              |
| 400    | `Missing required fields`                             | One of `amount`, `phone`, `email`, `name` is missing.            |
| 400    | `Invalid response hash`                               | The extension server signed its reply with a different hash key. |
| 500    | `TABBY_HASH_KEY environment variable is not set`      | Neither the currency-specific nor the fallback key is defined.   |
| 500    | `TABBY_EXTENSION_URL environment variable is not set` | Neither the currency-specific nor the fallback URL is defined.   |

The 500 messages name the unsuffixed variables even when a suffixed one was expected. Read them as "no value could be resolved for the active currency", not as "define `TABBY_HASH_KEY`".

### <mark style="color:red;">Using checkTabbyAvailability Mutation</mark>

The extension provides a Redux mutation hook for availability checks:

```typescript
import { useCheckTabbyAvailabilityMutation } from '@akinon/pz-tabby-extension/src/redux/api';

const YourComponent = () => {
  const [checkTabbyAvailability] = useCheckTabbyAvailabilityMutation();
  const [isTabbyAvailable, setIsTabbyAvailable] = useState(false);

  useEffect(() => {
    const checkAvailability = async () => {
      try {
        const response = await checkTabbyAvailability({
          amount: '1000',
          phone: '+971123456789',
          email: 'example@example.com',
          name: 'Akinon Akinon'
        }).unwrap();

        setIsTabbyAvailable(response.is_available);
      } catch (error) {
        console.error('Error checking Tabby availability:', error);
        setIsTabbyAvailable(false);
      }
    };

    checkAvailability();
  }, [checkTabbyAvailability]);

  return null;
};
```

The mutation returns:

* `is_available`: boolean indicating whether Tabby payment is available
* `salt`: string used for hash verification
* `hash`: string for response validation

The currency is not a parameter—it is taken from the cookie on the server. Sending a different currency in the request body has no effect on which installation is consulted.

## <mark style="color:red;">Context Object (Auto-generated Internally)</mark>

The `TabbyPaymentGateway` component internally generates a `context` object using:

* `preOrder` data from the current checkout session
* `userProfile` and `wishlist` details
* Historical orders and previous purchases

This context includes:

```tsx
{
  salt: string,
  hash: string,
  shipping_address: {
    city: string,
    address: string,
    zip: string
  },
  order_items: Array<{
    unit_price: number,
    title: string,
    quantity: number,
    category: string
  }>,
  buyer_history: {
    registered_since: string,
    loyalty_level: number,
    wishlist_count: number,
    is_email_verified: boolean,
    is_social_networks_connected: boolean,
    is_phone_number_verified: boolean
  },
  order_history: Array<{
    purchased_at: string,
    amount: number,
    payment_method: string,
    status: string,
    buyer: {
      phone: string,
      email: string,
      name: string
    },
    shipping_address: {
      city: string,
      address: string,
      zip: string
    },
    order_items: object
  }>
}
```

This object is passed to the `FormComponent` for completing the payment via Tabby. The commerce requests that build it are sent with an `X-Currency` header taken from the `currency` prop, so passing a currency that does not match the resolved installation produces a context describing one country and a signature belonging to another.

## <mark style="color:red;">Hash Reference</mark>

All hashes are SHA-512 over the values joined by `|`. Knowing the inputs makes a mismatch straightforward to diagnose:

| Where                       | Hashed value                                  |
| --------------------------- | --------------------------------------------- |
| Payment gateway form        | `salt \| sessionId \| hashKey`                |
| Availability request        | `salt \| amount \| email \| phone \| hashKey` |
| Availability response check | `salt \| "True" or "False" \| hashKey`        |

The hash key is the only secret in each of these. If a hash is rejected and the other inputs are demonstrably correct, the key belongs to a different installation.

## <mark style="color:red;">Troubleshooting</mark>

| Symptom                                                                               | Likely cause                                                                                                | Resolution                                                                                                                                          |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Availability check succeeds, but the payment form is rejected by the extension server | The API route resolved the currency-specific installation while the gateway page passed the unsuffixed one. | Resolve `extensionUrl` and `hashKey` in the page as shown in [Resolving the configuration in your page](#resolving-the-configuration-in-your-page). |
| Customer is redirected to the wrong country's Tabby checkout                          | `TABBY_EXTENSION_URL_<CURRENCY>` is missing for that currency, so the fallback URL was used.                | Define the pair for every currency you sell in, or confirm the fallback is correct for the remainder.                                               |
| HTTP 400 `Invalid response hash`                                                      | The hash key does not match the installation the request was sent to.                                       | Confirm the URL and key come from the same installation. A URL/key pair split across two installations produces exactly this.                       |
| HTTP 400 `Currency not found in cookies`                                              | The `pz-currency` cookie was not set on the request.                                                        | Confirm the request passes through the platform middleware and that the storefront was reached on a currency-bearing route.                         |
| HTTP 500 `... environment variable is not set`                                        | Neither the suffixed nor the unsuffixed variable is defined for the active currency.                        | Add the pair for that currency, or an unsuffixed fallback pair.                                                                                     |
| The suffixed variable is defined but appears to be ignored                            | The suffix does not match the uppercased currency, or the value is an empty string.                         | Match the suffix to the uppercase currency code exactly, and give it a non-empty value—an empty value falls through to the fallback.                |
| The gateway page renders nothing                                                      | `sessionId`, `currency` or `locale` is missing.                                                             | The component returns an empty fragment when any of the three is absent. Verify the query string and the routing mode.                              |
| Environment variable changes have no effect                                           | The values are read at request time on the server, but the deployment was not restarted.                    | Restart the application after changing environment variables.                                                                                       |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.akinon.com/technical-guides/project-zero/next.js/plugins/tabby-payment-gateway-1.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
