Revolut for WooCommerce: Why I Built My Own Integration
The off-the-shelf Revolut gateway for WooCommerce is a reasonable choice for plenty of stores, and if yours is one of them, keep it. But on a brand-sensitive checkout it kept failing in the exact place that decides whether a sale happens: unreliable Apple Pay and Google Pay buttons, a payment step that jumped as it loaded, and card fields so bare they quietly told shoppers not to trust the page. The fix is not to build your own card form, which would explode your PCI scope. It is to wrap Revolut's secure hosted card field in a checkout you fully control, render wallet buttons only when the browser can actually pay, and confirm every order from the signed webhook rather than the front end. That is how you get a branded, stable, trustworthy checkout without ever touching raw card data. Every mechanic below is grounded in Revolut's official documentation so you can verify it.
The short version
- The plugin fails where it matters most. It takes card payments fine, but on the one screen where hesitation becomes an abandoned cart, its bare fields, layout jumps, and flaky wallets quietly cost conversions.
- Own the experience without owning the risk. Revolut's
createCardFieldmounts a secure hosted iframe you style around, so card data never touches your code and PCI scope is unchanged while the UI becomes fully yours. - Render wallet buttons only when they will work. Calling
canMakePayment()first and destroying the button otherwise removes the "sometimes there, sometimes not" feeling that makes a checkout look broken. - The webhook is the source of truth. Treat the browser's success callback as a UX signal only and confirm the order from the signed
ORDER_COMPLETEDevent, which kills the "paid but still pending" class of bug. - Apple Pay cannot be tested in the sandbox. Knowing that in advance turns a confusing launch into a planned production check.
- You own the code, the keys, and the brand moment. No wrapper decides how your checkout looks or behaves at the instant a customer is holding their card.
Why does the payment step decide the sale?
The payment step is the most fragile moment in the entire store. A customer who breezed through your catalog and filled a cart will still pause, for a second, when it is time to type a card number. Everything they feel in that second, confidence or doubt, is decided by how the checkout looks and behaves, not by anything you said on the product page. That is the lens worth using here, because it explains a decision that might otherwise sound like over-engineering: choosing to replace a working plugin. The plugin was not broken in the sense of failing to charge cards. It was broken in the sense that mattered, undermining trust at the precise instant trust converts into revenue, and it gave me no way to fix the parts doing the damage.
Where does the off-the-shelf Revolut gateway fall short?
For a simple store, taking a card payment is enough, and the plugin does that. For a checkout a brand actually cares about, three specific things kept eroding the experience.
- The wallets were unreliable. Apple Pay and Google Pay are the fastest, highest-trust way to pay for the customers who have them, and they were not appearing or behaving consistently across devices and browsers. A wallet button that flickers in and out is worse than no button at all, because inconsistency reads as breakage.
- The checkout jumped. The payment area shifted and re-rendered as it loaded and as the customer moved through it. A layout shift, a redirect, a flash: each one reads to a shopper as instability, and instability is the last thing you want a person feeling while their card is in hand.
- The card fields were too bare to trust. This was the decisive one. The default inputs looked so plain and generic that they did not feel like part of the store, and at the moment a customer decides whether to hand over their details, a field that looks unfinished argues quietly against it.
None of these is catastrophic alone. Together, on the single screen where hesitation turns into an abandoned cart, they were expensive, and none of them was something the plugin let me change. This is exactly the judgment we lay out in when you need custom code rather than another plugin: reach for custom precisely when an off-the-shelf tool blocks the thing that matters most, and not a moment sooner.
Can you brand the checkout without taking on card-data risk?
The instinct when card fields look untrustworthy is to build your own card form. That instinct is dangerous, because the moment raw card data touches your inputs and your server, your PCI scope explodes into a far more demanding compliance burden, the kind we walk through in the PCI compliance for custom checkouts guide. The breakthrough was realizing the choice between a trustworthy UI and a safe one is a false one.
Revolut's web SDK exposes the card input as a secure hosted field, an iframe you mount into your own page with createCardField, documented in the card field guide. The sensitive entry stays inside Revolut's iframe, so card data never reaches my code and my PCI scope is unchanged, yet I control everything around it: the container, the typography, the spacing, the labels, the card-brand icons, the inline validation messages, and the trust cues. So I wrapped Revolut's secure element in a checkout that finally looked and felt like the rest of the store.
import RevolutCheckout from '@revolut/checkout';
// order.token comes from creating the order on the server (see below)
const { createCardField } = await RevolutCheckout(order.token, 'prod');
createCardField({
target: document.getElementById('my-branded-card-container'),
onSuccess() { /* authorized: show a pending state, let the webhook confirm */ },
onError(error) { /* render a friendly, on-brand inline error */ }
});
That single change, a polished container around a secure field, did more for conversion than any amount of persuasive copy, because it removed doubt at the exact instant doubt appears. You get the brand and keep the safety, which is the whole reason this approach beats both the bare plugin and the reckless roll-your-own form.
How do you make Apple Pay and Google Pay reliable?
With the UI under my control, the wallets stopped being an unpredictable accident and became a deliberate feature. Two rules made them dependable inside a WooCommerce checkout.
- Render the button only when a wallet is actually available. You call
canMakePayment()first and show the button only on success, destroying it otherwise. That alone erases the "sometimes there, sometimes not" feeling, because the button now appears exactly when tapping it will work. - Register the domain for Apple Pay. The wallet stays hidden until Revolut has verified you own the domain, a one-time setup step I walk through in detail in the Revolut wallets and headless guide. Google Pay needs no equivalent step.
One honest caveat will save you an afternoon: Apple Pay cannot be tested in the sandbox, so it has to be validated with a careful check in production. Knowing that in advance is the difference between a smooth launch and a confusing one, where a wallet that works perfectly for real customers appears "broken" only because it was never going to light up in a test environment.
How is the payment flow built, step by step?
The reason the custom version is not buggy is not magic. It is owning the whole flow and following the documented path rather than a wrapper's assumptions. In order, and grounded in Revolut's docs, the build runs like this.
- Create the order on the server first. The order is created server-side with create order, with the amount in the minor currency unit, and the short-lived token is handed to the browser to initialize the card field.
- Mount the branded card field. The token initializes
createCardFieldinside your styled container, so the customer sees your checkout while the sensitive input lives safely in Revolut's iframe. - Let 3D Secure happen inside the field. Strong customer authentication, including any bank challenge, is handled inside Revolut's secure element, so there is nothing fragile to wire up on the page and no redirect dance to manage yourself.
- Treat the front-end result as a hint, not a fact. The
onSuccesscallback flips the UI to a pending state; it never marks the order paid, because a browser can close, drop its connection, or lie. - Confirm from the signed webhook. The order is marked complete only from the authoritative
ORDER_COMPLETED(orORDER_AUTHORISED) webhook, exactly as Revolut recommends, with the signature verified and the handler made idempotent so a repeated delivery cannot double-process an order. - Reconcile the edges. A missed webhook is caught by a scheduled reconciliation against the order status, so a delivery hiccup never leaves a paid customer stranded in "pending."
Steps four through six are what kill the classic "paid but still pending" bug, and the delivery-guarantee thinking behind them is the whole subject of the reliable webhooks guide. This is the same integration discipline behind why connecting your stack beats copy and paste: when you control the seams, the bugs that live in the seams disappear. The fully serverless version of this exact flow, including the precise signature scheme and how it runs without a WooCommerce plugin at all, is the companion piece on the Revolut Merchant API on Vercel; this article is deliberately about the plugin-replacing WooCommerce build, and that one is about the headless approach.
A checklist before you trust a custom gateway
Before you let any custom payment integration take real money, walk this list.
- Does raw card data stay inside the provider's hosted field, keeping your PCI scope unchanged?
- Is the order created server-side, with the client only ever holding a short-lived token?
- Does the wallet button appear only after
canMakePayment()confirms the browser can pay? - Is the domain registered and the validation file hosted so Apple Pay works in production?
- Is order completion driven by a signature-verified webhook, not the browser callback?
- Is the webhook handler idempotent, so a repeated delivery cannot process an order twice?
- Is there a reconciliation job to catch any webhook that never arrives?
- Do you own the Revolut keys, the code, and the checkout markup, with no wrapper in the middle?
Common pitfalls
The failures here cluster around trusting the wrong thing at the wrong moment, and each one hides until a real order exposes it.
Believing the browser instead of the webhook. The most expensive mistake is marking an order paid from onSuccess. A closed tab or a dropped connection then leaves money taken and status wrong, and the customer emails you angrily about a charge with no confirmation.
Building a real card form to get a nicer look. Reaching for your own inputs to fix ugliness drags raw card data through your server and turns a design problem into a compliance one. The hosted field gives you the same visual control with none of that exposure.
Assuming Apple Pay is broken because the sandbox is silent. Teams burn hours "fixing" a wallet that was always going to be dark in test, when the real step was registering the domain and validating live.
A concrete case. A brand-forward WooCommerce store, details changed, ran the standard gateway and watched mobile conversion sag. Their wallet buttons appeared on some phones and not others, the card box looked like a generic gray rectangle bolted onto an otherwise polished store, and a handful of customers each week reported being charged while their order sat "pending" because a support agent had been manually reading the browser result. We rebuilt the checkout around the hosted card field styled to match the store, gated the wallet button behind canMakePayment(), registered the domain so Apple Pay lit up for real customers, and moved order completion onto the signed webhook with an idempotent handler and a nightly reconciliation. The gray rectangle became a checkout that looked like the store, the wallet button stopped flickering, and "paid but pending" tickets went to zero because the browser was no longer the thing deciding whether an order was done. Nothing about the fix required taking on card-data risk; it required moving each decision to the place that could actually be trusted with it.
How I build this so it earns trust and keeps it
A checkout is the last place you want surprises, so the way it gets built matters as much as the result.
- Discovery and a clear roadmap first. We agree on exactly what the checkout should look like and do, and where the current setup is costing you, before any code. You see the plan and a fixed price for the first phase.
- A fixed-scope first phase. Usually one clean card payment through the new branded field, reconciled by webhook, before wallets and extras are layered on.
- Sandbox first, then a deliberate production pass. The whole flow is proven in the sandbox, then the production-only paths like Apple Pay get a controlled live check, so nothing reaches a real customer untested.
- You own the code, the keys, and the experience. It is your Revolut account, your WooCommerce store, and your checkout, with no lock-in and no wrapper deciding how your brand looks at the most important moment.
- Direct access to the developer. When you want to adjust the field, the wallets, or the copy, that is a quick conversation with the person who built it, not a wait for a third-party plugin release.
Notice what that does to the fear that usually surrounds touching a live checkout: every step is small, proven in a safe environment first, and reversible, so confidence is built rather than gambled.
Proof, not promises
This is the kind of work we do every day. We run the WooSmiths WooCommerce studio, we ship headless commerce with full payments like the LeO-Optic store, and we build money-sensitive, rules-heavy systems like customs-invoice.com. A trustworthy checkout that keeps card data safely inside the provider's secure field while looking and behaving like your brand is exactly the sort of thing custom does better than any drop-in plugin.
If your Revolut checkout on WooCommerce feels generic, jumps around, or lets you down on wallets, you do not have to live with it, and you do not have to take on card-data risk to fix it. Tell me what your checkout is doing today and I will give you a straight read on what a safe first phase to rebuild it would look like.
FAQ
Is it safe to build a custom Revolut checkout on WooCommerce?
Yes, as long as the sensitive card entry stays inside Revolut's hosted field rather than your own inputs. Because createCardField mounts a secure iframe, the card number never touches your code or your server, so building a custom, branded checkout does not expand your PCI scope the way a self-built card form would. The danger is not customization itself; it is customizing the wrong layer. Style everything around the hosted field freely, and let the provider handle the one part, raw card data, that carries the compliance weight.
Why not just use the official Revolut WooCommerce plugin?
For a straightforward store it is a perfectly good choice and worth keeping. You outgrow it only when the checkout is a brand asset you need to control, because the plugin gives you little say over the look of the card fields, the reliability of the wallet buttons, or the layout stability of the payment step. When those cost you conversions and the plugin offers no way to fix them, replacing it with a custom integration around the same secure field is the point where building earns its keep.
How do I stop the "paid but still pending" order bug?
Confirm orders from the webhook, never from the browser. The front-end onSuccess callback should only move the interface into a pending state, because the browser can close or lose its connection before it reports anything reliable. Mark the order complete solely from Revolut's signed ORDER_COMPLETED event, verify the signature, and make the handler idempotent so a repeated delivery cannot process it twice. Add a scheduled reconciliation against the order status to catch any webhook that never arrives, and the pending-limbo class of bug disappears.
Why do Apple Pay and Google Pay buttons show up inconsistently?
Usually because the button is rendered without first checking whether the browser can actually complete a wallet payment. The reliable pattern is to call canMakePayment() and render the button only when it succeeds, destroying it otherwise, so it appears precisely when it will work. For Apple Pay specifically you also have to host Revolut's validation file and register the domain through the Merchant API, and remember that Apple Pay will not light up in the sandbox, so its final proof has to happen with a careful check in production.
Will a custom checkout lock me in to the developer who built it?
It should do the opposite. A properly built integration uses your own Revolut account and keys, lives in your WooCommerce store, and ships as code you own outright, so adjusting the card field, the wallets, or the copy is a request to whoever you choose rather than a wait for someone's plugin release. That ownership is the practical difference between a custom build and a rented plugin: the checkout, the brand moment, and the keys stay yours, which is exactly what makes future changes fast and the relationship with any developer a free choice.
Should I build this as a WooCommerce integration or go headless?
It depends on where your store already lives. If you run WooCommerce and want to keep it, wrapping the secure card field inside the existing checkout, as described here, is the direct path. If you are moving to a modern headless front end, the same payment principles apply but the plumbing runs serverless without a WooCommerce plugin at all, which is the build covered in the companion Revolut Merchant API on Vercel piece. Both keep card data in the hosted field and both confirm from the webhook; the choice is about your storefront architecture, not about payment safety.
Have a project in mind?
Let's turn it into custom software that moves your business forward.