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_completeevent.
Integration flow overview
A complete transaction involves both your frontend (handling user interaction) and your backend (handling secure API calls).
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.On the backend (aka server-side): Your frontend sends the
payment_session_idto your server. Your server then securely uses its secret API key to call the Reservepay Merchant API and initiate the actual payment flow.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.
Integration steps
Install the SDK
Include the SDK script tag in your HTML file. This loads the reservepay object into your application.
Example
<script src="https://sdk.reservepay.com/js/v1/reservepay.js"></script>
// Initialize the sdk
//
// Note that the Reservepay sdk is available in window.Reservepay
const reservepay = window.Reservepay.initReservepay({
merchantId: "123456789100",
installationId: "ins_123",
})
Tokenize card details (card payments only)
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" },
},
})
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 */
})
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.
Demo
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. 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.
Example
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,
})
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
// 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: "100000",
currency: "THB",
payment_session_id: paymentSessionId,
capture: true,
return_url: "https://your-site.com/return-url",
}),
})
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-urlto 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-paymentendpoint.DISPLAY_QR: The user needs to be shown a QR code. Call/sdk/retrieve-qr-datato 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-actionwhile the QR code is displayed.CHECK_STATUS: The payment flow is complete. The loop can stop. Proceed to the next step.
Example
PromptPay — 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
Card — 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 /sdk/check-status in parallel to receive the final outcome.
const tds = reservepay.customCardFields.openThreeDSecure({
url: redirectUrl,
container: document.getElementById("3ds-mount"),
onSuccess: () => {
// 3DS challenge passed
},
onFailed: () => {
// 3DS 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
Example
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")
}
Confirming the payment status
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)
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_complete event.
To verify the signature of incoming events our requests include an X-Webhook-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.
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
Example
discoverNextActionPolling.abort()
checkStatusPolling.abort()
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.
