Integration guides
Accepting payments
Open in Markdown

Custom integration

Build your entire checkout experience from the ground up using our powerful APIs and SDK. Perfect for complete control over the whole user experience.

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

<script src="https://sdk.reservepay.com/js/v1/reservepay.js"></script>
// Initialize the SDK from window.Reservepay
const reservepay = window.Reservepay.initReservepay({
  merchantId: "123456789100",
  installationId: "ins_123",
})

Collect and 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:

<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:

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:

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.

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

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

Card

Pass the token returned by instance.tokenize:

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 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 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 find-payment endpoint.

  • DISPLAY_QR: The user needs to be shown a QR code. Call 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 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:

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.

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:

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 check-status in parallel to receive the final outcome.

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 discover-next-action returns CHECK_STATUS, make one final call to get the definitive outcome. Call 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

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

discoverNextActionPolling.abort()
checkStatusPolling.abort()

Accept payments with a saved card

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":

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, or via save-card. 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, 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:

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

Then create the saved-card instance:

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 and Check the status:

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:

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

Card number
Expiration
CVV

Backend integration

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

Initiate the payment flow

After your frontend sends the payment_session_id, your backend must call the 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

// 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

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 save-card endpoint when no payment is in flight — see Saving cards for future use.

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.

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

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

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
  })
})

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 find-card.

Confirming the payment status

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

Webhooks (optional)

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 for setup and verification instructions.

Accept payments in websites and apps

Choose the integration method that best fits your needs and start accepting payments seamlessly on the web and in mobile apps.

Hosted checkout

Redirect customers to our secure, optimized checkout page. Perfect for quick integration with minimal development effort.

Read the guide
Pre-built forms

Embed our secure, pre-built payment forms directly into your existing checkout page while we handle PCI compliance.

Read the guide
Custom UI

Build your entire checkout experience from the ground up using our powerful APIs and SDK for complete control.

Read the guide

No account yet?

Start integrating all these amazing features into your app or website by creating your own merchant account today. It's free to sign up and only takes a few minutes to get started.