Quadriel Quadriel ← Back to app

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).

You'll need a publishable key (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)

FieldTypeRequiredDescription
publishableKeystringRequiredYour 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) => voidRecommendedFires 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) => voidOptionalFires on a terminal (non-retryable) failure.
onCancel() => voidOptionalFires when the payer closes the overlay without paying.
checkoutOriginstringOptionalAdvanced — override the origin used to build the checkout iframe URL. Defaults to the origin the script was served from.

widget.open(params)

FieldTypeRequiredDescription
method"card" | "direct_debit"RequiredThe checkout opens straight into this method and the payer can't switch. Direct Debit never triggers a 3DS redirect.
paymentobjectRequiredWhat to charge — see Payment below.
customerobjectOptionalWho is paying — see Customer below. Omit to create an anonymous one-off customer.
autoClosenumberOptionalSeconds before the overlay closes after success. Default 3, 0 = stay open until you close it.

payment

FieldTypeRequiredDescription
type"one_time" | "recurring"RequiredA single charge, or a subscription billed on a schedule.
amountintegerRequiredAmount in the currency's minor unit (pence for GBP). 2500 = £25.00. Per-collection for recurring.
currencystringRequiredISO-4217 code, e.g. GBP.
referencestringOptionalYour own order / subscription id. Echoed back in the result and in webhooks — re-check it on your server.
descriptionstringOptionalShown to the payer inside the checkout.
scheduleobjectRecurring onlyBilling cadence for recurring payments — see Schedule below.

payment.schedule (recurring only)

FieldTypeRequiredDescription
type"UNENDING" | "LIMITED"RequiredBill forever, or stop after a set number of collections.
frequencystringRequiredWEEKLY | FORTNIGHTLY | MONTHLY | QUARTERLY | YEARLY.
frequencyDaynumberOptionalDay of the cycle to collect on (e.g. 1 = the 1st of the month).
startDatestring (YYYY-MM-DD)OptionalFirst collection date. Defaults to the next valid date.

customer

FieldTypeRequiredDescription
externalIdstringRecommendedYour own customer/user id. Looked up and created automatically on first payment, then reused — the cleanest way to get one-click repeat payments.
idstring (UUID)OptionalAn existing customer's id. Errors if not found.
namestringWhen creatingFull name or company name. Required when a new customer is created.
emailstringRecommendedWhere the receipt is sent.
phonestringOptionalStored on the customer record.
address{ line1, city, state, postal_code, country }When creatingAll 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:

EventDescription
checkout.completedA 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.paidA 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.