# Accept payments via Click to Pay - Web (Beta)

:::warning [Beta]
The Click to Pay SDK is currently in Beta. To enable it for your merchant account, contact your Revolut account executive.
:::

Revolut's Click to Pay widget allows you to offer your customers a faster, more secure checkout experience backed by Visa and Mastercard. 

Enrolled customers can complete payments in just a few clicks - the card networks handle authentication and payment credential delivery behind the scenes. Customers can enter their card details during checkout through Click to Pay and opt in to save them to a new or existing Click to Pay profile for future purchases.

In this tutorial, we'll guide you through integrating Click to Pay, powered by the [Revolut Checkout Widget](/docs/sdks/merchant-web-sdk/introduction). Customers can authenticate with their card network and complete the payment within a pop-up on your checkout page, reducing friction and improving conversion rates.

![Click to Pay on Web](/img/accept-payments/payment-methods/click-to-pay/click-to-pay.png 'Click to Pay on Web')

:::note [Limitations]

- **One-time payments only:** Click to Pay supports one-time card-not-present payments. Merchant-initiated transactions (MIT) and recurring payments are not available. For subscriptions and recurring billing, consider [SEPA Direct Debit](/docs/guides/merchant/accept-payments/online-payments/sepa-direct-debit/introduction) or [Revolut Pay](/docs/guides/merchant/accept-payments/online-payments/revolut-pay/introduction).
- **Country availability:** Click to Pay is available in 63 countries. See the [full country list](/docs/guides/merchant/accept-payments/online-payments/click-to-pay/introduction#available-countries) for details.
- **No sandbox:** Click to Pay is not available in the sandbox environment. Testing requires a production merchant account.
:::

## What is Click to Pay

[Click to Pay](https://www.emvco.com/emv-technologies/src/) is an EMVCo Secure Remote Commerce (SRC) standard backed by Visa and Mastercard. It enables customers to pay online with a streamlined, card-network-backed checkout experience. For enrolled customers, the card networks securely store payment credentials and handle authentication - no manual card entry required. Customers can enter their card details during checkout through Click to Pay and opt in to save them to a new or existing Click to Pay profile for future use.

Click to Pay supports three consumer journeys:

- **Recognised consumer:** the customer's device is already recognised by the card network, and their Click to Pay profile is loaded automatically - no OTP or manual entry required.
- **Returning but unrecognised consumer:** the customer's device is not recognised, the customer enters their email, verifies their identity with an OTP sent by the card network, and selects from their saved cards.
- **New consumer:** the customer chooses Click to Pay, enters their card details and profile information, and can opt in to save their card to a new Click to Pay profile for future use.

In all cases, 3DS authentication is handled by the card network within the Click to Pay flow. Revolut receives the authenticated payment credentials and performs authorisation, just like a standard card payment.

## How it works

Implementing the Click to Pay widget involves these core components:

1. **Server-side:** expose an endpoint on your backend that creates an order using the [Merchant API: Create an order](/docs/api/merchant#create-order) endpoint.
1. **Client-side:** the Click to Pay widget opens a pop-up where the customer authenticates with their card network and completes the payment, using your endpoint to create the order in the background.
1. **Webhook endpoint:** set up webhooks to monitor payment lifecycle events. Click to Pay uses the same standard card payment webhook events as regular card payments.

The order and payment flow is as follows:

1. The customer goes to the checkout page, chooses to pay via Click to Pay and clicks the payment button.
1. The widget opens the Click to Pay pop-up and calls your `createOrder` function to [create the order](/docs/api/merchant#create-order) via your backend in the background.
1. The customer authenticates with their card network (Visa or Mastercard) within the pop-up.
1. The card network performs 3DS authentication and securely delivers the payment credentials to Revolut.
1. Revolut authorises the payment, just like a standard card payment.
1. The `onSuccess` callback fires - the payment has been authorised.
1. Your server receives the `ORDER_COMPLETED` webhook event (or `ORDER_PAYMENT_DECLINED` / `ORDER_PAYMENT_FAILED` / `ORDER_CANCELLED` if unsuccessful).

:::info
For more information about the order and payment lifecycle, see: [Order and payment lifecycle](/docs/guides/merchant/reference/order-lifecycle).
:::

### Implementation overview

Here is an overview of the key integration steps:

1. [Set up an endpoint for creating orders](#1-set-up-an-endpoint-for-creating-orders)
1. [Install the SDK](#2-install-the-sdk)
1. [Initialise the SDK](#3-initialise-the-sdk)
1. [Configure and mount the widget](#4-configure-and-mount-the-widget)
1. [Handle payment results](#5-handle-payment-results)

### Before you begin

Before you start, make sure you have the following:

- [ ] An active **Merchant account** with Revolut Business. If you don't have one yet, [apply for a Merchant account](/docs/guides/merchant/get-started).
- [ ] Your **API keys** - both your public key (for initialising the SDK) and secret key (for creating orders on your backend). See [Generate the API keys](/docs/guides/merchant/get-started#generate-api-keys).
- [ ] A **backend server** to securely create orders using the Merchant API. Your secret API key must never be exposed on the client side.
- [ ] A **website checkout page** where you'll mount the Click to Pay widget.

<!-- TODO: Confirm with PO whether Click to Pay requires any merchant-specific prerequisites. SDR (Confluence: SDR Click to Pay, page 5694324737) says merchant onboarding to Visa and Mastercard schemes is automatic by Revolut via Netcetera - no action needed from the merchant. SDR also mentions a feature flag ENABLE_CLICK_TO_PAY - verify whether merchants need to request enablement. -->

## Implement Click to Pay

The following steps walk you through the full integration: setting up your backend to create orders, installing and initialising the SDK, configuring and mounting the Click to Pay widget, and handling payment results.

### 1. Set up an endpoint for creating orders

Before implementing the client-side widget, you must first create a dedicated endpoint on your server. This is a critical security step, as your secret API key must never be exposed on the client side.

The role of this server-side endpoint is to act as a secure bridge between your frontend and the Merchant API. When a customer initiates a payment on your website, your frontend will call this endpoint. Your endpoint is then responsible for:

1. Receiving the checkout details (e.g., `amount`, `currency`) from the frontend request.
1. Securely calling the [Merchant API: Create an order](/docs/api/merchant#create-order) endpoint with the received details.
1. Receiving the order details from the Merchant API, including the public `token`.
1. Passing this `token` back to your frontend in the response.

Later, in the client-side SDK configuration, the `createOrder` callback function will call this endpoint to fetch the `token`, which is required to initialise the checkout widget.

- ![Request]

  Send a `POST` request to the [Create an order](/docs/api/merchant#create-order) endpoint with the order details, including your **Secret API key** in the authorisation header:

  ```http [Request example]
  POST /api/orders HTTP/1.1
  Host: merchant.revolut.com
  Authorization: Bearer sk_abcdef12347890_...
  Content-Type: application/json
  Revolut-Api-Version: 2026-04-20

  {
    "amount": 1000,
    "currency": "GBP"
  }
  ```

  | Parameter | Description | Required |
  |-----------|-------------|----------|
  | `amount` | The order amount in minor currency unit (e.g., cents). For £10.00, use `1000`. | Yes |
  | `currency` | 3-letter [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code for the payment. See [supported currencies](https://help.revolut.com/business/help/merchant-accounts/payments/in-which-currencies-can-i-accept-payments/). | Yes |

  :::tip
  Replace `Revolut-Api-Version` with your target version. We highly recommend using the latest API version to access the latest features.
  :::

- ![Response]

  Below is an example of the JSON response your endpoint will receive from the Merchant API after successfully creating an order. The crucial field to extract and return to your frontend is the `token`:

  ```json [Response example] {3}
  {
    "id": "6516e61c-d279-a454-a837-bc52ce55ed49",
    "token": "0adc0e3c-ab44-4f33-bcc0-534ded7354ce",
    "type": "payment",
    "state": "pending",
    "created_at": "2023-09-29T14:58:36.079398Z",
    "updated_at": "2023-09-29T14:58:36.079398Z",
    "amount": 1000,
    "currency": "GBP",
    "outstanding_amount": 1000,
    "capture_mode": "automatic",
    "checkout_url": "https://checkout.revolut.com/payment-link/0adc0e3c-ab44-4f33-bcc0-534ded7354ce",
    "enforce_challenge": "automatic"
  }
  ```

:::info
For complete details on all available parameters, see: [Merchant API: Create an order](/docs/api/merchant#create-order).
:::

### 2. Install the SDK

Before you begin working with the SDK, ensure the Revolut Checkout Widget is installed in your project. This widget is a necessary component to create and configure the Click to Pay widget. You can install the widget via your project's package manager.

```install
npm install @revolut/checkout
```

:::tip
Alternatively, you can add the widget to your code base by adding the embed script to your page directly. To learn more, see: [Installation](/docs/sdks/merchant-web-sdk/get-started#installation).
:::

### 3. Initialise the SDK

Import `RevolutCheckout` from `@revolut/checkout` in your frontend code, then use the `RevolutCheckout.payments()` module's `clickToPay` instance with your [Merchant API Public key](https://business.revolut.com/settings/apis?tab=merchant-api) to initialise the widget.

- ![$With async await]

  ```js [my-app.js] {1,3-6}
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  // Configuration code will go here
  ```

- ![$Without async await]

  ```js [my-app.js] {1,3-6}
  import RevolutCheckout from '@revolut/checkout'

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    // Configuration code will go here
  })
  ```

| Parameter     | Description                        | Type                  | Required |
| ------------- | ---------------------------------- | --------------------- | -------- |
| `publicToken` | Your Merchant API public key       | `string`              | Yes      |
| `mode`        | API environment. Default: `'prod'` | `'prod' \| 'sandbox'` | No       |
| `locale`      | Widget language (defaults to `'auto'` for automatic detection) | [`Locale`](/docs/sdks/merchant-web-sdk/types/locale) | No |

:::info
For more information about the `RevolutCheckout.payments()` module, see: [Payments module](/docs/sdks/merchant-web-sdk/initialisation/payments-module)
:::

### 4. Configure and mount the widget

Prepare your checkout page by adding a button that users click to initiate the payment, then invoke the `clickToPay({ ... })` instance with your configuration parameters and call the `.show()` method to open the Click to Pay pop-up.

#### 4.1 Add a DOM element

First, add a button to your checkout page where you want the widget to appear. This can be a standalone button, or it can be triggered from a payment method selector - for example, the customer selects Click to Pay from a list of payment options, then clicks a pay button that triggers this button's click handler.

```html [checkout.html]
<!-- ... -->

<button id="click-to-pay-button">Pay with Click to Pay</button>

<!-- ... -->
```

#### 4.2 Set up the click handler

Attach a click handler to your button. For now, this is just a placeholder - you'll add the widget configuration in the [next step](#43-configure-the-widget).

- ![$With async await]

  ```js [my-app.js] {8,10-12}
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  const clickToPayButton = document.getElementById('click-to-pay-button')

  clickToPayButton.addEventListener('click', async () => {
    // Widget configuration will go here
  })
  ```

- ![$Without async await]

  ```js [my-app.js] {3,9-11}
  import RevolutCheckout from '@revolut/checkout'

  const clickToPayButton = document.getElementById('click-to-pay-button')

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    clickToPayButton.addEventListener('click', () => {
      // Widget configuration will go here
    })
  })
  ```

#### 4.3 Configure the widget

Invoke the `clickToPay({ ... })` instance inside the click handler with your configuration parameters, and call `.show()` to open the Click to Pay pop-up.

- ![$With async await]

  ```js [my-app.js] {10-17,19}
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  const clickToPayButton = document.getElementById('click-to-pay-button')

  clickToPayButton.addEventListener('click', async () => {
    const instance = clickToPay({
      createOrder: async () => {
        // Call your backend here to create an order and return order.token
        const order = await yourServerSideCall()
        return { publicId: order.token }
      },
    })

    instance.show()
  })
  ```

- ![$Without async await]

  ```js [my-app.js] {9-16,18}
  import RevolutCheckout from '@revolut/checkout'

  const clickToPayButton = document.getElementById('click-to-pay-button')

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    clickToPayButton.addEventListener('click', () => {
      const instance = clickToPay({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ publicId: order.token }))
        },
      })

      instance.show()
    })
  })
  ```

| Code snippet | Description |
| ------------ | ----------- |
| `RevolutCheckout.payments(...)` | Initialises the Revolut Checkout payments module and returns an object containing the `clickToPay` factory. Pass your `publicToken` and `mode` here. |
| `clickToPay` | Creates a Click to Pay instance when called with your options. It returns a `ClickToPayInstance`. |
| `ClickToPayInstance` | The instance returned by `clickToPay`. It exposes `.show()` to open the Click to Pay pop-up and `.destroy()` to close the widget and clean up resources. |
| `createOrder` | A function you define to send order details like `amount`, `currency` from your frontend to your backend. Your backend then calls [Merchant API: Create an order](/docs/api/merchant#create-order), receives the `token`, and returns `{ publicId: order.token }` to the widget, so it can start the checkout session. |
| Instance methods | Call `.show()` to open the Click to Pay pop-up. Call `.destroy()` to close the widget and clean up resources. |

:::info
For more details about the available parameters, see: [Merchant Web SDK: Click to Pay](/docs/sdks/merchant-web-sdk/payment-methods/click-to-pay).
:::

##### Additional settings

The `clickToPay` module of the Revolut Checkout Widget offers an additional setting you can leverage to enhance user experience.

###### Provide billing address [!details]

Pass the `billingAddress` parameter to provide the customer's billing address for payment processing. The SDK accepts an [`Address`](/docs/sdks/merchant-web-sdk/types/address) object.

:::note
The SDK does not collect billing address from the customer. You must collect the billing address on your frontend and pass it to the SDK via the `billingAddress` option. 

The SDK uses this data for payment processing and regulatory requirements.
:::

```js [my-app.js] {7-15}
const instance = clickToPay({
  createOrder: async () => {
    const order = await yourServerSideCall()
    return { publicId: order.token }
  },

  // Collect billing address on your frontend and pass it here
  billingAddress: {
    countryCode: 'GB',
    region: 'London',
    city: 'London',
    streetLine1: '1 Revolut Street',
    streetLine2: '',
    postcode: 'EC2V 7HN',
  },
})

instance.show()
```

[//]: # 'break'

###### Instance lifecycle management [!details]

The `clickToPay()` method returns a `ClickToPayInstance` with `.show()` and `.destroy()` methods. You control when to open and close the widget.

**Best practices:**

- Create the instance once when you need it and reuse it across multiple clicks - avoid creating a new instance on every click unless you're destroying the previous one.
- Call `.destroy()` when the widget is no longer needed (e.g., page navigation, component unmount) to clean up event listeners and resources.
- If you create a new instance per click, destroy the previous one first to prevent multiple pop-ups from stacking.

```js [my-app.js]
let clickToPayInstance

// Create instance once
const instance = clickToPay({
  createOrder: async () => {
    const order = await yourServerSideCall()
    return { publicId: order.token }
  },
  onSuccess: ({ orderId }) => {
    console.log('Payment successful!', orderId)
    // Destroy after successful payment
    clickToPayInstance.destroy()
    window.location.href = `/confirmation?orderId=${orderId}`
  },
  onError: ({ error }) => {
    console.error('Payment failed:', error.message)
  },
  onCancel: () => {
    console.log('Payment cancelled')
  },
})

clickToPayInstance = instance

// Show on click
document.getElementById('click-to-pay-button').addEventListener('click', () => {
  instance.show()
})

// Clean up when no longer needed (e.g., page unload)
window.addEventListener('beforeunload', () => {
  instance.destroy()
})
```


### 5. Handle payment results

Click to Pay payments are processed like standard card payments. The client-side callbacks provide immediate feedback to the customer, while webhooks deliver the final payment outcome to your backend.

#### Client-side callbacks

The widget is configured and mounted. Add the event callbacks to handle the payment results. Add `onSuccess`, `onError`, and `onCancel` inside the `clickToPay({...})` configuration object:

- ![$With async await]

  ```js [my-app.js] {18-22,24-27,29-32}
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  const clickToPayButton = document.getElementById('click-to-pay-button')

  clickToPayButton.addEventListener('click', async () => {
    const instance = clickToPay({
      createOrder: async () => {
        // Call your backend here to create an order and return order.token
        const order = await yourServerSideCall()
        return { publicId: order.token }
      },

      onSuccess: ({ orderId }) => {
        // Payment completed successfully
        console.log('Payment successful!', orderId)
        window.location.href = `/confirmation?orderId=${orderId}`
      },

      onError: ({ error, orderId }) => {
        console.error('Payment failed:', error.message, orderId)
        alert(`Payment failed: ${error.message}`)
      },

      onCancel: ({ orderId }) => {
        console.log('Payment cancelled', orderId)
        alert('Payment was cancelled.')
      },
    })

    instance.show()
  })
  ```

- ![$Without async await]

  ```js [my-app.js] {17-20,22-25,27-30}
  import RevolutCheckout from '@revolut/checkout'

  const clickToPayButton = document.getElementById('click-to-pay-button')

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    clickToPayButton.addEventListener('click', () => {
      const instance = clickToPay({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ publicId: order.token }))
        },

        onSuccess: ({ orderId }) => {
          console.log('Payment successful!', orderId)
          window.location.href = `/confirmation?orderId=${orderId}`
        },

        onError: ({ error, orderId }) => {
          console.error('Payment failed:', error.message, orderId)
          alert(`Payment failed: ${error.message}`)
        },

        onCancel: ({ orderId }) => {
          console.log('Payment cancelled', orderId)
          alert('Payment was cancelled.')
        },
      })

      instance.show()
    })
  })
  ```

| Callback | Description |
| -------- | ----------- |
| `onSuccess` | Fires when the payment has been authorised. With automatic capture (default), the payment is captured immediately after. With manual capture, you [capture the payment](/docs/api/merchant#capture-order) separately. Redirect to a confirmation page or display a success message. |
| `onError` | Fires when an error occurs. Receives [`RevolutCheckoutError`](/docs/sdks/merchant-web-sdk/types/revolut-checkout-error). Use it to display error messaging, log diagnostics, or prompt the user to try again. |
| `onCancel` | Fires when the user cancels the payment. `orderId` may be `undefined` if order creation failed. Use it to re-enable the checkout form or track abandonment. |

:::note
With automatic capture (default), authorisation and capture happen in quick succession. If you use manual capture, `onSuccess` fires after authorisation - you must [capture the payment](/docs/api/merchant#capture-order) separately to complete the transaction.
:::

:::warning [Callbacks are not guaranteed]
Widget callbacks are not guaranteed to fire due to network issues, browser closures, or ad-blockers. Always use [webhooks](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks) for critical backend operations like order fulfilment.
:::

#### Webhooks

Set up your backend to receive webhooks for payment lifecycle events. Click to Pay uses the same standard card payment webhook events as regular card payments - there are no Click-to-Pay-specific webhook events.

- ![Request]

  Send a `POST` request to the [Create a webhook](/docs/api/merchant#create-webhook) endpoint with your webhook URL and the events you want to subscribe to:

  ```http [Request example]
  POST /api/webhooks HTTP/1.1
  Host: merchant.revolut.com
  Authorization: Bearer sk_abcdef12347890_...
  Content-Type: application/json

  {
    "url": "https://example.com/webhooks",
    "events": [
      "ORDER_COMPLETED",
      "ORDER_AUTHORISED",
      "ORDER_PAYMENT_DECLINED",
      "ORDER_PAYMENT_FAILED",
      "ORDER_CANCELLED",
      "ORDER_FAILED"
    ]
  }
  ```

  | Parameter | Description | Required |
  |-----------|-------------|----------|
  | `url` | Your webhook URL. Must be a valid HTTP or HTTPS URL capable of receiving `POST` requests. | Yes |
  | `events` | Array of event types to subscribe to. See [Webhook events](/docs/api/merchant#create-webhook) for the full list. | Yes |

- ![Response]

  The API returns the created webhook object, including the `signing_secret` you'll use to [verify webhook payloads](/docs/guides/merchant/monitor-and-observe/webhooks/verify-the-payload-signature):

  ```json [Response example]
  {
    "id": "181c49cb-bedc-4ae2-9e1c-61d51b99867f",
    "url": "https://example.com/webhooks",
    "events": [
      "ORDER_CANCELLED",
      "ORDER_PAYMENT_DECLINED",
      "ORDER_AUTHORISED",
      "ORDER_COMPLETED",
      "ORDER_PAYMENT_FAILED",
      "ORDER_FAILED"
    ],
    "state": "ACTIVE",
    "signing_secret": "wsk_abcdEFGH123456..."
  }
  ```

  | Field | Description |
  |-------|-------------|
  | `id` | The unique identifier of the webhook subscription. |
  | `state` | The state of the webhook subscription. `ACTIVE` means events are being sent. |
  | `signing_secret` | Use this to verify the `Revolut-Signature` header on incoming webhook payloads. See [Verify the payload signature](/docs/guides/merchant/monitor-and-observe/webhooks/verify-the-payload-signature). |

:::info
For detailed information on setting up webhooks, see: [Use webhooks to track order and payment lifecycle](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks).
:::

##### Payment outcomes

<!-- TODO: Verify with dev whether Click to Pay supports manual capture, pre-authorisation, and incremental authorisation. If manual capture is not supported, remove the "Payment authorised" outcome tab below and the ORDER_AUTHORISED event from the webhook examples. -->

Handle the following scenarios:

- ![Payment successful]

  The payment has been authorised by the card network.

  **Webhook expected:**

  | Webhook event | Description |
  |---------------|-------------|
  | `ORDER_AUTHORISED` | The payment has been authorised but not yet captured |
  | `ORDER_COMPLETED` | The payment has been captured and completed |

  **What to do:**

  - **Automatic capture (default):** both `ORDER_AUTHORISED` and `ORDER_COMPLETED` fire in quick succession. Fulfil the order once `ORDER_COMPLETED` is received.
  - **Manual capture:** `ORDER_AUTHORISED` fires first. [Capture the payment](/docs/api/merchant#capture-order) to complete the transaction, or [cancel the order](/docs/api/merchant#cancel-order) to decline. `ORDER_COMPLETED` fires after you capture.

- ![Payment declined]

  The payment was declined by the card issuer.

  **Webhook expected:**

  | Webhook event | Description |
  |---------------|-------------|
  | `ORDER_PAYMENT_DECLINED` | Payment was declined by the card issuer |

  **What to do:**

  1. Retrieve the [order](/docs/api/merchant#retrieve-order) or [payment](/docs/api/merchant#retrieve-payment-details) details to get the `decline_reason`.
  1. Notify the customer and direct them to use a different payment method.

  :::info
  See the [list of decline reasons](/docs/guides/merchant/reference/error-codes/decline-reasons) to understand why the payment was unsuccessful.
  :::

- ![Payment failure]

  The payment failed due to a technical error or the order was cancelled.

  **Webhooks expected:**

  | Webhook event | Description |
  |---------------|-------------|
  | `ORDER_PAYMENT_FAILED` | Payment failed due to a technical error |
  | `ORDER_CANCELLED` | Order was cancelled |
  | `ORDER_FAILED` | Order failed |

  **What to do:**

  1. Retrieve the [order](/docs/api/merchant#retrieve-order) details to understand the failure reason.
  1. Notify the customer and offer an alternative payment method.

## Examples

<!-- TODO: Re-add example repo link card when this integration is added to the repo -->

<!-- - [:CodeRepository: Explore our integration examples repository](https://github.com/revolut-engineering/revolut-checkout-example "Discover all available examples and see how different payment solutions are implemented") -->

### Example with minimal required parameters

- ![$With async await]

  ```js [my-app.js]
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  const clickToPayButton = document.getElementById('click-to-pay-button')

  clickToPayButton.addEventListener('click', async () => {
    const instance = clickToPay({
      createOrder: async () => {
        // Call your backend here to create an order and return order.token
        const order = await yourServerSideCall()
        return { publicId: order.token }
      },

      onSuccess: ({ orderId }) => {
        console.log('Payment successful!', orderId)
        window.location.href = `/confirmation?orderId=${orderId}`
      },

      onError: ({ error, orderId }) => {
        console.error('Payment failed:', error.message, orderId)
        alert(`Payment failed: ${error.message}`)
      },
    })

    instance.show()
  })
  ```

- ![$Without async await]

  ```js [my-app.js]
  import RevolutCheckout from '@revolut/checkout'

  const clickToPayButton = document.getElementById('click-to-pay-button')

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    clickToPayButton.addEventListener('click', () => {
      const instance = clickToPay({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ publicId: order.token }))
        },

        onSuccess: ({ orderId }) => {
          console.log('Payment successful!', orderId)
          window.location.href = `/confirmation?orderId=${orderId}`
        },

        onError: ({ error, orderId }) => {
          console.error('Payment failed:', error.message, orderId)
          alert(`Payment failed: ${error.message}`)
        },
      })

      instance.show()
    })
  })
  ```

### Example with additional parameters

- ![$With async await]

  ```js [my-app.js]
  import RevolutCheckout from '@revolut/checkout'

  const { clickToPay } = await RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  })

  const clickToPayButton = document.getElementById('click-to-pay-button')

  clickToPayButton.addEventListener('click', async () => {
    const instance = clickToPay({
      createOrder: async () => {
        // Call your backend here to create an order and return order.token
        const order = await yourServerSideCall()
        return { publicId: order.token }
      },

      billingAddress: {
        countryCode: 'GB',
        region: 'London',
        city: 'London',
        streetLine1: '1 Revolut Street',
        streetLine2: '',
        postcode: 'EC2V 7HN',
      },

      onSuccess: ({ orderId }) => {
        console.log('Payment successful!', orderId)
        window.location.href = `/confirmation?orderId=${orderId}`
      },

      onError: ({ error, orderId }) => {
        console.error('Payment failed:', error.message, orderId)
        alert(`Payment failed: ${error.message}`)
      },

      onCancel: ({ orderId }) => {
        console.log('Payment cancelled', orderId)
        alert('Payment was cancelled.')
      },
    })

    instance.show()
  })
  ```

- ![$Without async await]

  ```js [my-app.js]
  import RevolutCheckout from '@revolut/checkout'

  const clickToPayButton = document.getElementById('click-to-pay-button')

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ clickToPay }) => {
    clickToPayButton.addEventListener('click', () => {
      const instance = clickToPay({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ publicId: order.token }))
        },

        billingAddress: {
          countryCode: 'GB',
          region: 'London',
          city: 'London',
          streetLine1: '1 Revolut Street',
          streetLine2: '',
          postcode: 'EC2V 7HN',
        },

        onSuccess: ({ orderId }) => {
          console.log('Payment successful!', orderId)
          window.location.href = `/confirmation?orderId=${orderId}`
        },

        onError: ({ error, orderId }) => {
          console.error('Payment failed:', error.message, orderId)
          alert(`Payment failed: ${error.message}`)
        },

        onCancel: ({ orderId }) => {
          console.log('Payment cancelled', orderId)
          alert('Payment was cancelled.')
        },
      })

      instance.show()
    })
  })
  ```

## Implementation checklist

:::warning
Click to Pay is **not available in the Sandbox environment**. Integration testing requires a production merchant account.
:::

Before deploying your Click to Pay implementation to your live website, complete the checklist below to ensure everything works as expected.

### General checks

- [ ] Click to Pay widget opens correctly when the customer clicks the payment button.
- [ ] Your backend creates the order successfully when the widget is triggered.
- [ ] Widget successfully calls `createOrder` function to fetch and use the order `token`.
- [ ] Customer can authenticate with their card network (Visa or Mastercard) within the pop-up.
- [ ] Payment flow:
    - [ ] Recognised customers can complete payment without manual card entry.
    - [ ] Returning but unrecognised customers can verify with OTP and select saved cards.
    - [ ] New customers can enter card details and complete payment.
    - [ ] Payment completes successfully and `onSuccess` callback fires.
    - [ ] Customer is redirected to a confirmation page.

- [ ] Error handling works as expected:
    - [ ] Failed payments trigger the error callback (`onError`).
    - [ ] Customer sees appropriate error messaging.
    - [ ] Cancel callback (`onCancel`) is triggered when payment is abandoned.

### Webhook verification

- [ ] [Webhook endpoint is set up](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks) to receive order and payment updates.
- [ ] Webhook subscription created with `ORDER_AUTHORISED`, `ORDER_COMPLETED`, `ORDER_PAYMENT_DECLINED`, `ORDER_PAYMENT_FAILED`, `ORDER_CANCELLED`, and `ORDER_FAILED` events.
- [ ] `ORDER_COMPLETED` webhook event is received when a payment completes successfully.
- [ ] If using manual capture, `ORDER_AUTHORISED` is received after authorisation, and `ORDER_COMPLETED` is received after you [capture the payment](/docs/api/merchant#capture-order).
- [ ] Your backend only fulfils orders after receiving the `ORDER_COMPLETED` webhook event - not on `ORDER_AUTHORISED`.
- [ ] Webhook signature verification is implemented for security.

### Click to Pay specific checks

- [ ] Billing address collected on your frontend is correctly passed to the SDK via the `billingAddress` parameter for payment processing.
- [ ] Country availability confirmed for target markets - see the [country availability list](/docs/guides/merchant/accept-payments/online-payments/click-to-pay/introduction#available-countries).
- [ ] Widget handles all three consumer journeys (recognised, returning, new) correctly.
- [ ] Instance is properly destroyed when the widget is closed or the page is unloaded (call `.destroy()` in your cleanup logic).

Once your implementation passes all the checks, you can confidently deploy it to your live website.

These checks only cover the implementation path described in this tutorial. If your application handles more features of the Merchant API, see the [Implementation checklists](/docs/guides/merchant/test-and-go-live/testing/implementation-checklists).

:::tip
**Congratulations!** You've successfully implemented Click to Pay and are ready to accept payments with a faster, more secure checkout experience.
:::

## What's next

- [:CodeRepository: Click to Pay Web SDK reference](/docs/sdks/merchant-web-sdk/payment-methods/click-to-pay 'Full SDK reference for the web SDK')
- [:ArrowExchange: Order and payment lifecycle](/docs/guides/merchant/reference/order-lifecycle 'Understand how orders and payments are processed')
- [:Webhook: Using webhooks](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks 'Track the payment lifecycle with server-to-server webhooks')
- [:HelpChat: Decline reasons](/docs/guides/merchant/reference/error-codes/decline-reasons 'Understand why card payments may be declined')
- [:Repayment: Refunds](/docs/guides/merchant/operations/refunds 'Learn how to refund your orders')
- [:DocumentChecked: Order management](/docs/api/merchant#retrieve-order-list 'Explore the full capabilities of the Orders API')