Build pay by bank into your product
Create a payment from your server, send your customer to our hosted pay page, and receive a signed webhook when their bank confirms it. One API key and three endpoints.
Overview
A payment request is one amount a customer pays you. The flow is the same for a shop checkout, an invoice or a platform:
- Your server creates a payment request and gets back a
payUrl. - You send your customer to the
payUrl. They choose their bank and approve the payment in their banking app. - We send your webhook endpoint a signed
payment_request.paidevent when the bank confirms it, and redirect the customer to yourreturnUrl. - You mark the order paid from the webhook, not from the customer returning.
Money goes straight to your business bank account. Requests and responses are JSON, amounts are integers in pence, and times are ISO 8601 in UTC.
Authentication
Every request carries an API key in the Authorization header. Create keys in your dashboard under Integrations, Your own system. A key belongs to your business rather than a person, so it keeps working when staff change.
Authorization: Bearer bzp_sk_live_...- A key is shown once, when you create it. We store only a hash and cannot show it again. If you lose one, revoke it and create another.
- Keep keys on your server. Never put one in a browser, an app bundle or a public repository.
- Revoking a key stops it immediately. The dashboard shows when each key was last used, so you can see which one is live before you revoke.
- A key only ever acts for the business it belongs to.
Environments
| Environment | Base URL | Keys start with |
|---|---|---|
| Live | https://api.briizpay.com | bzp_sk_live_ |
| Test | https://dev.api.briizpay.com | bzp_sk_test_ |
Test keys only work against the test environment and live keys only against live, so a test key pasted into a live shop fails straight away. To build against the test environment, ask us for a test account.
Create a payment
| Field | Description |
|---|---|
| amountMinorinteger · required | The amount in pence, greater than zero. 4999 is £49.99. |
| currencystring | GBP. Defaults to GBP. |
| memostring | A description shown with the payment, up to 500 characters. |
| externalReferencestring | Your own order, invoice or matter reference, up to 190 characters. Returned in the webhook. |
| customerNamestring | Who is paying, up to 120 characters. |
| customerEmailstring | The payer's email. An address that does not look valid is dropped rather than failing the request. |
| returnUrlstring | Where to send the customer after they approve. Must be an absolute https URL of 2,048 characters or fewer. |
Request
curl https://api.briizpay.com/v1/payment-requests \
-H "Authorization: Bearer bzp_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amountMinor": 4999,
"currency": "GBP",
"memo": "Order #1042",
"externalReference": "1042",
"customerName": "Sam Carter",
"customerEmail": "sam@example.com",
"returnUrl": "https://yourshop.co.uk/checkout/order-received/1042"
}'Response
HTTP/1.1 201 Created
{
"data": {
"id": "7b1f2c1e-5d3a-4f0e-9c1a-2e8b6d4f1a90",
"status": "PENDING",
"amountMinor": 4999,
"currency": "GBP",
"memo": "Order #1042",
"externalReference": "1042",
"customerName": "Sam Carter",
"customerEmail": "sam@example.com",
"returnUrl": "https://yourshop.co.uk/checkout/order-received/1042",
"payUrl": "https://app.briizpay.com/pay/pl/7b1f2c1e-5d3a-4f0e-9c1a-2e8b6d4f1a90",
"expiresAt": "2026-09-16T10:30:00.000Z",
"createdAt": "2026-09-15T10:30:00.000Z"
}
}- Send your customer to payUrl exactly as returned, rather than building the address yourself. It stops working at expiresAt, which follows your plan's payment link validity.
- The amount is fixed. Your customer cannot change it, and tips are off for requests created through the API.
- Each request can be paid once.
Get a payment
Returns the payment request in the same shape as create. Use it to reconcile when a webhook has not arrived: it is the authority, and a customer arriving at your returnUrl is not.
Request
curl https://api.briizpay.com/v1/payment-requests/7b1f2c1e-5d3a-4f0e-9c1a-2e8b6d4f1a90 \
-H "Authorization: Bearer bzp_sk_live_..."Cancel a payment
Stops a payment request being payable, for example when an order is cancelled or a checkout times out. Returns the request with status CANCELLED.
Request
curl -X POST https://api.briizpay.com/v1/payment-requests/7b1f2c1e-5d3a-4f0e-9c1a-2e8b6d4f1a90/cancel \
-H "Authorization: Bearer bzp_sk_live_..."- Cancelling twice is not an error, so it is safe to retry after a timeout.
- A request that has already been paid cannot be cancelled and returns 409. Refund it from your dashboard.
Payment statuses
| Status | Meaning |
|---|---|
| PENDING | Created and waiting for the customer to pay. |
| COMPLETED | Paid. The bank has confirmed the payment. |
| CANCELLED | Cancelled by you. It can no longer be paid. |
| EXPIRED | Not paid before expiresAt. It can no longer be paid. |
Webhooks
Add a webhook endpoint in your dashboard under Integrations, Your own system. You get a signing secret starting whsec_, shown once. When a payment is paid we send one payment_request.paid event to every endpoint on your account.
Delivery
POST https://yourshop.co.uk/webhooks/briizpay
Content-Type: application/json
User-Agent: BriizPay-Webhooks/1
Briizpay-Signature: t=1789468200,v1=5f0c8e...
{
"id": "0e6f3b8a-2c4d-4b1e-8f7a-9d2c1b3e4f5a",
"type": "payment_request.paid",
"createdAt": "2026-09-15T10:31:12.000Z",
"data": {
"paymentRequestId": "7b1f2c1e-5d3a-4f0e-9c1a-2e8b6d4f1a90",
"transactionId": "c4a1d2e3-6b7f-4a8c-9d0e-1f2a3b4c5d6e",
"status": "COMPLETED",
"amountMinor": 4999,
"currency": "GBP",
"externalReference": "1042",
"customerName": "Sam Carter",
"customerEmail": "sam@example.com",
"paidAt": "2026-09-15T10:31:12.000Z"
}
}- Respond with any 2xx status within 8 seconds. Anything else, including a redirect, counts as a failure.
- A failed delivery is retried after 1, 5, 30, 120 and 360 minutes, six attempts in all.
- Deliveries can arrive more than once. The top-level id is the same on every retry of an event, so record it and ignore one you have already handled.
- Check amountMinor and currency against your order before marking it paid.
- Endpoints must be public https URLs. We do not follow redirects, so register the final URL.
- Every delivery and your endpoint's response is in the dashboard delivery log.
Verify signatures
The Briizpay-Signature header is t=<unix seconds>,v1=<hex>. v1 is an HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your signing secret. Reject a delivery whose signature does not match or whose timestamp is more than five minutes old.
Node.js
import crypto from "node:crypto";
// rawBody must be the request body exactly as received, before JSON parsing.
function verifyBriizpaySignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(",").map((piece) => piece.trim().split("=", 2)),
);
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}PHP
<?php
// $payload must be the raw body: file_get_contents('php://input')
function briizpay_verify_signature($payload, $header, $secret, $tolerance = 300) {
$parts = [];
foreach (explode(',', $header) as $piece) {
$pair = explode('=', trim($piece), 2);
if (count($pair) === 2) {
$parts[$pair[0]] = $pair[1];
}
}
if (empty($parts['t']) || empty($parts['v1'])) {
return false;
}
if (abs(time() - (int) $parts['t']) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $payload, $secret);
return hash_equals($expected, $parts['v1']);
}- Verify against the raw body. Parsing the JSON and serialising it again changes the bytes and the signature will not match.
- Compare with a constant-time function, as both examples do.
Errors
Errors return a JSON body with a human-readable msg, and a machine-readable code where there is one.
HTTP/1.1 402 Payment Required
{
"msg": "Your plan payment is due. Pay now to keep accepting payments.",
"code": "PERIOD_DUE"
}| Status | When |
|---|---|
| 400 | A field is invalid, for example amountMinor is not a whole number or returnUrl is not https. code AMOUNT_OVER_LIMIT when the amount is above your plan's maximum payment. |
| 401 | The API key is missing, malformed or revoked. |
| 402 | Your account cannot take payments right now. code is one of NO_SUBSCRIPTION, INACTIVE, PERIOD_DUE, TOP_UP_REQUIRED, SUSPENDED or CANCELLED, and msg says what to do. |
| 404 | No payment request with that ID on your account. |
| 409 | Cancelling a payment that has already been paid. Refund it from your dashboard instead. |
Custom fields
COMING TO THE APICustom fields ask your customer up to three questions before they pay, such as a car registration, a number of guests or a job reference, and save the answers with the payment. They are available today on payment links and QR codes you create in the dashboard.
Setting fields when you create a payment through the API, and receiving the answers in the webhook, is coming next. If you need it for your integration, tell us.
Need a hand?
Tell us what you are building and we will help you get to your first live payment. Selling online with WooCommerce? The plugin does all of this for you.