Optional analytics on public pages. No session recording. Support works either way. Privacy details
Start here
Create a sandbox invoice, display checkout-safe fields, and fulfill only after invoice.paid.
const response = await fetch(`${process.env.BOSPAY_API_BASE}/invoices`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"BOSPay-Version": "2026-09-23",
"X-API-Key": process.env.BOSPAY_API_KEY,
"Idempotency-Key": "order-1048-attempt-1",
},
body: JSON.stringify({
"fiatAmount": 150,
"fiatCurrency": "ZAR",
"tokenSymbol": "USDC",
"chain": "sepolia",
"metadata": {
"orderId": "1048"
}
})
});
if (!response.ok) {
throw new Error(`BOSPay request failed: ${response.status}`);
}
const data = await response.json();Persist invoiceId against your order before returning anything to the customer. Return only checkout-safe fields: paymentAddress, cryptoAmount, tokenSymbol, chain, expiresAt, paymentLink.
import { createHmac, timingSafeEqual } from "crypto";
// rawBody must be the exact bytes received (Buffer), not re-serialized JSON.
export function verifyBospaySignature({ secret, timestamp, signature, rawBody }) {
if (!timestamp || !/^\d+$/.test(timestamp) || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 5 * 60) return false;
const expected = Buffer.from(
"sha256=" +
createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex"),
);
// During a secret rotation the header holds one value per valid secret.
return signature.split(",").some((candidate) => {
const provided = Buffer.from(candidate.trim());
return provided.length === expected.length && timingSafeEqual(provided, expected);
});
}After a valid invoice.paid event, GET the invoice from your backend and match organization, order, network, and asset. Mark the order paid once.