API guide
Everything you need to get from a fresh key to a first successful call, then to confirmed payments over webhooks. The API reference has the full endpoint list and schemas; this page is the narrative path through them.
sk_test_… keys run in test mode, sk_live_…
keys move real money. Create either from your dashboard.
1 · Get an API key
Secret keys authenticate your server-side calls. In the app, go to
Developers → API keys (admins only) and Create a key. The full
sk_test_… / sk_live_… secret is shown once at creation — store it
somewhere safe; we only keep a prefix. Rotate or revoke a key from the same screen at any time.
Send it on every request as a bearer token:
Authorization: Bearer sk_test_your_key
Never expose a secret key in front-end code. (For browser checkout, use a publishable key with the Checkout Widget instead.)
2 · Quickstart — your first invoice
Create a customer, draft an invoice, then finalize it to issue a hosted payment link. Amounts
are always in the currency's minor unit (pence for GBP — 10000 = £100.00).
Pass an Idempotency-Key on writes so a retried request never double-creates.
# 1. Point at the API and your test key
export QUADRIEL_API="https://rifayrirolwkfilokjis.supabase.co/functions/v1/api/v1"
export QUADRIEL_KEY="sk_test_your_key" # from Developers → API keys
# 2. Create a customer → { "customer_id": "...", ... }
curl -s "$QUADRIEL_API/customers" \
-H "Authorization: Bearer $QUADRIEL_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "type": "company", "family_name": "Acme Ltd", "email": "ap@acme.example" }'
# 3. Create a draft invoice for that customer → { "id": "...", "total": 12000, "line_items": [...] }
curl -s "$QUADRIEL_API/invoices" \
-H "Authorization: Bearer $QUADRIEL_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customer_id": "CUSTOMER_ID_FROM_STEP_2",
"collection_method": "send_invoice",
"payment_method": "card",
"currency": "GBP",
"line_items": [
{ "description": "Consulting", "quantity": 1, "unit_amount": 10000, "vat_rate": 20 }
]
}'
# 4. Finalize it → issues a number + a hosted payment link token
curl -s "$QUADRIEL_API/invoices/INVOICE_ID_FROM_STEP_3/finalize" \
-X POST -H "Authorization: Bearer $QUADRIEL_KEY"
# => { "id": "...", "status": "open", "number": "INV-0001",
# "payment_link_id": "...", "hosted_payment_url": "https://.../pay/<token>" }
# Send the payer to hosted_payment_url — it is ready to share as-is.
That's a complete round trip against the live API. Swap send_invoice for
charge_automatically to collect without sending a link, and see the
reference for payments, mandates and payment links.
3 · Errors, idempotency & rate limits
Errors come back as { "error": { "code": "…", "message": "…" } } with a
matching HTTP status (400/401/403/404/409/422/429). Retry 429 and
5xx with backoff; on 2xx you're done. Reuse the same
Idempotency-Key when you retry a write and you get the original result back, not a
duplicate. The reference intro covers the exact money, pagination and rate-limit rules.
4 · Webhooks
Confirm outcomes server-side — don't rely on a browser redirect. Add your endpoint in Developers → Webhooks; we POST each event to it as JSON. Every request carries:
x-webhook-event— the event type (below)x-webhook-signature— HMAC-SHA256, lowercase hex, computed over<timestamp>.<raw body>with your endpoint's signing secretx-webhook-timestamp— unix seconds at delivery; reject if more than 5 minutes from now (replay protection). Each retry is re-signed with a fresh timestamp
The body is the event payload plus a version field. Verify the signature over the
raw bytes before trusting anything, and reply 2xx to acknowledge (we retry
with backoff otherwise):
const crypto = require("crypto");
// Use the RAW request bytes — not a re-serialized object.
app.post("/quadriel/webhook", express.raw({ type: "application/json" }), (req, res) => {
// Replay protection: reject stale timestamps, then verify the signature
// over "<timestamp>.<raw body>". Retries carry a fresh timestamp+signature;
// dedupe on event_reference in the payload.
const timestamp = req.get("x-webhook-timestamp") || "";
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(401);
}
const expected = crypto
.createHmac("sha256", process.env.QUADRIEL_WEBHOOK_SECRET) // your endpoint's signing secret
.update(timestamp + ".") // signed timestamp prefix
.update(req.body) // then the raw Buffer
.digest("hex"); // lowercase 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());
switch (req.get("x-webhook-event")) {
case "invoice.paid": /* mark the invoice paid in your system */ break;
case "checkout.completed": /* fulfil the order */ break;
case "collection.paid": /* record the recurring collection */ break;
}
res.sendStatus(200); // any 2xx acknowledges; non-2xx is retried with backoff
});
Events
| Event | When it fires |
|---|---|
checkout.completed | A hosted-checkout / widget flow finished — one-off paid, card saved, or DD mandate set up. |
collection.paid | A scheduled recurring collection was taken. |
invoice.finalized | An invoice was issued (number assigned, payment artefacts created). |
invoice.paid | An invoice was paid in full. |
invoice.payment_failed | A collection against an invoice failed. |
invoice.voided | An invoice was voided before payment. |
invoice.uncollectible | An invoice was written off as uncollectible. |
terminal.order_requested | A card-present terminal order was requested. |
You can see real, recent payloads for your endpoint — and replay them — under Developers → Webhooks → Deliveries. Two common shapes:
checkout.completed
{
"version": 1,
"payment_id": "…",
"customer_id": "…",
"customer_external_id": "user-42",
"method": "card",
"structure": "one_time",
"amount": 2500,
"currency": "GBP",
"reference": "ORDER-991"
}
collection.paid
{
"version": 1,
"collection_id": "…",
"payment_id": "…",
"customer_id": "…",
"amount": 999,
"currency": "GBP",
"reference": "SUB-2026-001",
"due_date": "2026-07-01"
}
hosted_token. Every invoice.* event
includes a hosted_token — the bearer for that invoice's hosted payment page
(/pay/<token>). Anyone holding it can view the payer's details (name, email,
phone, address, tax IDs) and the invoice, and can start a card payment, until the link expires
or is cancelled. It is the same link we email the payer, so it is not a new exposure —
but it makes your endpoint URL, and any log that captures request bodies, as sensitive as the
pay link itself. Treat it accordingly, and don't forward the token anywhere you wouldn't send
the pay link.
5 · Changelog
| Date | Change |
|---|---|
| 2026-09-20 | Documented that invoice.* webhook events include a hosted_token (the payer's hosted-payment-page link) and should be handled as sensitive. |
| 2026-09-18 | Public REST API v1 documented: customers, invoices, payments, payment links, mandates, transactions, activity and webhook management. Signed webhooks (HMAC-SHA256). Embeddable Checkout Widget. |
Building a browser checkout instead? See the Checkout Widget guide.