Checkout Widget
Take one-off payments and subscriptions — card or Direct Debit — directly on your website. The payment form opens in a secure overlay served from our domain, so card details never touch your page (SAQ-A).
pk_live_… / pk_test_…), enabled from
your dashboard (Developers → Checkout widget). It's safe to embed in page source — it can only start a
checkout, never read data or move money. For server-to-server calls, use a secret key with the
REST API instead.
1 · Install
Load the script from our domain — don't bundle or self-host it, so you always get the latest secure build:
<script src="https://merchant.quadriel.com/widget/v1/checkout.js"></script>
2 · Take a one-off payment
Construct the widget once per page load (so the result callbacks are registered even after a
3-D Secure redirect), then call open() when the payer clicks pay:
<button id="pay">Pay £25.00</button>
<script>
// Construct the widget on every page load and register the callbacks here.
// A card 3-D Secure challenge sends the payer to their bank and back, which
// destroys the open() promise — the result then arrives on these callbacks.
const widget = new Checkout.Widget({
publishableKey: "pk_live_your_key",
onSuccess: function (result) {
// result.paymentId, result.customerId, result.reference, result.amount…
window.location.href = "/thank-you";
},
onError: function (err) { alert("Payment failed: " + err.message); },
onCancel: function () { /* payer closed the widget */ }
});
document.getElementById("pay").onclick = function () {
widget.open({
method: "card",
payment: {
type: "one_time",
amount: 2500, // £25.00 in pence
currency: "GBP",
reference: "ORDER-991" // your order id
},
customer: {
externalId: "user-42", // your customer id — reused on repeat payments
name: "Jane Smith",
email: "jane@example.com",
address: { line1: "1 High Street", city: "London", state: "Greater London", postal_code: "EC1A 1BB", country: "GB" }
}
});
};
</script>
3 · Set up a subscription
Pass a recurring payment with a schedule. Works with card or Direct Debit:
widget.open({
method: "direct_debit", // subscriptions work with card or Direct Debit
payment: {
type: "recurring",
amount: 999, // £9.99 per collection
currency: "GBP",
reference: "SUB-2026-001",
schedule: {
type: "UNENDING", // or "LIMITED"
frequency: "MONTHLY", // WEEKLY | FORTNIGHTLY | MONTHLY | QUARTERLY | YEARLY
frequencyDay: 1, // collect on the 1st
startDate: "2026-07-01" // first collection (optional)
}
},
customer: { externalId: "user-42", name: "Jane Smith", email: "jane@example.com" }
});
4 · Handle the result
A card 3-D Secure challenge navigates the payer away and back, which destroys the
open() promise — so always rely on the onSuccess / onError
callbacks registered at construction, and confirm the payment server-side via a webhook before
fulfilling. Re-check reference and amount on your server.
Reference
new Checkout.Widget(options)
| Field | Type | Required | Description |
|---|---|---|---|
publishableKey | string | Required | Your pk_live_… (or pk_test_…) key. Safe to embed in page source — it can only start a checkout, never read data or move money. |
onSuccess | (result) => void | Recommended | Fires on a completed payment. Also fires when the payer returns from a 3-D Secure redirect on a fresh page load — so register it on every page load. |
onError | (err) => void | Optional | Fires on a terminal (non-retryable) failure. |
onCancel | () => void | Optional | Fires when the payer closes the overlay without paying. |
checkoutOrigin | string | Optional | Advanced — override the origin used to build the checkout iframe URL. Defaults to the origin the script was served from. |
widget.open(params)
| Field | Type | Required | Description |
|---|---|---|---|
method | "card" | "direct_debit" | Required | The checkout opens straight into this method and the payer can't switch. Direct Debit never triggers a 3DS redirect. |
payment | object | Required | What to charge — see Payment below. |
customer | object | Optional | Who is paying — see Customer below. Omit to create an anonymous one-off customer. |
autoClose | number | Optional | Seconds before the overlay closes after success. Default 3, 0 = stay open until you close it. |
payment
| Field | Type | Required | Description |
|---|---|---|---|
type | "one_time" | "recurring" | Required | A single charge, or a subscription billed on a schedule. |
amount | integer | Required | Amount in the currency's minor unit (pence for GBP). 2500 = £25.00. Per-collection for recurring. |
currency | string | Required | ISO-4217 code, e.g. GBP. |
reference | string | Optional | Your own order / subscription id. Echoed back in the result and in webhooks — re-check it on your server. |
description | string | Optional | Shown to the payer inside the checkout. |
schedule | object | Recurring only | Billing cadence for recurring payments — see Schedule below. |
payment.schedule (recurring only)
| Field | Type | Required | Description |
|---|---|---|---|
type | "UNENDING" | "LIMITED" | Required | Bill forever, or stop after a set number of collections. |
frequency | string | Required | WEEKLY | FORTNIGHTLY | MONTHLY | QUARTERLY | YEARLY. |
frequencyDay | number | Optional | Day of the cycle to collect on (e.g. 1 = the 1st of the month). |
startDate | string (YYYY-MM-DD) | Optional | First collection date. Defaults to the next valid date. |
customer
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Recommended | Your own customer/user id. Looked up and created automatically on first payment, then reused — the cleanest way to get one-click repeat payments. |
id | string (UUID) | Optional | An existing customer's id. Errors if not found. |
name | string | When creating | Full name or company name. Required when a new customer is created. |
email | string | Recommended | Where the receipt is sent. |
phone | string | Optional | Stored on the customer record. |
address | { line1, city, state, postal_code, country } | When creating | All five fields required when creating a new customer — a full address is required for UK payers. Omit when reusing an existing customer. |
Webhooks
Confirm payments on your server. We POST signed events to your endpoint (configure it in
Developers → Webhooks). Verify the x-webhook-signature HMAC over the raw body, and dedupe on
event_reference:
| Event | Description |
|---|---|
checkout.completed | A checkout finished — one-off paid, card saved, or Direct Debit mandate set up. Fields: payment_id, customer_id, customer_external_id, method, structure, amount, currency, reference. |
collection.paid | A recurring collection was taken. Fields: collection_id, payment_id, customer_id, amount, currency, reference, due_date. |
const crypto = require("crypto");
app.post("/api/payments-webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body) // the RAW bytes, not a re-stringified object
.digest("hex");
const got = req.get("x-webhook-signature") || "";
if (got.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString());
// Idempotent: skip if you've already processed event.event_reference
if (req.get("x-webhook-event") === "checkout.completed") {
fulfilOrder(event.reference, event.amount);
}
res.sendStatus(200); // 2xx acknowledges; we retry otherwise
});
Next steps
Building a server-side integration instead? See the REST API reference — customers, invoices, payments, mandates and webhook management, authenticated with a secret key.