# Glomo Unified SDK (Web)

Official JavaScript SDK for integrating Glomo payment checkout flows into your web applications. The Unified SDK supports standard payments, LRS (Liberalised Remittance Scheme) payments, subscriptions, and payment sessions — automatically detecting the correct flow based on your configuration.

## Prerequisites

Before using this SDK, you need:

- API credentials (Public Key) from your Glomo [dashboard](https://app.glomopay.com/api-keys-and-webhooks/api-keys)
- A created order (`order_id`), subscription (`subscription_id`), or payment session (`paymentSessionId` + `paymentSessionToken`)
- A modern browser with ES module support


## Installation

Import the SDK directly from the hosted URL using an ES module import:

```html
<script type="module">
  import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';
</script>
```

## Integration Guides

The SDK supports three integration journeys. Choose the one that matches your use case.

### Journey 1: Payment Session (Integrated LRS & KYC)

Use this when you have created a payment session via the Glomo API. Payment sessions bundle the LRS checkout flow with integrated KYC verification. When using a payment session, you do **not** need a `publicKey` or `orderId`.

**What you need:**

- `paymentSessionId` — the payment session ID returned when you [create a payment session](/api-reference/openapi/payment_session/createpaymentsession)
- `paymentSessionToken` — the JWT token returned alongside the payment session ID


HTML
```html
<button id="pay-button">Pay with Payment Session</button>
<script type="module">
  import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';

  const checkout = new GlomoCheckoutApi({
    paymentSessionId: 'ps_679a16457aP6K',                  // Payment session ID from your server
    paymentSessionToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI...',  // JWT token from your server
  });

  checkout.on('payment.success', function (response) {
    console.log('Payment successful:', response);
    // response contains: { paymentId, orderId, signature, status }
  });

  checkout.on('payment.failure', function (response) {
    console.log('Payment failed:', response);
  });

  checkout.on('checkout.closed', function () {
    console.log('Checkout closed by user');
  });

  document.getElementById('pay-button').addEventListener('click', () => {
    checkout.open();
  });
</script>
```

React (TS)
```tsx
import { FC, useEffect, useRef } from "react";

interface PaymentSessionButtonProps {
  paymentSessionId: string;
  paymentSessionToken: string;
}

const PaymentSessionButton: FC<PaymentSessionButtonProps> = ({
  paymentSessionId,
  paymentSessionToken,
}) => {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    let checkout: any;

    const loadCheckout = async () => {
      try {
        const { GlomoCheckoutApi } = await import(
          'https://unified-sdk.glomopay.com/index.js'
        );

        checkout = new GlomoCheckoutApi({ paymentSessionId, paymentSessionToken });

        checkout.on('payment.success', (response: unknown) => {
          console.log('Payment successful:', response);
        });

        checkout.on('payment.failure', (response: unknown) => {
          console.error('Payment failed:', response);
        });

        checkout.on('checkout.closed', () => {
          console.log('Checkout closed by user');
        });

        if (buttonRef.current) {
          buttonRef.current.addEventListener('click', () => checkout.open());
        }
      } catch (error) {
        console.error('Error loading the checkout SDK:', error);
      }
    };

    loadCheckout();
  }, [paymentSessionId, paymentSessionToken]);

  return <button ref={buttonRef}>Pay now</button>;
};
```

### Journey 2: Order Checkout (Standard & LRS)

Use this when you have created an order via the Glomo API and have an `orderId`. The SDK automatically detects whether the order is a standard payment or an LRS payment — no additional configuration is needed.

An account funding order uses this journey too. See [Add funds with a card](/payin/account-funding).

**What you need:**

- `publicKey` — your public key from the Glomo [dashboard](https://app.glomopay.com/api-keys-and-webhooks/api-keys)
- `orderId` — the order ID returned when you [create an order](/payin/order) on your server (starts with `order_`)


HTML
```html
<button id="buy-button">Buy Now</button>
<script type="module">
  import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';

  const checkout = new GlomoCheckoutApi({
    publicKey: 'live_687b0151Bid24PAI', // Your public key from the dashboard
    orderId: 'order_679a16457aP6K',     // Order ID from your server
  });

  // Called when the payment completes successfully.
  // Send the response to your server for signature verification.
  checkout.on('payment.success', function (response) {
    console.log('Payment successful:', response);
    // response contains: { paymentId, orderId, signature, status }
  });

  // Called when the payment fails. The failure event carries no signature.
  checkout.on('payment.failure', function (response) {
    console.log('Payment failed:', response);
    // response contains: { orderId, paymentId, status, error: { code, message } }
  });

  // Called when the user closes the checkout without completing payment.
  checkout.on('checkout.closed', function () {
    console.log('Checkout closed by user');
  });

  // For bank transfer payments only:
  // Emitted when the user submits their bank transfer details.
  // This does NOT mean the payment is complete — wait for a webhook to confirm.
  checkout.on('payment.bank_transfer_submitted', function (response) {
    console.log('Bank transfer submitted:', response);
    // response contains: { status, orderId, senderAccountNumber, senderName, payerRelationship }
  });

  document.getElementById('buy-button').addEventListener('click', () => {
    checkout.open();
  });
</script>
```

React (TS)
```tsx
import { FC, useEffect, useRef } from "react";

interface CheckoutButtonProps {
  orderId: string;
  publicKey: string;
}

const CheckoutButton: FC<CheckoutButtonProps> = ({ orderId, publicKey }) => {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    let checkout: any;

    const loadCheckout = async () => {
      try {
        const { GlomoCheckoutApi } = await import(
          'https://unified-sdk.glomopay.com/index.js'
        );

        checkout = new GlomoCheckoutApi({ orderId, publicKey });

        checkout.on('payment.success', (response: unknown) => {
          console.log('Payment successful:', response);
        });

        checkout.on('payment.failure', (response: unknown) => {
          console.error('Payment failed:', response);
        });

        checkout.on('checkout.closed', () => {
          console.log('Checkout closed by user');
        });

        if (buttonRef.current) {
          buttonRef.current.addEventListener('click', () => checkout.open());
        }
      } catch (error) {
        console.error('Error loading the checkout SDK:', error);
      }
    };

    loadCheckout();
  }, [orderId, publicKey]);

  return <button ref={buttonRef}>Pay now</button>;
};
```

#### Order Checkout with `callbackUrl`

If you prefer to handle the payment result server-side via a redirect instead of in JavaScript, pass `callbackUrl`. The SDK will redirect the user to your URL with the payment result as query parameters. Terminal `.on()` events (`payment.success`, `payment.failure`, `payment.bank_transfer_submitted`) will not fire.

HTML
```html
<button id="buy-button">Buy Now</button>
<script type="module">
  import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';

  const checkout = new GlomoCheckoutApi({
    publicKey: 'live_687b0151Bid24PAI',                      // Your public key from the dashboard
    orderId: 'order_679a16457aP6K',                          // Order ID from your server
    callbackUrl: 'https://yourwebsite.com/payment-result',   // Redirect URL for payment result
  });

  // No payment.success or payment.failure listeners needed —
  // when callbackUrl is set, the SDK redirects instead of emitting these events.

  // checkout.closed still fires, so you can handle abandonment.
  checkout.on('checkout.closed', function () {
    console.log('Checkout closed by user');
  });

  document.getElementById('buy-button').addEventListener('click', () => {
    checkout.open();
  });
</script>
```

React (TS)
```tsx
import { FC, useEffect, useRef } from "react";

interface CheckoutButtonProps {
  orderId: string;
  publicKey: string;
  callbackUrl: string;
}

const CheckoutButton: FC<CheckoutButtonProps> = ({ orderId, publicKey, callbackUrl }) => {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    let checkout: any;

    const loadCheckout = async () => {
      try {
        const { GlomoCheckoutApi } = await import(
          'https://unified-sdk.glomopay.com/index.js'
        );

        checkout = new GlomoCheckoutApi({ orderId, publicKey, callbackUrl });

        // No payment.success or payment.failure listeners needed —
        // when callbackUrl is set, the SDK redirects instead of emitting these events.

        // checkout.closed still fires, so you can handle abandonment.
        checkout.on('checkout.closed', () => {
          console.log('Checkout closed by user');
        });

        if (buttonRef.current) {
          buttonRef.current.addEventListener('click', () => checkout.open());
        }
      } catch (error) {
        console.error('Error loading the checkout SDK:', error);
      }
    };

    loadCheckout();
  }, [orderId, publicKey, callbackUrl]);

  return <button ref={buttonRef}>Pay now</button>;
};
```

### Journey 3: Subscription Checkout

Use this when you have created a subscription and want to collect the first payment or set up recurring billing.

**What you need:**

- `publicKey` — your public key from the Glomo [dashboard](https://app.glomopay.com/api-keys-and-webhooks/api-keys)
- `subscriptionId` — the subscription ID returned when you [create a subscription](/payin/subscriptions) on your server


> **Important:** You cannot pass both `orderId` and `subscriptionId` — use one or the other.


HTML
```html
<button id="subscribe-button">Subscribe</button>
<script type="module">
  import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';

  const checkout = new GlomoCheckoutApi({
    publicKey: 'live_687b0151Bid24PAI',    // Your public key from the dashboard
    subscriptionId: 'sub_679a16457aP6K',   // Subscription ID from your server
  });

  checkout.on('payment.success', function (response) {
    console.log('Subscription payment successful:', response);
    // response contains: { paymentId, orderId, signature, status }
  });

  checkout.on('payment.failure', function (response) {
    console.log('Subscription payment failed:', response);
  });

  checkout.on('checkout.closed', function () {
    console.log('Checkout closed by user');
  });

  document.getElementById('subscribe-button').addEventListener('click', () => {
    checkout.open();
  });
</script>
```

React (TS)
```tsx
import { FC, useEffect, useRef } from "react";

interface SubscribeButtonProps {
  subscriptionId: string;
  publicKey: string;
}

const SubscribeButton: FC<SubscribeButtonProps> = ({ subscriptionId, publicKey }) => {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    let checkout: any;

    const loadCheckout = async () => {
      try {
        const { GlomoCheckoutApi } = await import(
          'https://unified-sdk.glomopay.com/index.js'
        );

        checkout = new GlomoCheckoutApi({ publicKey, subscriptionId });

        checkout.on('payment.success', (response: unknown) => {
          console.log('Subscription payment successful:', response);
        });

        checkout.on('payment.failure', (response: unknown) => {
          console.error('Subscription payment failed:', response);
        });

        checkout.on('checkout.closed', () => {
          console.log('Checkout closed by user');
        });

        if (buttonRef.current) {
          buttonRef.current.addEventListener('click', () => checkout.open());
        }
      } catch (error) {
        console.error('Error loading the checkout SDK:', error);
      }
    };

    loadCheckout();
  }, [subscriptionId, publicKey]);

  return <button ref={buttonRef}>Subscribe</button>;
};
```

## Configuration Reference

The `GlomoCheckoutApi` constructor accepts a configuration object. The required parameters depend on which journey you are using:

| Parameter | Type | Journey 1 (Payment Session) | Journey 2 (Order) | Journey 3 (Subscription) | Description |
|  --- | --- | --- | --- | --- | --- |
| `paymentSessionId` | `string` | Required | Not used | Not used | Payment session identifier. |
| `paymentSessionToken` | `string` | Required | Not used | Not used | JWT token for the payment session. Required when `paymentSessionId` is provided. |
| `publicKey` | `string` | Not used | Required | Required | Your Glomo public key. If it starts with `test_`, mock mode is enabled automatically. |
| `orderId` | `string` | Not used | Required | Not used | The order ID for this transaction. Must start with `order_`. |
| `subscriptionId` | `string` | Not used | Not used | Required | Subscription ID for subscription payments. Cannot be used together with `orderId`. |
| `callbackUrl` | `string` | Optional | Optional | Optional | URL to redirect the user after a terminal payment event (`payment.success`, `payment.failure`, `payment.bank_transfer_submitted`). When set, `.on()` listeners for these events are **not invoked** — only the redirect fires. Must be an `http:` or `https:` URL. |
| `appColorScheme` | `string` | Optional | Optional | Optional | Colour scheme of your page: `'light'`, `'dark'` or `'system'`. Checkout matches it when dark mode is enabled for your account. Defaults to light. See [Dark mode](#dark-mode). |


**Validation rules:**

- **Journey 1:** Both `paymentSessionId` and `paymentSessionToken` are required. `publicKey` and `orderId` are not needed
- **Journey 2:** Both `publicKey` and `orderId` are required
- **Journey 3:** Both `publicKey` and `subscriptionId` are required. Cannot be combined with `orderId`
- **callbackUrl (all journeys):** When provided, the SDK navigates the parent window to this URL on payment completion, failure, or bank transfer submission. The event payload fields are appended as query string parameters. Terminal event listeners (`.on('payment.success', ...)`, `.on('payment.failure', ...)`, `.on('payment.bank_transfer_submitted', ...)`) are **not invoked** — handle the result server-side via the redirect query parameters instead. Non-terminal events (`checkout.closed`) still fire normally. Only `http:` and `https:` URLs are accepted.


## API Reference

### Methods

#### `open(): Promise<void>`

Opens the checkout in a modal overlay. Automatically detects the correct checkout flow (standard, LRS, or payment session) based on your configuration. A loading spinner is shown while initializing.

```javascript
await checkout.open();
```

#### `close(): void`

Closes the checkout and cleans up all resources. It does not emit `checkout.closed`: that event only fires when the user closes checkout, so handle anything you'd do on close in the code that calls `close()`.

```javascript
checkout.close();
```

#### `on(event, callback): () => void`

Registers an event listener. Returns an unsubscribe function to remove the listener.

```javascript
const unsubscribe = checkout.on('payment.success', (data) => {
  console.log('Payment succeeded:', data);
});

// Later: remove the listener
unsubscribe();
```

#### `setAppColorScheme(scheme): void`

Updates the colour scheme when your page switches between light and dark. Accepts `'light'`, `'dark'` or `'system'`; any other value is ignored with a console warning. An open checkout switches straight away.

```javascript
checkout.setAppColorScheme('dark');
```

### Events

The events table and the payload types below describe standard checkout (orders and subscriptions). LRS orders send a different shape: `payment.success` carries `status: "pending"`, and `payment.failure` carries flat `errorCode` and `errorMessage` fields plus an optional `signature`. For LRS, see [LRS client-side events](/payin/resident-india-remittance-under-lrs/client-side-events).

| Event | Description | Payload |
|  --- | --- | --- |
| `payment.success` | Payment completed successfully. **Not emitted when `callbackUrl` is set.** | `{ status, orderId, paymentId, signature }` |
| `payment.failure` | Payment failed. Carries no signature. **Not emitted when `callbackUrl` is set.** | `{ orderId, paymentId, status, error: { code, message } }` |
| `payment.bank_transfer_submitted` | User submitted bank transfer details. **Does NOT confirm payment** — await a webhook for confirmation. **Not emitted when `callbackUrl` is set.** | `{ status, orderId, senderAccountNumber, senderName, payerRelationship }` |
| `checkout.closed` | Checkout was closed by the user (not sent for `close()` calls) | `{}` |


### Using `callbackUrl` (Redirect Mode)

Instead of handling terminal payment events in JavaScript, you can pass a `callbackUrl` to redirect the user after payment completion. The SDK navigates the parent window to `callbackUrl` with the event payload fields appended as query string parameters. When `callbackUrl` is set:

- `.on()` listeners for `payment.success`, `payment.failure`, and `payment.bank_transfer_submitted` are **not invoked**. `callbackUrl` and JS callbacks are mutually exclusive for terminal payment events — this prevents merchants from double-handling the same event.
- Non-terminal events (`checkout.closed`) continue to fire `.on()` listeners normally.
- Only `http:` and `https:` callback URLs are accepted.


**Query parameters by event:**

| Event | Query parameters appended |
|  --- | --- |
| `payment.success` | `orderId`, `paymentId`, `status`, `signature` |
| `payment.failure` | `orderId`, `paymentId`, `status`, `errorCode`, `errorMessage` (no `signature`) |
| `payment.bank_transfer_submitted` | `orderId`, `status`, `senderAccountNumber`, `senderName`, `payerRelationship` |


`status` reflects the payment lifecycle and varies by payment method (e.g. `success`, `pending`, `failed`, `bank_transfer_submitted`). Treat it as an opaque string and rely on the event type for branching logic.

**Example redirect after successful payment:**

```
https://yourwebsite.com/payment-result?orderId=order_abc123&paymentId=payt_abc123&status=success&signature=abc123def456
```

> **Important:** Verify the `signature` query parameter on your server exactly as you would for the `.on()` payload. See [Signature Verification](#signature-verification).


> **Note:** This differs from the checkout-sdk, which emits `.on()` events **and** redirects when `callbackUrl` is set. The Unified SDK only redirects — registering `.on()` listeners for terminal events alongside `callbackUrl` has no effect.


### Payload Types

These types apply to standard checkout (orders and subscriptions). For LRS payloads, see [LRS client-side events](/payin/resident-india-remittance-under-lrs/client-side-events).

```typescript
// payment.success
interface PaymentSuccessPayload {
  status: string;      // "success"
  orderId: string;     // Order ID, or subscription ID for a subscription (e.g., "order_abc123")
  paymentId: string;   // Payment ID (e.g., "payt_abc123")
  signature: string;   // Signature for server-side verification
}

// payment.failure (not signed)
interface PaymentFailurePayload {
  orderId: string;
  paymentId: string;
  status: string;      // "failed"
  error: {
    code: string;      // Payment error code, or "Failed" when there is no specific reason
    message: string;   // Message shown to the customer, or a generic message when there is no specific reason
  };
}

// payment.bank_transfer_submitted (not signed)
interface BankTransferPayload {
  status: string;               // "bank_transfer_submitted"
  orderId: string;              // The order ID
  senderAccountNumber: string;  // Last 4 characters of the account the customer paid from
  senderName: string;           // Account holder name (the customer's name for their own account)
  payerRelationship: string;    // Payer's relationship to the customer; empty when not collected
}
```

## Dark mode

Pass `appColorScheme` so checkout matches your page:

```javascript
const checkout = new GlomoCheckoutApi({
  publicKey: 'live_xxx',
  orderId: 'order_xxx',
  appColorScheme: 'system', // 'light' | 'dark' | 'system'
});
```

- `'system'` follows the customer's device setting, including changes while checkout is open.
- `'light'` and `'dark'` keep checkout in that scheme regardless of the device setting.
- If you leave `appColorScheme` out, checkout stays light, as it always has.
- Call `setAppColorScheme()` when your page changes theme.


For how the same property behaves on the mobile SDKs, and what their automatic value resolves to, see [Dark mode across the SDKs](/platform/sdk/dark-mode). Note the mobile SDKs spell the automatic value `auto` rather than `'system'`, and it is their default.

Dark mode is enabled per account. Until it is enabled for yours, checkout stays light whatever you pass, so you can ship the option ahead of time. Contact Glomo support to turn it on. White-labelled checkouts always stay light.

The loading screen starts light and switches once checkout has confirmed dark mode is enabled for your account.

## Signature Verification

After receiving a `payment.success` event, verify the `signature` on your server. `payment.failure` is not signed, so confirm a failure on your server. Send the payload to your server and compute the signature using the order ID you created on your server, with each of your active secret keys. Accept the response if any one of them matches. Your active secret keys are every secret key on your account that has not expired. The signature may be made with any one of them, and it can differ between payments, so don't assume it is your newest key. When you revoke a key, or it expires, remove it from your list.

Trim each key and drop empty entries before you check, and stop with an error if no key is left. An empty key must never count as a match:

```
data = orderId + "|" + paymentId + "|" + status
active_secret_keys = [trim(k) for k in split(config.GLOMO_SECRET_KEYS, ",") if trim(k) != ""]
if active_secret_keys is empty:
    raise error("GLOMO_SECRET_KEYS is not set")
valid = false
for secret_key in active_secret_keys:
    expected = hex(HMAC-SHA256(data, secret_key))
    if constant_time_equals(expected, signature):
        valid = true
```

For detailed instructions and sample code in PHP, Ruby, Go, JavaScript, and Python, see [Verify checkout response and signature](/payin/checkout#steps-to-integrate-checkout).

## Troubleshooting

### Mock Mode Testing

Mock mode is automatically enabled based on your `publicKey`:

- If your `publicKey` starts with `test_`, mock mode is enabled and the checkout URL will include `mode=mock`
- Otherwise, live mode is used


**Example:**

```javascript
// Mock mode (publicKey starts with "test_")
const checkout = new GlomoCheckoutApi({
  publicKey: 'test_abc123',
  orderId: 'order_xyz789',
});

// Live mode
const checkout = new GlomoCheckoutApi({
  publicKey: 'live_abc123',
  orderId: 'order_xyz789',
});
```

### Checkout Popup Blocked

If the checkout fails to open in production, your website's `Cross-Origin-Opener-Policy` header may be blocking popups. See [Checkout Popup Blocked in Production Environment](/payin/checkout#checkout-popup-blocked-in-production-environment) for solutions.

### CORS Error on Checkout Load

If you see CORS errors in the browser console when the checkout loads, browser extensions may be interfering. See [CORS Error on Checkout Load](/payin/checkout#cors-error-on-checkout-load-preferences-api-failure) for troubleshooting steps.

### Content Blocked by Content Security Policy

If the checkout iframe or SDK scripts fail to load due to CSP headers, you need to allow the Glomo domains in your CSP configuration. See [Content Blocked by CSP](/payin/checkout#content-blocked-by-content-security-policy-csp) for setup instructions.

### Handling Abandoned Checkouts

To detect when a user closes the checkout without completing payment, listen for the `checkout.closed` event:

```javascript
let paymentCompleted = false;

checkout.on('payment.success', () => {
  paymentCompleted = true;
});

checkout.on('payment.failure', () => {
  paymentCompleted = true;
});

checkout.on('checkout.closed', () => {
  if (!paymentCompleted) {
    console.log('User closed checkout without completing payment');
    // Prompt the user to retry or log the abandonment
  }
});
```

> When using `callbackUrl`, the `checkout.closed` event still fires, so this pattern works regardless of whether you are using redirect mode or event-listener mode.


### Using the Web Checkout in a Native Mobile App

If you are embedding the checkout inside a native iOS/Android app via a WebView (rather than using this JavaScript SDK on a web page), see [Web Checkout in a Mobile App (WebView)](/platform/sdk/experimental). Note that the native [Flutter](/platform/sdk/flutter-sdk) and [React Native](/platform/sdk/react-native-sdk) SDKs are the recommended path.