DEVELOPERS · API V1

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:

  1. Your server creates a payment request and gets back a payUrl.
  2. You send your customer to the payUrl. They choose their bank and approve the payment in their banking app.
  3. We send your webhook endpoint a signed payment_request.paid event when the bank confirms it, and redirect the customer to your returnUrl.
  4. 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

EnvironmentBase URLKeys start with
Livehttps://api.briizpay.combzp_sk_live_
Testhttps://dev.api.briizpay.combzp_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

POST/v1/payment-requests
FieldDescription
amountMinorinteger · requiredThe amount in pence, greater than zero. 4999 is £49.99.
currencystringGBP. Defaults to GBP.
memostringA description shown with the payment, up to 500 characters.
externalReferencestringYour own order, invoice or matter reference, up to 190 characters. Returned in the webhook.
customerNamestringWho is paying, up to 120 characters.
customerEmailstringThe payer's email. An address that does not look valid is dropped rather than failing the request.
returnUrlstringWhere 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

GET/v1/payment-requests/{id}

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

POST/v1/payment-requests/{id}/cancel

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

StatusMeaning
PENDINGCreated and waiting for the customer to pay.
COMPLETEDPaid. The bank has confirmed the payment.
CANCELLEDCancelled by you. It can no longer be paid.
EXPIREDNot 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"
}
StatusWhen
400A 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.
401The API key is missing, malformed or revoked.
402Your 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.
404No payment request with that ID on your account.
409Cancelling a payment that has already been paid. Refund it from your dashboard instead.

Custom fields

COMING TO THE API

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