- WooCommerce
- Magento
- Shopify Soon
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_completedevent.
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.
Testing: On your test account, use the test card generator to create a card for the outcome you want to test: approval, decline, or 3D Secure challenge. Use any future expiry date, such as
12/29, and any 3-digit CVV (4 digits for AMEX).
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. The following text properties are supported. Apply layout and background styles to the field 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" |
-webkit-text-fill-color |
"#1a1a2e" |
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" },
},
})
Style the field container
Apply background, border, padding, and focus styles to your container. The field iframe is transparent; styles controls the text inside it. Properties outside the allowlist are ignored.
<div id="card-number" class="rp-field"></div>
.rp-field {
background-color: #14141f;
border: 1px solid #2c2c3d;
border-radius: 8px;
padding: 10px 12px;
}
.rp-field--focused {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgb(99 102 241 / 0.25);
}
.rp-field--error {
border-color: #dc2626;
}
In the create call above, set styles.input.color to "#f4f4f5" to give the text contrast against this dark background. Use padding to size the container around the iframe. Setting a fixed container height does not stretch or center the input inside it.
Styling limitations
- CSS variables: Resolve variables on your page before passing their values in
styles. The sanitizer rejectsvar(...)values. - Placeholders: The style API has no
::placeholderscope. Keep visible field labels instead of relying on placeholders alone.
Validate the fields
The fields emit events you can use to drive validation in your own form. Every event includes field (cardNumber, expirationDate, or cvv), isValid, and isPotentiallyValid:
focus: The customer entered a field.blur: The customer left a field.validityChange: The field's value became valid or invalid.cardTypeChange: The detected card brand changed. Also includescardType, such as"visa"or"mastercard".
This example highlights the card number container on focus, marks it as invalid when the customer leaves it incomplete, and clears the error once the number becomes valid:
const field = document.querySelector("#card-number")
instance.on("focus", (event) => {
if (event.field !== "cardNumber") return
field?.classList.add("rp-field--focused")
})
instance.on("blur", (event) => {
if (event.field !== "cardNumber") return
field?.classList.remove("rp-field--focused")
field?.classList.toggle("rp-field--error", !event.isValid)
})
instance.on("validityChange", (event) => {
if (event.field !== "cardNumber") return
if (event.isValid) field?.classList.remove("rp-field--error")
})
Repeat the pattern for each field, show an error message next to the highlighted border, and check every field again when the customer submits.
The .valid and .invalid scopes in styles only change the text inside the iframe. .invalid applies once the value can no longer become valid. Use the events above for everything outside the iframe.
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). Callget-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 thefind-paymentendpoint.DISPLAY_QR: The user needs to be shown a QR code. Callretrieve-qr-datato get the base64-encoded QR data. Base64-decode the response and render it as a QR code in your UI. Continue pollingdiscover-next-actionwhile 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)
Keep polling while the QR code is on screen. If the customer doesn't pay before the payment's expires_at, the payment moves to EXPIRED and Reservepay sends a payment_expired webhook. To let the customer try again, create a new payment session, initiate it with a new external_id, and show the new QR code.
Card (3D Secure redirect)
When the next action is REDIRECT, fetch the URL. You can redirect the customer or mount the challenge inline.
Not every card payment gets a challenge. 3D Secure is enabled per merchant account and can have a minimum amount, so a payment below that amount (or on an account without 3D Secure) goes straight to CHECK_STATUS. Handle both outcomes:
const discoverNextActionPolling =
reservepay.endpoints.sdk.createDiscoverNextActionPolling()
const action = await discoverNextActionPolling.execute({
payment_session_id: paymentSessionId,
pollingUntilAction: "REDIRECT",
})
const redirectUrl = action === "REDIRECT"
? await reservepay.endpoints.sdk.getRedirectUrl({
payment_session_id: paymentSessionId,
})
: null
If redirectUrl is null, continue with Check the status. Otherwise, redirect the customer to complete 3D Secure:
if (redirectUrl) 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 container = document.getElementById("3ds-mount")
if (redirectUrl && container) {
const tds = reservepay.customCardFields.openThreeDSecure({
url: redirectUrl,
container,
onSuccess: () => {
// Confirm the payment status on your backend.
},
onFailed: () => {
// Show the failed challenge and check the payment status.
},
onError: (err) => {
// Show an error message to the customer.
},
})
// Call tds.destroy() when the challenge UI closes or unmounts.
}
Once the payment reaches FAILED, look up its code in Payment failure codes to tell a declined card apart from a failed 3D Secure step.
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 stages: save the card on the first payment, then reuse it on later visits, where the customer enters only the CVV.
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 save the card to the customer, either with the payment or on its own. See Saving the card to a contact. After Reservepay saves the card, you can retrieve its opaque token instead of storing it separately.
Required: Reservepay doesn't save the CVV with the card. When the customer comes back to pay with a saved card, collect the CVV again as shown in the next section.
Otherwise, the payment fails. See The CVV on saved cards.
Reusing a saved card
On return visits, retrieve the customer's saved cards from your backend and let the customer select one. Then charge it in three steps:
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, // required for saved cards: token retrieved from your backend
fields: {
cardNumber: { container: "#card-number" },
cvv: { container: "#cvv", placeholder: "123" },
},
})
When the customer submits, call tokenize(). In saved-card mode, it doesn't create a new token: it sends the CVV to rehydrate-card and returns the saved card's token unchanged, so it doesn't need name or usage. Then pass the token to selectPaymentMethod right away and continue with Handle next actions and Check the status:
const { token } = await instance.tokenize()
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
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",
external_id: "order_12345"
})
})
Pass an external_id based on your order or invoice reference. If a request times out and you retry it with the same external_id, the original payment is returned instead of a duplicate, and you can look the payment up later with find-payment. Each payment attempt needs its own reference: if a payment fails or expires and the customer tries again, use a new one, such as order_12345_2. See Calling the API for the full rules.
Saving and reusing cards
To save the customer's card with this payment, add customer_id and save_card to your initiate-payment-flow call. See Saving cards for future use for saving, retrieving, and charging saved cards.
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.
Incoming events are signed. Verify the signature against the raw JSON body, before you parse or re-encode it, using the verification key for that endpoint. The header name and algorithm depend on the signature type the endpoint was configured with. See Real-time notifications for the signature types, header names, and verification steps.
Choose the integration method that best fits your needs and start accepting payments seamlessly on the web and in mobile apps.
Redirect customers to our secure, optimized checkout page. Perfect for quick integration with minimal development effort.
Read the guideEmbed our secure, pre-built payment forms directly into your existing checkout page while we handle PCI compliance.
Read the guideBuild your entire checkout experience from the ground up using our powerful APIs and SDK for complete control.
Read the guideNo 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.
