## 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 Reservepay pre-built form which captures all necessary payment details and creates a `payment_session_id` via the `onPaymentSessionReady` callback.

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: The Reservepay pre-built form automatically displays the payment status and hands back control to you via the callbacks.

> **Testing:** On your test account, use the [test card generator](/tools/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).

<!-- screenshot: prebuilt-method-picker - pre-built form payment method picker; strip merchant/installation IDs; replace with ![Payment method picker](/guides/prebuilt-method-picker.webp) -->

<!-- screenshot: prebuilt-card-form - pre-built card entry form; strip IDs; replace with ![Card form](/guides/prebuilt-card-form.webp) -->

<!-- screenshot: prebuilt-success - pre-built success state; strip IDs; replace with ![Payment success](/guides/prebuilt-success.webp) -->

## Web integration

### Install the SDK {#web-install-web-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>
```

### Attach the SDK

Once the SDK script is loaded, you can initialize the payment form using the `window.Reservepay.initReservepayFormUI` method. This function takes a configuration object.


The required top-level parameters are:

* `formType`: Defines the UI mode. Use `"popup"` or `"embedded"`.  
* `merchantId`: Your unique merchant identifier.  
* `installationId`: The specific installation ID for this integration.  
* `formOptions`: An object containing configuration specific to the form, such as `amount` and the necessary event callbacks.

#### Popup form

The popup opens when the customer selects the trigger element.

```html
<button id="reservepay-popup-trigger">Pay Now</button>
```

```ts
window.Reservepay.initReservepayFormUI({
  formType: "popup",
  merchantId: "123456789100",
  installationId: "ins_123",
  formOptions: {
    popupTriggerSelector: "#reservepay-popup-trigger",
    amount: "100000",
    async onPaymentSessionReady(sessionId) {
      await sendPaymentSessionToBackend(sessionId)
    },
    onPaymentSuccess(sessionId) {
      console.log("Payment successful", sessionId)
    },
    onPaymentFailed(sessionId) {
      console.log("Payment failed", sessionId)
    },
  },
})
```

#### Embedded form

The embedded form renders inside a container in your checkout.

```html
<div id="reservepay-mount"></div>
```

```ts
window.Reservepay.initReservepayFormUI({
  formType: "embedded",
  merchantId: "123456789100",
  installationId: "ins_123",
  formOptions: {
    containerSelector: "#reservepay-mount",
    amount: "100000",
    async onPaymentSessionReady(sessionId) {
      await sendPaymentSessionToBackend(sessionId)
    },
    onPaymentSuccess(sessionId) {
      console.log("Payment successful", sessionId)
    },
    onPaymentFailed(sessionId) {
      console.log("Payment failed", sessionId)
    },
  },
})
```

`sendPaymentSessionToBackend` is a placeholder for your frontend-to-backend request. Implement it to send the `sessionId` to your server, then follow [Initiate the payment flow](#initiate-the-payment-flow).

#### Handle callbacks

| Callback | When it runs | What to do |
| --- | --- | --- |
| `onPaymentSessionReady(sessionId)` | The SDK creates the payment session, before the customer submits payment details. | Send the `sessionId` to your backend and associate it with the order or cart. |
| `onPaymentSuccess(sessionId)` | The customer completes the payment flow. | Show a success state or redirect to a confirmation page. |
| `onPaymentFailed(sessionId)` | The payment fails, is declined, or is canceled. | Show the failure and let the customer retry when appropriate. |

> Use frontend callbacks to update the UI. Fulfill the order only after a [webhooks](#webhooks) or backend status check confirms the payment.

### Accept payments with a saved card {#saved-card-pre-built}

Saved cards let returning customers check out with a single CVV entry: collect a reusable token on the first payment, then pass it back to the form on later visits.

#### Saving a card

Set `enableSaveCard: true` in `formOptions`. The SDK shows a **Save this card** checkbox; when the customer opts in, `onPaymentSessionReady` receives a second argument: `{ saveCard: true, savedCardToken }`. Send both 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](/guides/customer-intelligence#saving-the-card-to-a-contact). After Reservepay saves the card, you can retrieve its opaque token instead of storing it separately.

> The SDK acknowledges the session even if `onPaymentSessionReady` throws, so the payment continues. Handle token-storage failures on your backend.

```ts
window.Reservepay.initReservepayFormUI({
  formType: "embedded",
  merchantId: "123456789100",
  installationId: "ins_123",
  formOptions: {
    containerSelector: "#reservepay-mount",
    amount: "100000",
    enableSaveCard: true,
    async onPaymentSessionReady(sessionId, details) {
      await sendPaymentSessionToBackend(sessionId)
      if (details.saveCard) {
        await saveCardForCustomer(details.savedCardToken) // your backend call
      }
    },
    onPaymentSuccess(sessionId) {
      console.log("Payment successful", sessionId)
    },
    onPaymentFailed(sessionId) {
      console.log("Payment failed", sessionId)
    },
  },
})
```

#### Reusing a saved card

On later checkouts, [retrieve the customer's saved-card tokens from your backend](/guides/customer-intelligence#retrieving-saved-cards), then pass them through `formOptions.savedCardTokens` (string or array). The form displays the masked card and asks only for the CVV - with multiple tokens, it shows a card picker and a **Use a different card** option first. The form sends the CVV to `sdk/rehydrate-card` before it creates the payment session, so you don't call it yourself. See [The CVV on saved cards](/guides/customer-intelligence#saved-card-cvv).

> If a token in `savedCardTokens` cannot be decoded - for example, if it is malformed or stale - it's dropped silently. If none decode, the SDK falls back to the standard new-card form automatically.

```ts
window.Reservepay.initReservepayFormUI({
  formType: "embedded",
  merchantId: "123456789100",
  installationId: "ins_123",
  formOptions: {
    containerSelector: "#reservepay-mount",
    amount: "100000",
    savedCardTokens: customerSavedCardTokens, // string or string[] from your backend
    async onPaymentSessionReady(sessionId) {
      await sendPaymentSessionToBackend(sessionId)
    },
    onPaymentSuccess(sessionId) {
      console.log("Payment successful", sessionId)
    },
    onPaymentFailed(sessionId) {
      console.log("Payment failed", sessionId)
    },
  },
})
```

## Android integration

### Install the SDK {#android-install-the-sdk}

Add the SDK to your app's `build.gradle`:

```groovy
repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.reservepay:reservepay-android:1.0.0'
}
```

Declare `ReservepayActivity` in `AndroidManifest.xml`:

```xml
<activity
    android:name="com.reservepay.ReservepayActivity"
    android:exported="false" />
```

### Start a payment {#android-start-a-payment}

Configure the SDK once, typically in `Application.onCreate()`, then call `startPayment` for each transaction.

```kotlin
import com.reservepay.ReservepayCallbacks
import com.reservepay.ReservepaySDK
import com.reservepay.configure
import com.reservepay.startPayment

// Create callback
private val reservepayCallbacks = object : ReservepayCallbacks {
    override fun onPaymentSessionReady(sessionId: String, savedCardToken: String?) {
        // Send sessionId to your backend. Store savedCardToken when present.
    }
    override fun onPaymentStatus(status: String) {
    }
    override fun onPaymentCancelled() {
    }
    override fun onPaymentError(error: String) {
    }
}

// Configure the SDK once, before starting any payment
ReservepaySDK.getInstance().configure(
    context = context,
    isDebug = true,
    merchantId = "123456789100",
    installationId = "ins_123",
)

// Start a payment
ReservepaySDK.getInstance().startPayment(
    context,
    100000L, // THB 1,000.00 in satang
    "THB", // Currency
    tokens = customerSavedCardTokens, // Saved card tokens to offer for reuse, or null for none
    callback = reservepayCallbacks
)
```

Mobile amounts are integers in the currency's smallest unit. Pass saved-card tokens through `tokens`, or `null` when the customer has none. See [Mobile callback behavior](#mobile-callbacks) before continuing to [Backend integration](#backend-integration).

## iOS integration

### Install the SDK {#ios-install-the-sdk}

In Xcode, choose **File > Add Package Dependencies**. Enter the Reservepay package URL supplied with your SDK release, select that release's supported version, and add `ReservepaySDK` to your target.

### Start a payment {#ios-start-a-payment}

Configure the SDK once at app launch, then call `startPayment` for each transaction.

```swift
import ReservepaySDK

// Create callback
let callback = ReservepayCallback(
    onPaymentSessionReady: { sessionId, savedCardToken in
        // Send sessionId to your backend. Store savedCardToken when present.
    },
    onPaymentCancelled: {
        print("Payment cancelled")
        // Dismiss the view controller
        viewController.dismiss(animated: true)
    },
    onPaymentStatus: { status in
        print("Payment status: \(status)")
    },
    onPaymentError: { error in
        print("Payment error: \(error)")
    }
)

// Configure the SDK once, before starting any payment
ReservepaySDK.companion.getInstance().configure(
    isDebug: true,
    merchantId: "123456789100",
    installationId: "ins_123"
)

// Start a payment
ReservepaySDK.companion.getInstance().startPayment(
    viewController: viewController,
    amount: 100000, // THB 1,000.00 in satang
    currency: "THB",
    tokens: customerSavedCardTokens, // Saved card tokens to offer for reuse, or nil for none
    callback: callback
)
```

Mobile amounts are integers in the currency's smallest unit. Pass saved-card tokens through `tokens`, or `nil` when the customer has none.

## Mobile callback behavior {#mobile-callbacks}

Android and iOS use the same callback roles:

| Callback | What to do |
| --- | --- |
| `onPaymentSessionReady` | Send the session ID to your backend. If the saved-card token is present, store it against the customer. |
| `onPaymentStatus` | Update the UI from the reported status. |
| `onPaymentError` | Show or log the error. |
| `onPaymentCancelled` | Dismiss the payment flow or return to checkout. |

Web forms report completion through `onPaymentSuccess` and `onPaymentFailed`; mobile uses the status, error, and cancellation callbacks above. Continue with the shared backend steps after `onPaymentSessionReady`.

## 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",
    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 `merchants/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](/guides/calling-api-endpoints#idempotency-and-timeouts) for the full rules.

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

To save the customer's card with this payment, add `customer_id` and `save_card` to your `merchants/initiate-payment-flow` call. See [Saving cards for future use](/guides/customer-intelligence#saving-cards-for-future-use) for saving, retrieving, and charging saved cards.

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

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](/guides/real-time-notifications) for the signature types, header names, and verification steps.

