Open a session
From the server, call POST /v1/checkout_sessions with a key carrying payments:write. The response has id, url, payment_link, amount, currency and mode. Save a stable idempotency key before making the request, then save the returned session ID and URL against the order before redirecting the buyer. The secret key stays on the server, never in browser code.
curl -s -X POST https://api.bolrachpay.com/v1/checkout_sessions \
-H "Authorization: Bearer $BOLRACH_KEY" \
-H "Idempotency-Key: order-1042-checkout-1" \
-H "Content-Type: application/json" \
-d '{
"description": "Order 1042",
"amount": "250000",
"currency": "NGN",
"success_url": "https://merchant.example/orders/1042/payment",
"cancel_url": "https://merchant.example/orders/1042/payment"
}'{
"object": "checkout_session",
"id": "cs_...",
"mode": "test",
"url": "https://pay.bolrach.com/t/s/...",
"status": "open",
"amount": "250000",
"currency": "NGN"
}success_url and cancel_url are optional, absolute HTTPS URLs on the merchant site. Use a saved order page for both so the buyer can recover the same order after payment, cancellation or a dropped connection. These are the public API's snake_case field names, and they also work on POST /v1/payment_links. A direct payment intent uses return_url instead (see the quickstart).
The same thing in Node, with orders kept in the shop's own database:
// Express. The key stays on the server; the order row is yours.
app.post("/orders/:id/pay", async (req, res) => {
const order = await orders.get(req.params.id);
const idem = order.checkoutKey ?? (await orders.saveCheckoutKey(order.id, `order-${order.id}-checkout-1`));
const r = await fetch("https://api.bolrachpay.com/v1/checkout_sessions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BOLRACH_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idem },
body: JSON.stringify({
description: `Order ${order.id}`, amount: String(order.totalMinor), currency: order.currency,
success_url: `https://shop.example/orders/${order.id}`, cancel_url: `https://shop.example/orders/${order.id}`,
}),
});
const session = await r.json();
await orders.saveCheckout(order.id, { sessionId: session.id, url: session.url, mode: session.mode });
res.redirect(303, session.url);
});Confirm before you ship
A confirmed success returns the buyer to the stored success URL. Pay may add payment, status and a receipt reference to it. Treat every browser parameter as untrusted. A return link, a success screen or a receipt reference alone does not authorize fulfilment. Read the saved session from the server using a key with payments:read, or process a verified signed webhook and check its actual payment against the order.
# On either return page, your SERVER reads the session id it saved with the order:
curl -s https://api.bolrachpay.com/v1/checkout_sessions/$CHECKOUT_SESSION_ID \
-H "Authorization: Bearer $BOLRACH_KEY"
# paid=true and status=completed say money was collected at some point, not that it is still there.
# Read the payment it names, also from your server, before the first fulfilment:
curl -s https://api.bolrachpay.com/v1/payment_intents/$PAYMENT_INTENT_ID \
-H "Authorization: Bearer $BOLRACH_KEY"
# Require status=succeeded today, and the same order, amount, currency and mode you saved.For a hosted session, wait for the server record's status to be completed and paid to be true. That read also catches up on any provider update we missed, and it returns payment_intent for the payment that was collected. A completed session can remain paid after the payment is refunded, partially refunded or disputed. So before the first fulfilment, call GET /v1/payment_intents/:id from your server with that payment_intent and require its current status to be succeeded, or apply a written refund and dispute policy. Do not ship merely because the checkout says completed. Check the exact whole-number minor-unit amount, currency and mode against the order as saved, and check the shop's own order state for an existing fulfilment, cancellation or refund. Make fulfilment idempotent so a webhook and the buyer's return cannot fulfil the same order twice.
Pending, interrupted or cancelled
An open session or an unavailable confirmation means the result is still unknown. Show a waiting state and a Check status action; do not mark it paid or failed, promise a refund or begin another payment. A declined or cancelled provider attempt can return to Pay's recovery screen. Its merchant-return control uses your stored cancel URL, but choosing that control does not itself cancel or refund the payment. Your saved order page must read the server result again.
Reopen the same saved checkout URL to resume its original expiry window. A completed session offers a merchant-return link instead of charging again. An expired or cancelled session offers a return to your saved order when a cancel URL was set. If payment may have arrived while the browser was away, reconcile the saved session before creating a new one.
Retrying a create
Idempotency-Key is optional here for backward compatibility, and you should still send it. Use 1 to 120 characters: letters, digits and : . _ -. Save one key per order attempt and reuse that exact key and normalized request payload if the response is lost. Pay keeps the link, the session and the replay record together for your merchant, mode and this endpoint, so a replay returns the same session without extending its expiry. A changed payload with the same key returns 409; look at the original attempt instead of quietly generating a new key. Without the header, every POST creates a new session and has no retry guarantee.
Hosted sessions do not accept metadata or client_reference_id. Keep your order-to-session mapping in your own database. A new order attempt gets a new key only after you have looked at the original payment; a new key is not a way around an uncertain, expired or conflicted attempt.
Test and live
The key chooses the mode: a test key creates a /t/s/ checkout and a live key an /l/s/ one. Store the returned mode with the order and verify in that same mode. Sending a mode in the request body cannot turn a test key into a live one. A live payment also needs the business's live setup and a processor for the currency.
What the buyer sees
The checkout leads with your brand and what is being bought. One small line under it names the business that is legally taking the money, as registered, because that is the name the buyer's card statement will show. Receipts carry the same two names.