## Account setup and configuration

Before you can integrate, you must set up your merchant environment. You’ll need the following:

- **Create a merchant account:** Sign up and gain access to your merchant dashboard

- **Create an SDK installation:** In your dashboard, under developer settings, create a new SDK installation for each website or mobile application you plan to deploy on. This process involves:
  - Choosing your platform (e.g., Web, iOS, Android).
  - Providing application details like your website domain or app bundle ID.
  - Selecting payment methods you want to offer on that platform (e.g., Card, PromptPay).
  - Configuring customer data to require (e.g., email address, mobile number).

- **Create a new API key:** In your dashboard, under developer settings, create a new API key.
  Keep your API key secret. Your API key is a secret credential. It must only be used on your backend server. Never expose this key in your frontend (client-side) JavaScript, mobile app code, or any public repository.

- **Connect a webhook endpoint (optional):** In your dashboard, under developer settings, create a new webhook endpoint and listen to the `payment_completed` event.

## Integration flow overview

A complete transaction involves both your frontend (handling user interaction) and your backend (handling secure API calls).

1. On the frontend (aka client-side): Your customer interacts with your UI. You use the Reservepay SDK to capture all payment details and create a `payment_session_id`.

2. On the backend (aka server-side): Your frontend sends the `payment_session_id` to your server. Your server then securely uses its secret API key to call the Reservepay Merchant API and initiate the actual payment flow.

3. Back on the frontend: Your frontend uses the SDK to poll for the next action (like redirecting the user to another page or displaying a QR code) and, ultimately, the final payment status

### About payment session IDs

The `payment_session_id` is a unique identifier that represents a single payment attempt from start to finish. It is the essential "link" between your customer's actions on the frontend (using the SDK) with the secure payment initiation request made by your backend (using your API key). You can think of it as a unique tracking number for the entire transaction lifecycle.

> **Testing card payments:** Use the [test card generator](/tools/test-card-generator) to create Luhn-compliant card numbers.

## Web integration

### Install the SDK

Include the SDK script tag in your HTML file. This loads the reservepay object into your application.

> To maintain strict PCI compliance and ensure the integrity of sensitive payment information, our library must be loaded directly from our secure servers via a script tag and must not be bundled or imported via a package manager.

```html
<script src="https://sdk.reservepay.com/js/v1/reservepay.js"></script>
```

```ts
// Initialize the SDK from window.Reservepay
const reservepay = window.Reservepay.initReservepay({
  merchantId: "123456789100",
  installationId: "ins_123",
})
```

### Collect and tokenize card details {#tokenize-card-details}

If the user selects "Card" as their payment method, you must first tokenize their card information. Use **custom card fields** to collect card data through Reservepay-hosted iframes that you mount into your own UI. The iframes keep raw card details out of your page, reducing your PCI scope, while you retain full control over layout and surrounding form elements.

Add container elements to your page for each field you want to mount:

```html
<div id="card-number"></div>
<div id="expiration-date"></div>
<div id="cvv"></div>
<input id="cardholder-name" type="text" />
```

Create a custom card fields instance, pointing each field at its container. You may pass a `styles` object to apply text-level CSS inside the iframes. Only the following properties are allowed — layout, background, border, padding, and `url()` values are rejected, since you control all visual chrome on the container:

| Property         | Example                                  |
| :--------------- | :--------------------------------------- |
| `color`          | `"#1a1a2e"`                              |
| `font-size`      | `"14px"`                                 |
| `font-family`    | `"system-ui, -apple-system, sans-serif"` |
| `font-weight`    | `"500"`                                  |
| `font-style`     | `"italic"`                               |
| `letter-spacing` | `"0.02em"`                               |
| `line-height`    | `"1.5"`                                  |
| `text-align`     | `"left"`                                 |
| `text-transform` | `"uppercase"`                            |
| `opacity`        | `"0.9"`                                  |
| `transition`     | `"color 0.2s ease"`                      |

Styles can be scoped under `input` (base), `:focus`, `.valid`, or `.invalid`:

```ts
const instance = await reservepay.customCardFields.create({
  fields: {
    cardNumber: {
      container: "#card-number",
      placeholder: "4242 4242 4242 4242",
    },
    expirationDate: { container: "#expiration-date", placeholder: "MM / YY" },
    cvv: { container: "#cvv", placeholder: "123" },
  },
  styles: {
    input: { "font-size": "14px", color: "#1a1a2e" },
    ".invalid": { color: "#dc2626" },
    ".valid": { color: "#16a34a" },
  },
})
```

#### Validate the fields

Subscribe to field events to drive validation UI in your own form:

```ts
instance.on("validityChange", (e) => {
  // e.field, e.isValid, e.isPotentiallyValid
})
instance.on("cardTypeChange", (e) => {
  // e.cardType — "visa", "mastercard", etc.
})
instance.on("focus", (e) => {
  /* e.field */
})
instance.on("blur", (e) => {
  /* e.field, e.isValid */
})
```

#### Tokenize the card

When the user submits, call `tokenize` with the cardholder name collected from your own input. This returns a single-use token to pass into the next step.

```ts
const { token } = await instance.tokenize({
  name: cardholderName,
  usage: "SINGLE",
})
```

Call `instance.destroy()` when your card form unmounts to remove the iframes and listeners.

### Create a payment session

Next, create a payment session. This tells our system you're about to start a payment. Call `sdk/select-payment-method` with the amount, currency, payment method, and any required customer info (or use the `selectPaymentMethod` helper provided by the SDK, see example below). If the `payment_method` is `CARD`, you must include the token from the previous step. This will return a `payment_session_id`. Send this ID to your backend.

#### PromptPay

```ts
const paymentSessionId = await reservepay.endpoints.sdk.selectPaymentMethod({
  amount: "100000",
  currency: "THB",
  payment_method: "PROMPTPAY",
})
```

#### Card

Pass the token returned by [`instance.tokenize`](#tokenize-card-details):

```ts
const paymentSessionId = await reservepay.endpoints.sdk.selectPaymentMethod({
  amount: "100000",
  currency: "THB",
  payment_method: "CARD",
  token,
})
```

### Handle next actions

After your backend has initiated the payment, your frontend must poll to discover what to do next. Call `sdk/discover-next-action` in a loop (or use the polling helper provided by the SDK, see example below), passing your `merchant_id`, `installation_id`, and `payment_session_id` and handle the action returned:

- `WAIT`: The payment is processing.

- `REDIRECT`: The user needs to be redirected (e.g., for 3D Secure or to a mobile banking application). Call `sdk/get-redirect-url` to get the destination URL and redirect the user to this URL. After the user returns to your site, resume polling on the frontend or fetch the payment details on your backend by using the `merchants/find-payment` endpoint.

- `DISPLAY_QR`: The user needs to be shown a QR code. Call `sdk/retrieve-qr-data` to get the base64-encoded QR data. Base64-decode the response and render it as a QR code in your UI. Continue polling `sdk/discover-next-action` while the QR code is displayed.

- `CHECK_STATUS`: The payment flow is complete. The loop can stop. Proceed to the next step.

#### PromptPay (QR)

Poll until `DISPLAY_QR`, then fetch and render the QR code:

```ts
const discoverNextActionPolling =
  reservepay.endpoints.sdk.createDiscoverNextActionPolling()

const action = await discoverNextActionPolling.execute({
  payment_session_id: paymentSessionId,
  pollingUntilAction: "DISPLAY_QR",
})

// Then retrieve the QR data
const qrData = await reservepay.endpoints.sdk.retrieveQrData({
  payment_session_id: paymentSessionId,
})

// Display QR code to user (qrData is base64 encoded)
const qrCodeString = atob(qrData)

// And finally render the QR code in your UI (sample code shown - any QR generation library will work)
renderQRCode("#qrCodeContainer", qrCodeString)
```

#### Card (3D Secure redirect)

Poll until `REDIRECT` (for 3D Secure), then fetch the URL. You can either redirect the user, or mount the challenge inline on your page.

```ts
const discoverNextActionPolling =
  reservepay.endpoints.sdk.createDiscoverNextActionPolling()

const action = await discoverNextActionPolling.execute({
  payment_session_id: paymentSessionId,
  pollingUntilAction: "REDIRECT",
})

const redirectUrl = await reservepay.endpoints.sdk.getRedirectUrl({
  payment_session_id: paymentSessionId,
})
```

Redirect the user to complete 3D Secure:

```ts
window.location.href = redirectUrl
```

Or mount the 3D Secure challenge inline using `openThreeDSecure`. The challenge renders inside a Reservepay-hosted iframe in your container, so the user stays on your page. Poll `sdk/check-status` in parallel to receive the final outcome.

```ts
const tds = reservepay.customCardFields.openThreeDSecure({
  url: redirectUrl,
  container: document.getElementById("3ds-mount"),
  onSuccess: () => {
    // 3D Secure challenge passed
  },
  onFailed: () => {
    // 3D Secure challenge failed
  },
  onError: (err) => {
    // Surface error
  },
})

// Call tds.destroy() to abort the challenge manually
```

### Check the status

Once `sdk/discover-next-action` returns `CHECK_STATUS`, make one final call to get the definitive outcome. Call `sdk/check-status` using the same `merchant_id`, `installation_id`, and `payment_session_id`. This will return a final status (e.g., `SUCCESSFUL`, `FAILED`). Update your UI accordingly

```ts
const checkStatusPolling = reservepay.endpoints.sdk.createCheckStatusPolling()

const status = await checkStatusPolling.execute({
  payment_session_id: paymentSessionId,
})

if (status === "SUCCESSFUL") {
  // Show success message with payment details
  console.log("Payment successful")
}

if (status === "FAILED") {
  // Handle payment failure
  console.log("Payment failed")
}
```

### Polling considerations

You must cancel polling to stop background requests when your UI closes. Without `abort()`, polling continues making API calls every few seconds for up to 10 minutes (hundreds of requests!)

You should call `abort()` when:

- Component unmounts (payment dialog closes)
- User clicks cancel or navigates away
- Payment completes (success or failure)

This prevents memory leaks, unnecessary server load, and resource exhaustion

```ts
discoverNextActionPolling.abort()
checkStatusPolling.abort()
```

### Accept payments with a saved card {#saved-card-own-ui}

Saved cards work in two steps: create a reusable token on the first payment, then use it to render the saved card number and a CVV field on later visits.

#### Saving a card

When the customer opts in, tokenize with `usage: "MULTIPLE"`:

```ts
const { token } = await instance.tokenize({
  name: cardholderName,
  usage: "MULTIPLE",
})
```

Send the token to your backend and associate the card with the customer — inline when you [initiate the payment flow](#save-card-backend), or via [`merchants/save-card`](/guides/customer-intelligence). After ReservePay saves the card, you can retrieve its opaque token instead of storing it separately.

#### Reusing a saved card

On return visits, [retrieve the customer's saved cards from your backend](#save-card-backend), let the customer select one, and pass its `token` as `savedCardToken` to `customCardFields.create`. The SDK renders the masked card number as a read-only field and mounts a CVV field for the customer to complete.

> Use `reservepay.customCardFields.create(...)` directly for saved-card flows. The `initReservepayCustomCardFields` convenience helper does not forward `savedCardToken`.

Mount containers for the read-only card number and CVV fields:

```html
<div id="card-number"></div>
<div id="cvv"></div>
```

Then create the saved-card instance:

```ts
const instance = await reservepay.customCardFields.create({
  savedCardToken: customerSavedCardToken, // token retrieved from your backend
  fields: {
    cardNumber: { container: "#card-number" },
    cvv: { container: "#cvv", placeholder: "123" },
  },
})
```

Tokenize the CVV — saved-card mode doesn't require `name` — and pass the token to `selectPaymentMethod`, then continue with [Handle next actions](#handle-next-actions) and [Check the status](#check-the-status):

```ts
const { token } = await instance.tokenize({
  usage: "MULTIPLE",
})

const paymentSessionId = await reservepay.endpoints.sdk.selectPaymentMethod({
  amount: "100000",
  currency: "THB",
  payment_method: "CARD",
  token,
})
```

Call `instance.destroy()` when your form unmounts to remove the CVV iframe and listeners.

#### Handle an invalid saved-card token

If `savedCardToken` cannot be decoded, `create` throws an error with the code `INVALID_SAVED_CARD_TOKEN`. Catch only this code and fall back to the full [new-card flow](#tokenize-card-details):

```ts
async function createCardFields() {
  try {
    return await reservepay.customCardFields.create({
      savedCardToken: customerSavedCardToken,
      fields: {
        cardNumber: { container: "#card-number" },
        cvv: { container: "#cvv", placeholder: "123" },
      },
    })
  } catch (error) {
    if (error && error.code === "INVALID_SAVED_CARD_TOKEN") {
      return startNewCardFlow()
    }

    throw error
  }
}

const instance = await createCardFields()
```

### Live card-fields demo

<div id="card-fields-demo" data-controller="card-fields-demo" class="rounded-lg border border-gray-200 bg-white p-4 my-4">
  <div class="grid gap-3 max-w-md">
    <div class="text-sm text-gray-700 space-y-1 block">
      <span class="block">Card number</span>
      <div data-card-fields-demo-target="cardNumber" class="h-10 rounded-md border border-gray-300 bg-white px-3 py-2"></div>
    </div>
    <div class="grid grid-cols-2 gap-3">
      <div class="text-sm text-gray-700 space-y-1 block">
        <span class="block">Expiration</span>
        <div data-card-fields-demo-target="expirationDate" class="h-10 rounded-md border border-gray-300 bg-white px-3 py-2"></div>
      </div>
      <div class="text-sm text-gray-700 space-y-1 block">
        <span class="block">CVV</span>
        <div data-card-fields-demo-target="cvv" class="h-10 rounded-md border border-gray-300 bg-white px-3 py-2"></div>
      </div>
    </div>
    <label class="text-sm text-gray-700 space-y-1 block">
      Cardholder name
      <input id="name" type="text" placeholder="John Doe" class="h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 box-border" />
    </label>
  </div>
  <pre data-card-fields-demo-target="output" class="m-0 rounded-md bg-slate-900 text-emerald-300 p-3 text-xs min-h-10 whitespace-pre-wrap break-all max-h-60 overflow-y-auto my-2"></pre>
</div>

## Backend integration {#backend-integration}

These steps are identical regardless of which platform initiated the payment session.

### Initiate the payment flow {#initiate-the-payment-flow}

After your frontend sends the `payment_session_id`, your backend must call the `merchants/initiate-payment-flow` endpoint which will return a `payment_id`. This call must be authenticated using your secret API key as a bearer token in the `Authorization` header. This "authorizes" the payment session and kicks off the payment flow.

#### Example

```ts
// IMPORTANT: This must be called from your server-side code with your API key
// Never expose your API key (reservepay_xxx) in client-side code
// Make a request from your backend to Reservepay API:
//
await fetch('https://api.reservepay.com/merchants/initiate-payment-flow', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY', // Use your API key here (server-side only!)
    'Api-Version': '2025-04-01',
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: "1000",
    currency: "THB",
    payment_session_id: paymentSessionId,
    capture: true,
    return_url: "https://your-site.com/return-url"
  })
})
```

### Saving and reusing cards {#save-card-backend}

If the customer consented to saving their card, you can persist it two ways. Inline with this payment: add `customer_id` and `save_card` to the `initiate-payment-flow` call you already make (below). On its own: use the standalone `merchants/save-card` endpoint when no payment is in flight — see [Saving cards for future use](/guides/customer-intelligence).

#### Save a card

Add `customer_id` and `save_card` to your `initiate-payment-flow` call. This only saves the card if the reusable token from the steps above is multi-use. Set `save_card` to `ADD` to attach the card to the customer, or `DEFAULT` to also make it their default card.

```ts
await fetch('https://api.reservepay.com/merchants/initiate-payment-flow', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Api-Version': '2025-04-01',
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: "1000",
    currency: "THB",
    payment_session_id: paymentSessionId,
    capture: true,
    return_url: "https://your-site.com/return-url",
    customer_id: customerId,
    save_card: "ADD"
  })
})
```

> If saving the card fails, the payment still goes through. The error is not surfaced in the response — check your saved cards afterward if you need to confirm.

#### Reuse a saved card

To reuse a saved card, you don't need to store the original token yourself. Look up the customer's cards with `merchants/retrieve-contact`. Each active card includes the merchant-scoped opaque `token` accepted by the browser SDK, while `card_id` identifies the card for backend operations.

```ts
const contact = await fetch('https://api.reservepay.com/merchants/retrieve-contact', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Api-Version': '2025-04-01',
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ contact_id: customerId })
}).then(res => res.json())

const savedCards = contact.default_card
  ? [
      contact.default_card,
      ...contact.cards.filter(
        card => card.card_id !== contact.default_card.card_id
      )
    ]
  : contact.cards

const savedCardTokens = savedCards.map(card => card.token)
```

Return `savedCardTokens` from your backend and pass it to the browser SDK for a customer-present saved-card checkout. The example puts the default card first because the SDK initially selects the first valid token.

For a merchant-initiated payment instead, pass a card's `card_id` to `merchants/request-payment` to charge it directly from your backend. This doesn't need a payment session, SDK involvement, or the customer present, and it skips 3D Secure. Omit `card_id` to charge the customer's default card instead.

```ts
await fetch('https://api.reservepay.com/merchants/request-payment', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Api-Version': '2025-04-01',
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: "1000",
    currency: "THB",
    installation_id: installationId,
    capture: true,
    customer_id: customerId,
    card_id: cardId
  })
})
```

> `merchants/request-payment` requires the card payment method's MIT (merchant-initiated transaction) flag. Contact support to enable it before using this endpoint.

To look up a single card by its ID or token instead of listing all of a customer's cards, use `merchants/find-card`.

### Confirming the payment status {#verify}

To confirm the payment status from your backend call `merchants/find-payment` using the `payment_id` returned by the `merchants/initiate-payment-flow` endpoint. This returns the full payment object, including the `status` field.

### Webhooks (optional) {#webhooks}

For a more robust integration, we highly recommend using webhooks. Instead of relying only on polling, your server can receive asynchronous notifications. To achieve the same result as the previous step, listen for the `payment_completed` event.

To verify the signature of incoming events our requests include an `Reservepay-Signature` header. You must verify this signature using your webhook endpoint verify key (available in your dashboard) and a libsodium compatible library. This verification is critical to make sure that the request genuinely came from Reservepay and was not forged. See [Real-time notifications](/guides/real-time-notifications) for setup and verification instructions.

