DEV Community

Cover image for When a simple order update breaks Revolut Checkout workflow
Asoseil
Asoseil

Posted on

When a simple order update breaks Revolut Checkout workflow

During a recent payment gateway integration, i discovered a fundamental design flaw in the Revolut SDK that breaks dynamic checkout flows and forces developers to implement complex workarounds.

The Problem: Static state in a dynamic world

On the web, RevolutCheckout component requires creating a new order via API before rendering the checkout input form. This creates an unusual lifecycle:

  • Frontend requests backend to create a Revolut order
  • Backend calls Revolut API and receives an order token
  • Frontend initializes RevolutCheckout with that token
  • Payment component renders

This flow has a critical limitation: the checkout component caches the initial order state and never re-synchronizes it with the backend.

When things break

In modern single-page checkout experiences, users frequently modify their orders:

  • Adjusting quantities
  • Applying discount codes
  • Adding or removing items
  • Updating shipping options

Each change requires updating the order amount on Revolut's backend. While the API supports this via PATCH /api/orders/{order_id}, the checkout component doesn't handle it.

When you update an order after the checkout component has mounted, attempting to complete the payment fails with a generic error:

"Transaction failed. Please contact the merchant to resolve this problem."

Reproduction

Here's a minimal example that demonstrates the issue:

Create an Order

curl -L -X POST 'https://sandbox-merchant.revolut.com/api/orders' \
-H 'Content-Type: application/json' \
-H 'Revolut-Api-Version: 2025-10-16' \
-H 'Authorization: Bearer <key>' \
--data-raw '{"amount": 500, "currency": "EUR"}'

Response:
json{
  "id": "540d94a9-b356-a52f-8870-dabaac383a1b",
  "token": "b140372e-cb3b-47f8-8c7e-b22090ea4ac2",
  "type": "payment",
  "state": "pending",
  "created_at": "2025-11-07T06:41:45.820229Z",
  "updated_at": "2025-11-07T06:41:45.820229Z",
  "amount": 500,
  "currency": "EUR",
  "outstanding_amount": 500
}
Enter fullscreen mode Exit fullscreen mode

Update the Order (after RevolutCheckout is mount)

curl -L -X PATCH 'https://sandbox-merchant.revolut.com/api/orders/{order_id}' \
-H 'Content-Type: application/json' \
-H 'Revolut-Api-Version: 2025-10-16' \
-H 'Authorization: Bearer <key>' \
--data-raw '{"amount": 1000, "currency": "EUR"}'

Response:
json{
  "id": "540d94a9-b356-a52f-8870-dabaac383a1b",
  "token": "b140372e-cb3b-47f8-8c7e-b22090ea4ac2",
  "type": "payment",
  "state": "pending",
  "created_at": "2025-11-07T06:41:45.820229Z",
  "updated_at": "2025-11-07T06:43:21.785741Z",
  "amount": 1000,
  "currency": "EUR",
  "outstanding_amount": 1000
}
Enter fullscreen mode Exit fullscreen mode

The order ID and token remain valid, but the checkout component will fail when attempting to complete the payment.

Root cause analysis

The issue stems from a state synchronization mismatch between client and server:

RevolutCheckout caches the initial order state (including amount) when initialized, and the cached state is never refreshed, even when the order is updated via API.

When the user submits a payment, the component sends the cached amount, Revolut's backend detects the mismatch with the current amount and rejects the payment with a generic error message that provides no clarity about the actual issue.

This is a classic case of assuming immutability on the client side while allowing mutability on the server side.

Workaround

The only solution is to remount the checkout component whenever the order changes. Something like:

async function updateOrder() {
    try {
        // Update order via API
        const order = await updateRevolutOrder(
            accessToken,
            order.orderId
        );
        // Completely reinitialize the checkout components
        if (order && order.token) {
            await initializeCardField();
            await initializeRevolutPay();
        }
    } catch (e) {
        console.error("Error updating Revolut order:", e);
    }
}
Enter fullscreen mode Exit fullscreen mode

This workaround has significant drawbacks and requires remounting if you use both RevolutCheckout for the card field and Revolut Pay:

  • Poor user experience
    Every order update triggers a visible remount cycle, causing the checkout form to disappear and reappear. This creates flickering UI and resets any partial input.

  • Technical Overhead
    Backend logs become cluttered with abandoned order records while additional API calls are triggered for every modification, requiring complex state management to handle remounting logic and creating potential race conditions between user interactions and component lifecycle.

  • Silent failures
    Without discovering this through extensive testing, payments fail with no indication of the actual problem, and users see generic error messages that incorrectly suggest contacting the merchant, eroding trust in the checkout process.

  • Maintenance burden
    Developers must build and maintain logic that should be abstracted within the SDK itself.

What should happen

A robust checkout SDK should handle order updates in one of two ways:

Option 1: Invalidate on update
When an order is updated via API, invalidate the existing token and require a new checkout initialization. Make this explicit in the API response.

Option 2: Auto-refresh state
Before submitting payment, **automatically fetch the latest order state **from the backend to ensure synchronization. This could happen transparently within the SDK.

Either approach would eliminate the need for workarounds and provide clear, predictable behavior.

Other payment gateways

Stripe's approach, for comparison, separates the concepts of PaymentIntent (immutable) and amount confirmation (mutable). You can update amounts before confirmation without breaking the payment flow, and the Elements components handle state synchronization internally.

This isn't an edge case—order updates are a standard feature of e-commerce checkouts. Dynamic pricing, discount codes, quantity adjustments, and real-time calculations are expected in modern applications.

The RevolutCheckout inability to handle these updates gracefully represents a fundamental architectural flaw. It shifts the complexity that should be handled by the payment gateway onto every merchant integration.

For developers considering Revolut: Be aware that implementing dynamic checkout flows requires building significant workaround logic. Test order update scenarios thoroughly before going to production.

For the Revolut team: This issue has been reported with reproducible test cases. A fix would significantly improve developer experience and bring the SDK in line with modern payment gateway expectations.

Github revolut-checkout issue

Top comments (0)