# Accept payments via Hosted Checkout Page - API

Welcome to the API integration guide for Hosted Checkout Pages! This guide walks you through creating payment links programmatically using the Merchant API, implementing order management, and handling the complete payment lifecycle—from order creation to fulfilment.

This guide covers everything you need for a production-ready integration, including the recommended **3-layered architecture**, webhook implementation, abandoned order handling, and best practices for order fulfilment. The solution is **web-based** and works seamlessly on both desktop and mobile browsers, making it a versatile option for accepting payments without requiring native app integration.

![Hosted Checkout Page - Checkout page](/img/accept-payments/payment-methods/hosted-checkout-page/hosted-payment-page.png 'Hosted Checkout Page - Checkout page')

:::tip
Prefer a visual interface? Check out the [Payment link guide](/docs/guides/merchant/accept-payments/online-payments/hosted-checkout-page/payment-link) for creating payment requests directly from the Revolut Business dashboard without coding.
:::

## How it works

Implementing Hosted Checkout Page via the API requires the following components:

1. **Your order management system:** Your internal e-commerce platform that handles orders, inventory, fulfilment, and customer management.
1. **Merchant API integration:** Your backend integrates with the Merchant API to create orders and receive unique `checkout_url` links for each transaction.
1. **Status tracking mechanism:** A webhook server (recommended) or polling mechanism that monitors order status changes in the Merchant API and triggers appropriate actions in your order management system.

This architecture ensures clean separation of concerns: Revolut handles the checkout experience and payment processing, while you maintain full control over your business logic, inventory, and fulfilment workflows.

### Payment flow

The typical payment flow involves these steps:

1. **Order creation:** Your server creates an order via the [Merchant API: Create an order](/docs/api/merchant#create-order) endpoint and receives a `checkout_url`.
1. **Customer checkout:** You redirect the customer to the `checkout_url` or share it with them via email/SMS.
1. **Payment processing:** Customer completes payment on the Revolut-hosted checkout page using their preferred payment method. If payment fails, the Hosted Checkout Page automatically handles retries.
1. **Post-payment:** Customer is redirected to a success page (default Revolut page or your custom `redirect_url`).
1. **Status notification:** Your server receives webhook notifications for payment events (recommended), or polls the order status.
1. **Order fulfilment:** Your server verifies the final payment status (`completed` or `authorised`) and fulfils the order through your order management system.

### Customise your checkout page

You can customise the branding of your Hosted Checkout Pages (logo, cover image, button colours, website) through the Revolut Business dashboard. This customisation applies to all checkout pages created via both Payment link and API methods.

:::info
For detailed instructions on customising your checkout page, see the [Payment link guide: Customise your checkout page](/docs/guides/merchant/accept-payments/online-payments/hosted-checkout-page/payment-link#branding).
:::

### Implementation overview

Check the following high-level overview on how to implement Hosted Checkout Page via the API:

1. [Create an order](#1-create-an-order)
1. [Get customers to the checkout URL](#2-get-customers-to-the-checkout-url)
1. [Track order and payment status](#3-track-order-and-payment-status)
1. [Use webhooks for fulfilment](#4-use-webhooks-for-fulfilment)

### Before you begin

Before you start this tutorial, ensure you have completed the following steps:

- [Get started: 1. Apply for a Merchant account](/docs/guides/merchant/get-started)
- [Get started: 2. Generate API keys](/docs/guides/merchant/get-started)

## Implement Hosted Checkout Page

This section walks you through the API implementation step by step.

### 1. Create an order

To create a Hosted Checkout Page, you need to create an order using the [Merchant API: Create an order](/docs/api/merchant#create-order) endpoint. This endpoint accepts order details and returns an order object containing the `checkout_url` and `id`.

:::warning
Order creation must always be performed on your server. This is a critical security requirement because the request requires your **Secret API Key**, which must never be exposed in client-side code (frontend).
:::

**Required parameters:**

| Parameter  | Description                                                      |
| ---------- | ---------------------------------------------------------------- |
| `amount`   | The payment amount in the lowest denomination                    |
| `currency` | [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code |

**Useful optional parameters:**

| Parameter                       | Description                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------- |
| `description`                   | Description of the order (visible to the customer)                               |
| `customer.email`                | Customer email address for sending receipts                                      |
| `redirect_url`                  | Custom URL to redirect customers after successful payment                        |
| `merchant_order_data.reference` | Your internal order reference/ID for correlating Revolut orders with your system |

**Example response:**

The API returns a JSON object representing the created order. The most important field for this integration is `checkout_url`.

```json {2,12}
{
  "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"
}
```

| Parameter      | Description                                                                            |
| -------------- | -------------------------------------------------------------------------------------- |
| `id`           | The order ID. Save this to track its status or retrieve order details later.           |
| `checkout_url` | The unique URL for this payment. This is where your customer can complete the payment. |

::::details [Custom post-payment redirection (optional)]

By default, customers are redirected to Revolut's confirmation page after a successful payment. To provide a branded post-payment experience and handle the flow on your own website, you can specify a custom `redirect_url` when creating the order.

:::info
The `redirect_url` is only triggered after **successful payments**. When payment attempts fail, customers remain on the checkout page, allowing them to retry with a different payment method.
:::

**When to use custom redirection:**

- You want to show a branded success page
- You need to display order-specific information after payment
- You want to guide customers to the next step in your workflow (e.g., download page, account dashboard)

**Example payload with custom redirect:**

```json {5}
{
  "amount": 1000,
  "currency": "GBP",
  "description": "Invoice #12345",
  "redirect_url": "https://example.com/payment-confirmation"
}
```

:::info
Custom redirection is only supported via the API. Payment links generated via the GUI always use Revolut's self-hosted confirmation pages.
:::

**How it works:**

1. Customer completes payment successfully on the Revolut-hosted checkout page.
1. Customer is automatically redirected to your specified `redirect_url`.
1. Your redirect page should verify the payment status server-side using the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint before displaying confirmation.
1. Display the appropriate success message and next steps to the customer.

:::warning
Never rely solely on the redirect for critical business logic like order fulfilment. Always verify payment status server-side and use webhooks to trigger fulfilment workflows.
:::

::::

### 2. Get customers to the checkout URL

Once you have the `checkout_url`, you need to direct your customers to it. There are two main approaches depending on your use case:

- ![Redirect from your website]

  **Use this approach when:** Customers are already on your website during the checkout process (e.g., e-commerce checkout flows).

  After creating the order on your server, redirect the customer to the `checkout_url` using your frontend logic. Here are some common approaches:

  **Web (JavaScript):**

  After calling your backend API to create the order, use the returned `checkout_url` to redirect:

  ```javascript
  // Fetch checkout URL from your backend
  const response = await fetch('/api/create-order', { method: 'POST' })
  const order = await response.json()

  // Redirect to checkout URL
  window.location.href = order.checkout_url

  // Or open in new tab
  window.open(order.checkout_url, '_blank')
  ```

  **HTML:**

  You can populate the checkout URL dynamically using server-side rendering or JavaScript:

  ```html
  <!-- Option 1: Server-side rendering (e.g., in your template) -->
  <!-- Use your template engine's syntax to output the checkout URL as a link -->

  <!-- Option 2: Set the link target dynamically with JavaScript -->
  <!-- Give your link element an id, then set its href in JS -->
  <script>
    // After getting checkout_url from your backend
    document.getElementById('checkout-link').href = checkoutUrl
  </script>

  <!-- Option 3: Button with JavaScript -->
  <button id="pay-button">Proceed to Payment</button>
  <script>
    document
      .getElementById('pay-button')
      .addEventListener('click', async () => {
        const response = await fetch('/api/create-order', { method: 'POST' })
        const order = await response.json()
        window.location.href = order.checkout_url
      })
  </script>
  ```

  :::tip
  These are example approaches to help you get started. You can implement the redirect using any method that works with your technology stack and framework. The key is to ensure customers are directed to the `checkout_url` returned by the API.
  :::

  The customer will be taken to the Revolut-hosted checkout page, complete payment, and then be redirected back to your site (if you've configured a `redirect_url`).

- ![Share the checkout URL]

  **Use this approach when:** You need to send payment requests remotely (e.g., invoicing, pay-by-link scenarios).

  Send the `checkout_url` to your customer via your preferred communication channel:
  - **Email**: Include the checkout link in invoice or payment request emails
  - **SMS**: Send the link via text message for quick mobile access
  - **Messaging apps**: Share via messaging platforms
  - **In-app notification**: Push the link to your mobile app users
  - **QR code**: Generate a QR code for in-person scenarios (receipts, terminals)

  When the customer clicks the link, they'll be taken to the Revolut-hosted checkout page where they can complete payment using their preferred method.

  :::tip
  This approach is ideal for invoicing workflows, payment requests, and any scenario where the customer isn't actively on your website/app when you create the order.
  :::

### 3. Track order and payment status

Understanding the status of your orders and payments is crucial for implementing proper business logic. The Merchant API provides detailed information about both order-level and payment-level statuses.

#### Understanding orders and payments

An **order** represents a customer's purchase intent and contains one or more **payment attempts**. A single order can have multiple payment objects if the customer's initial payment attempt fails, and they retry.

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

#### Retrieve order status

Use the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint to check the overall order status and corresponding payment attempts:

```json {3,7-14}
{
  "id": "6516e61c-d279-a454-a837-bc52ce55ed49",
  "state": "completed",
  "amount": 1000,
  "currency": "GBP",
  "outstanding_amount": 0,
  "payments": [
    {
      "id": "8f7d3c2b-1e4a-4b9c-8d6e-2f5a7c9e1b3d",
      "state": "completed",
      "amount": 1000,
      ...
    }
  ],
  ...
}
```

**Order states:**

| State        | Description                                      | Final state                                     |
| ------------ | ------------------------------------------------ | ----------------------------------------------- |
| `pending`    | Order created, awaiting payment attempt          | :Cross:                                         |
| `processing` | Payment attempt in progress                      | :Cross:                                         |
| `authorised` | Payment authorised (for manual capture mode)     | :Check: (fulfilment trigger for manual capture) |
| `completed`  | Payment successfully captured and settled        | :Check: (fulfilment trigger)                    |
| `cancelled`  | Order cancelled (manually or due to auth expiry) | :Check:                                         |
| `failed`     | Order expired without successful payment         | :Check:                                         |

:::warning
Only trigger fulfilment and business logic when orders reach **final states**. Do not fulfil orders in `pending` or `processing` states as these are transient and may change.

- **For automatic capture mode:** Trigger fulfilment on `completed` state
- **For manual capture mode:** Trigger fulfilment on `authorised` state, then [capture the payment manually](/docs/guides/merchant/operations/capture-and-settlement/capture-later)
  :::

:::info[Important distinction]
Receiving `ORDER_PAYMENT_FAILED` or `ORDER_PAYMENT_DECLINED` webhooks does **not** mean the order has reached a **final unsuccessful state**. These events indicate individual payment attempt failures, but the customer can retry payment on the same order.

An order only reaches a final unsuccessful state through:

- **Manual cancellation**: You cancel via [Cancel an order](/docs/api/merchant#cancel-order) → `ORDER_CANCELLED` webhook → state becomes `cancelled`
- **Automatic expiration**: Order exceeds configured period (default 30 days, or custom via `expire_pending_after`) → `ORDER_FAILED` webhook → state becomes `failed`

Final successful states: `completed`, `authorised`
Final unsuccessful states: `cancelled`, `failed`
:::

#### Retrieve payment details

For more granular information about specific payment attempts, extract the payment ID from the order response and use the [Retrieve payment details](/docs/api/merchant#retrieve-payment-details) endpoint:

```json {4}
{
  "id": "8f7d3c2b-1e4a-4b9c-8d6e-2f5a7c9e1b3d",
  "order_id": "6516e61c-d279-a454-a837-bc52ce55ed49",
  "state": "completed",
  "amount": 1000,
  "currency": "GBP",
  "created_at": "2023-09-29T14:58:36.079398Z",
  "updated_at": "2023-09-29T15:02:18.123456Z",
  ...
}
```

Payment details provide specific information about why a payment attempt succeeded or failed, which is useful for troubleshooting and customer support.

#### Automatic error handling

The Hosted Checkout Page automatically handles payment errors and retries, requiring no additional implementation from merchants.

**How it works:**

1. **Payment fails:** When a payment attempt fails (declined card, insufficient funds, etc.), the Hosted Checkout Page displays an error message to the customer.
1. **Automatic retry option:** The customer sees a **Try again** button on the error page.
1. **Retry flow:** When the customer clicks **Try again**, the Hosted Checkout Page automatically restarts the payment flow.
1. **New payment object:** This creates a new payment attempt, which appears as a new entry in the `order.payments` array.
1. **Same order ID:** All retry attempts are linked to the same order ID, maintaining transaction consistency.

:::info
The `checkout_url` remains valid and can be reused for multiple payment attempts. Each attempt creates a new payment object in the `payments` array, allowing you to track the full payment history for a single order.
:::

**For merchants:**

You don't need to implement any retry logic. Simply monitor the order status through webhooks or polling:

- **Failed attempts:** You'll receive `ORDER_PAYMENT_FAILED` or `ORDER_PAYMENT_DECLINED` webhook events for each failed attempt.
- **Multiple payments array:** When retrieving the order via [Retrieve an order](/docs/api/merchant#retrieve-order), you'll see multiple entries in the `payments` array if the customer retried.
- **Final state:** Only trigger fulfilment logic when the order reaches a final state:
  - `completed` → `ORDER_COMPLETED` webhook
  - `authorised` → `ORDER_AUTHORISED` webhook (for manual capture mode)
  - `cancelled` → `ORDER_CANCELLED` webhook
  - `failed` → `ORDER_FAILED` webhook (order expires without payment)

### 4. Use webhooks for fulfilment

:::info
Webhooks are the **recommended method** for tracking payment status in real-time and should be used for all fulfilment logic. While polling is possible, it introduces delays and risks missing critical status changes.
:::

Webhooks provide immediate notifications for order and payment events, eliminating the need for polling and ensuring you never miss a status change. They are essential for:

- **Order fulfilment**: Trigger fulfilment workflows immediately upon successful payment
- **Inventory management**: Update stock levels in real-time
- **Customer notifications**: Send confirmation emails/SMS instantly
- **Abandoned order handling**: Receive notifications when orders are cancelled
- **Financial reconciliation**: Track all transactions accurately

#### Relevant webhooks for Hosted Checkout Page

| Webhook event            | Description                                                  | When sent                                                                              |
| ------------------------ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `ORDER_COMPLETED`        | Order successfully paid and captured (trigger fulfilment)    | Payment captured and settled                                                           |
| `ORDER_AUTHORISED`       | Order authorised, awaiting capture (for manual capture mode) | Payment authorised but not yet captured                                                |
| `ORDER_CANCELLED`        | Order cancelled                                              | Order cancelled via API                                                                |
| `ORDER_FAILED`           | Order expired without successful payment                     | Order expiration period elapsed (default 30 days or custom via `expire_pending_after`) |
| `ORDER_PAYMENT_DECLINED` | Payment attempt declined by processor                        | Card declined, insufficient funds, etc.                                                |
| `ORDER_PAYMENT_FAILED`   | Payment attempt failed (technical or processing error)       | Technical failure, processing error                                                    |

#### Understanding `ORDER_CANCELLED`

The `ORDER_CANCELLED` webhook is triggered when:

- You cancel an order via the [Cancel an order](/docs/api/merchant#cancel-order) endpoint (e.g., due to [abandonment detection](#handling-abandoned-orders))
- An authorised payment expires without being captured (in manual capture mode)
  - **Default**: Authorised orders automatically cancel after **7 days**
  - **Custom**: Set `cancel_authorised_after` ([ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format, max 7 days) when creating the order

- The order is manually cancelled through the Revolut Business dashboard

When you receive this event, your system should:

- Release any reserved inventory
- Update your order management system to mark the order as cancelled
- Trigger any cleanup or notification workflows
- Ensure fulfilment logic does not process this order

#### Understanding `ORDER_FAILED`

The `ORDER_FAILED` webhook is triggered when an order expires without successful payment. By default, orders expire after **30 days**, but you can customise this using the `expire_pending_after` parameter when creating an order.

**When it's triggered:**

- Order remains in `pending` or `processing` state beyond the expiration period
- No successful payment was completed before expiration
- Order automatically transitions to `failed` state

**Controlling expiration:**

Set a custom expiration period when creating an order using `expire_pending_after` ([ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format):

```json
{
  "amount": 1000,
  "currency": "GBP",
  "expire_pending_after": "PT30M" // 30 minutes (ISO 8601 Duration)
}
```

Common duration examples:

- `"PT5M"` = 5 minutes
- `"PT15M"` = 15 minutes
- `"PT1H"` = 1 hour
- `"PT24H"` = 24 hours
- `"P7D"` = 7 days

**When you receive this event, your system should:**

- Recognise the order has definitively failed and cannot be completed
- Release any reserved inventory
- Update your order management system to mark the order as expired/failed
- Consider this as an alternative to manual cancellation for abandoned orders (see [Handling abandoned orders](#handling-abandoned-orders))

:::info

- `ORDER_FAILED` represents **true abandonment** via automatic expiration, while `ORDER_CANCELLED` represents **active intervention** (manual cancellation via API or dashboard, or authorised payment expiry).
- For detailed webhook implementation instructions, see: [Use webhooks to track the payment lifecycle](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks).
  :::

::::details [Alternative: Polling for order status]

If you cannot implement webhooks, you can use polling as an alternative, though it's less efficient and introduces delays. With polling, your system periodically checks order status using the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint.

**Polling workflow:**

1. **Create order:** [Create an order](/docs/api/merchant#create-order) and store the order `id` in your system.
1. **Start polling:** Begin polling the order status at regular intervals (e.g., every 10-30 seconds).
1. **Check final states:** Monitor for final states (`completed`, `authorised`, `cancelled`, `failed`).
1. **Trigger actions:** When a final state is reached, trigger appropriate business logic.
1. **Stop polling:** Once a final state is reached, stop polling for that order.

**Polling limitations:**

- **Delayed notifications:** Status changes are only detected during polling intervals
- **Increased API calls:** Requires more API requests, especially for high-volume merchants
- **Risk of missing updates:** If polling stops prematurely, you may miss state changes
- **Resource intensive:** Requires maintaining active polling processes for all orders

:::warning
For production implementations, **webhooks are strongly recommended** over polling. Polling should only be used as a temporary solution or for low-volume scenarios where webhooks cannot be implemented.
:::

::::

## Handling abandoned orders

When a customer abandons checkout without completing payment, you need to decide how to manage these orders. Revolut provides two complementary approaches that can be used independently or together, depending on your business needs.

### Why abandoned order handling matters

Abandoned orders can:

- Lock inventory in your system unnecessarily
- Create confusion in order management and reporting
- Prevent accurate revenue forecasting
- Lead to operational inefficiencies

Implementing proper abandonment detection helps you maintain accurate order states and clean up stale orders that will never be completed.

### Approaches to handling abandoned orders

You can use one or both of these approaches based on your business requirements:

- ![Manual cancellation]

  **Best for:** Tight inventory control, active order lifecycle management, or when orders sync with your OMS

  With manual cancellation, you detect abandoned orders based on your business rules and actively cancel them.

  :::info
  This approach can be combined with automatic expiration for redundancy. See [Combining both approaches](#combining-both-approaches-recommended).
  :::

  **How it works:**
  1. Set your detection threshold
  2. Monitor orders via webhooks or polling
  3. Call [Cancel an order](/docs/api/merchant#cancel-order) when threshold exceeded
  4. Handle `ORDER_CANCELLED` webhook or poll for `cancelled` state
  5. Update your system (release inventory, etc.)

  #### Webhook-based detection (recommended)

  **Best for:** Merchants using webhooks for order status tracking

  If you're using webhooks to track order status, implement a timer-based detection mechanism:
  1. **Start timer on order creation:** When you create an order via the API and share the `checkout_url`, start a timer based on your chosen threshold
  2. **Monitor for completion:** If you receive an `ORDER_COMPLETED` or `ORDER_AUTHORISED` webhook event during this period, cancel the timer
  3. **Detect abandonment:** If the timer expires without receiving a completion event, mark the order as abandoned in your system
  4. **Cancel the order:** Call the [Cancel an order](/docs/api/merchant#cancel-order) endpoint to formally cancel the order in Revolut's system
  5. **Listen for confirmation:** Your webhook endpoint will receive an `ORDER_CANCELLED` event confirming the cancellation
  6. **Update your system:** Trigger your order management logic (release inventory, update status, etc.)

  #### Polling-based detection

  **Best for:** Merchants using polling instead of webhooks

  If you're polling order status instead of using webhooks, check for abandoned orders periodically:
  1. **Track order age:** Store the creation timestamp for each order in your system
  2. **Periodic polling:** Set up a background job that runs periodically (e.g., every 15 minutes)
  3. **Check order status:** For each order older than your threshold, poll the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint
  4. **Detect abandonment:** If the order is still in `pending` or `processing` state after the threshold, consider it abandoned
  5. **Cancel the order:** Call the [Cancel an order](/docs/api/merchant#cancel-order) endpoint
  6. **Verify cancellation:** Poll the order status again to confirm it's in `cancelled` state
  7. **Update your system:** Trigger your order management logic (release inventory, update status, etc.)

  #### Choose your cancellation timeframe

  | Timeframe         | Approach   | Best for                                                                  |
  | ----------------- | ---------- | ------------------------------------------------------------------------- |
  | **&lt;5 minutes** | Aggressive | Extremely limited inventory (flash sales, concert tickets, limited drops) |
  | **5-15 minutes**  | Balanced   | Standard e-commerce with inventory management needs                       |
  | **>30 minutes**   | Lenient    | Flexible inventory, digital goods, or services                            |

  :::tip
  Select your threshold based on your constraints, but also **decide what actions to take**:
  - Cancel immediately and release inventory?
  - Send reminder emails before cancelling?
  - Mark as abandoned but keep order active?
  - Different handling by order value or customer segment?

  Manual cancellation gives you full control over these decisions.
  :::

  **When to use manual cancellation:**
  - Tight inventory control required
  - Orders revoked/cancelled in your OMS need to sync to Revolut
  - Want to send reminders before cancellation
  - Need different handling for customer segments
  - Require audit trails of cancellation triggers

- ![Automatic expiration]

  **Best for:** "Set and forget" management, flexible inventory, or lower engineering complexity

  With automatic expiration, set `expire_pending_after` when creating the order, and Revolut automatically fails it if unpaid.

  :::info
  This approach can be combined with manual cancellation for faster cleanup. See [Combining both approaches](#combining-both-approaches-recommended).
  :::

  **How it works:**
  1. Include `expire_pending_after` ([ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format) in [Create an order](/docs/api/merchant#create-order):
     ```json
     {
       "amount": 1000,
       "currency": "GBP",
       "expire_pending_after": "PT15M" // 15 minutes
     }
     ```
  2. Order expires automatically if payment not completed
  3. Receive `ORDER_FAILED` webhook (or poll for `state = failed`)
  4. Update your system (release inventory, etc.)

  **Default behaviour:**
  - Without `expire_pending_after`: orders expire after **30 days** (`"P30D"`)
  - After expiration: `ORDER_FAILED` webhook triggers, state becomes `failed`
  - `checkout_url` becomes invalid

  **Recommended expiration periods:**

  | Period                | Duration              | Best for                                             |
  | --------------------- | --------------------- | ---------------------------------------------------- |
  | **5-15 minutes**      | `"PT5M"` to `"PT15M"` | Limited inventory needing automatic cleanup          |
  | **30-60 minutes**     | `"PT30M"` to `"PT1H"` | Standard e-commerce                                  |
  | **2-24 hours**        | `"PT2H"` to `"PT24H"` | Invoicing, B2B payments needing customer flexibility |
  | **30 days (default)** | `"P30D"`              | Maximum flexibility, minimal pressure                |

  **When to use automatic expiration:**
  - Flexible inventory or digital goods
  - Simple, automated order lifecycle
  - Don't need intermediate actions (reminders, grace periods)
  - Lower transaction volumes
  - Want to minimize engineering complexity

### Combining both approaches

For the most robust implementation, use both approaches together. This provides multiple layers of protection and ensures orders are cleaned up efficiently.

**How it works:**

1. **Set automatic expiration as a safety net:**
   - Configure `expire_pending_after` with a reasonable period (e.g., 24 hours: `"PT24H"`)
   - Ensures orders eventually fail even if manual cancellation doesn't trigger

2. **Implement manual cancellation for faster cleanup:**
   - Active detection with shorter threshold (e.g., 15 minutes)
   - Immediate inventory release and order cleanup
   - Allows for business-specific actions (reminders, notifications)

3. **Benefits of using both:**
   - **Redundancy:** Manual cancellation cleanup happens quickly; automatic expiration catches edge cases
   - **Flexibility:** Different thresholds for different urgency levels
   - **Reliability:** Orders always reach final states, even if manual process fails
   - **Audit trail:** Distinguish between actively cancelled orders vs truly abandoned orders

**Example scenario:**

```json
// Create order with 24-hour expiration as safety net
{
  "amount": 1000,
  "currency": "GBP",
  "expire_pending_after": "PT24H" // Automatic expiration after 24 hours
}
```

Then implement manual cancellation logic to cancel after 15 minutes of inactivity. Result:

- Orders typically cancelled manually after 15 minutes → `ORDER_CANCELLED` webhook → `cancelled` state
- If manual cancellation fails, order automatically expires after 24 hours → `ORDER_FAILED` webhook → `failed` state

:::tip
Using both approaches together is the **best practice** for production environments. It combines the control of manual cancellation with the reliability of automatic expiration.
:::

### Handling `ORDER_CANCELLED` and `ORDER_FAILED` events

Whether using manual cancellation, automatic expiration, or both, handle both final unsuccessful states properly.

**Common actions for both events:**

- Release inventory
- Update order management system
- Customer communication (optional)
- Cleanup temporary data/sessions
- Update analytics/reporting

**Event-specific handling:**

| Approach             | Webhook           | State       | Trigger                                                         |
| -------------------- | ----------------- | ----------- | --------------------------------------------------------------- |
| Manual cancellation  | `ORDER_CANCELLED` | `cancelled` | You cancelled via API, dashboard, or authorised payment expired |
| Automatic expiration | `ORDER_FAILED`    | `failed`    | Order exceeded `expire_pending_after` period                    |

**Implementation:**

- **Webhooks**: Listen for both `ORDER_CANCELLED` and `ORDER_FAILED` events
- **Polling**: Check for `state = 'cancelled'` or `state = 'failed'`

:::warning
Never fulfil orders in `cancelled` or `failed` states. These are final unsuccessful states.

Only fulfil on `completed` (automatic capture) or `authorised` (manual capture) — the final successful states.
:::

## Implementation checklist

Before deploying your implementation to your production environment, complete the checklist below to see if everything works as expected, using the Merchant API's Sandbox environment. To test in Sandbox environment, set the base URL of your API calls to: `https://sandbox-merchant.revolut.com/`.

### Order creation

- [ ] Your server successfully creates orders via the API
- [ ] The API returns a valid `checkout_url` and `id`
- [ ] Order creation works with minimal parameters (`amount`, `currency`)
- [ ] Order creation works with optional parameters (`description`, `customer.email`, `redirect_url`)

### Customer experience

- [ ] Customers can access the checkout page via the `checkout_url`
- [ ] The checkout page displays correctly on both desktop and mobile
- [ ] Customers can complete payment using test cards: [Test cards for successful payments](/docs/guides/merchant/test-and-go-live/testing/test-cards#test-for-successful-payments)
- [ ] Payment failures are handled gracefully: [Test cards for error cases](/docs/guides/merchant/test-and-go-live/testing/test-cards#test-for-error-cases)
- [ ] [Custom branding](/docs/guides/merchant/accept-payments/online-payments/hosted-checkout-page/payment-link#branding) (logo, colours, cover image) displays correctly if configured

### Payment tracking

- [ ] Your server can retrieve order status via the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint
- [ ] The order response includes the `payments` array showing all payment attempts
- [ ] Your server can retrieve individual payment details via the [Retrieve payment details](/docs/api/merchant#retrieve-payment-details) endpoint using a payment ID from the order
- [ ] Order state changes from `pending` to `completed` after successful payment
- [ ] When a payment attempt fails and customer retries on the same `checkout_url`, the order shows multiple entries in the `payments` array
- [ ] Your implementation correctly handles failed payment attempts using one of the [two approaches](#handling-failed-payment-attempts) (retry same order or create new order)

### Custom redirect (if implemented)

- [ ] Customers are redirected to your custom URL after successful payment completion
- [ ] Your redirect page verifies payment status server-side before showing confirmation
- [ ] Failed payment attempts keep customers on the checkout page (no redirect occurs)
- [ ] Your redirect page handles the order ID from the URL parameters

### Webhooks

- [ ] [Webhook endpoint is set up](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks)
- [ ] Webhook notifications are received for `ORDER_COMPLETED` events
- [ ] Webhook notifications are received for `ORDER_PAYMENT_FAILED` events
- [ ] Webhook notifications are received for `ORDER_CANCELLED` events
- [ ] Webhook notifications are received for `ORDER_FAILED` events (if using automatic expiration)
- [ ] Webhook signature verification is implemented for security
- [ ] Your backend only fulfils orders after receiving the `ORDER_COMPLETED` or `ORDER_AUTHORISED` webhook
- [ ] Your backend correctly handles `ORDER_CANCELLED` events (releases inventory, updates order status)

### Abandoned order handling

**If using manual cancellation:**

- [ ] [Abandonment detection logic](#handling-abandoned-orders) is implemented (webhook-based timer or polling-based)
- [ ] Abandoned orders are cancelled via [Cancel an order](/docs/api/merchant#cancel-order) after your threshold
- [ ] `ORDER_CANCELLED` events are handled appropriately (inventory released, status updated)
- [ ] Cancellation threshold configured for your business needs
- [ ] Webhook-based: Timers cancelled when `ORDER_COMPLETED`/`ORDER_AUTHORISED` received
- [ ] Polling-based: Background jobs correctly identify and cancel stale orders

**If using automatic expiration:**

- [ ] Orders created with appropriate `expire_pending_after` values ([ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format)
- [ ] `ORDER_FAILED` events are handled appropriately (inventory released, status updated)
- [ ] Expiration period tested in Sandbox
- [ ] Expired orders correctly transition to `failed` state

**If using manual capture mode:**

- [ ] `cancel_authorised_after` configured if custom authorisation window needed ([ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format, max `"P7D"`)
- [ ] System handles `ORDER_CANCELLED` events for expired authorisations

**Common (both approaches):**

- [ ] Inventory released when orders reach final unsuccessful states
- [ ] Order management system updated with correct final states
- [ ] Analytics/reporting reflect cancelled/failed orders
- [ ] No fulfilment triggered for `cancelled` or `failed` states

Once your implementation passes all checks in the Sandbox environment, test it in production with small amounts before going live. After verifying everything works correctly in both environments, you can confidently deploy to production.

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

:::tip
**Congratulations!** You've successfully implemented Hosted Checkout Page via the API and are ready to accept payments.
:::

## What's next

- [Learn about using webhooks](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks) - Track payment lifecycle events reliably
- [Explore the full Merchant API](/docs/api/merchant) - Discover advanced features and capabilities
- [Learn about the order and payment lifecycle](/docs/guides/merchant/reference/order-lifecycle)
- [Check the Payment link guide](/docs/guides/merchant/accept-payments/online-payments/hosted-checkout-page/payment-link) - Learn about the dashboard method
- [Learn about other payment methods](/docs/guides/merchant/accept-payments/online-payments/revolut-pay/introduction) - Explore Revolut Pay and other options