Developer docs

Accept card payments with one API call.

Your customer enters their email at checkout, BrokkrPay emails them a secure payment link on your store's behalf, they pay on a hosted page and return to your store. Get notified by webhook, then swap the test token for live.

How it works

Test examples

Your customer enters their email at checkout, BrokkrPay emails them a secure payment link on your store's behalf, they pay on a hosted page and return to your store. The examples below use a placeholder test token; nothing is charged.

1. Email a payment link

We email it to your customer

2. Customer pays

On the hosted payment page

3. Back to your store

Returned to your returnUrl

4. Order confirmed

Webhook completes the checkout

1

Get your API token

Each site has its own test and live token. Sign in to your BrokkrPay dashboard to copy your real token — the placeholder below shows the format.

Test

The token is a server-side secret — it creates real payment links. Keep it out of browser code and version control.

2

Create a payment link from your server

Collect the customer's email in your checkout, then make one authenticated POST with customerEmail — BrokkrPay creates the hosted payment page and emails the customer the link.

bash
curl -X POST https://api.brokkrpay.com/api/payment-links \
  -H "Authorization: Bearer bpk_test_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 180,
    "currency": "USD",
    "customerEmail": "customer@example.com",
    "reference": "order-1042",
    "returnUrl": "https://your-store.com/checkout/complete"
  }'
# amount 180 charges $180.00 — whole dollars, NOT cents.

The response is ready to hand to your frontend:

json
{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "state": "PENDING",
  "mode": "test",
  "amount": 180,
  "currency": "USD",
  "expiresAt": "2026-07-22T12:00:00.000Z",
  "customerEmailSent": true
}
Body fieldRequirementDescription
amountRequiredOrder total in whole currency units — not cents. 180 charges $180.00. Decimals are rejected.
currencyRequiredISO currency code. Currently USD.
customerEmailRequiredWhere BrokkrPay emails the payment link. The link is delivered by email and nowhere else, so without this the customer has no way to reach the payment page.
returnUrlRecommendedYour checkout page. Once the customer finishes on the hosted payment page they are sent here with ?orderId=…&status=success (or status=cancelled), so they land back in your store. Stored exactly as you write it — hash-routed single-page apps need a path-based URL, see step 3.
referenceRecommendedYour own order or cart ID, up to 200 characters. Echoed back as reference in every webhook for this order. Not required to be unique — we never deduplicate on it, and an order created without one gets reference: null.

amount is whole currency units, not cents

Most processors take minor units. We do not. 180 charges $180.00. If you send 18000 expecting $180.00 you will charge $18,000 — that is a valid amount and we will accept it.

Which amounts are accepted

Your customer pays on a hosted page whose line items come from our storefront’s product catalog — you never register products with BrokkrPay. So the total has to be reachable as a sum of the prices in that catalog (repeats allowed). It is dense at ordinary retail amounts, so in practice this only bites on unusual totals; when it does, creation fails with a 400 naming the amount, before the customer ever sees a payment page. Round to a nearby amount, or ask us to add the price point.

3

Bring the customer back to your checkout

BrokkrPay emails the link to customerEmail — that is the only way it is delivered. When the customer finishes paying, they are returned to the returnUrl you set, back inside your own store.

The payment link is email-only

The API response never contains the checkout URL, so there is nothing to surface in your UI or open in a tab — customers reach the payment page from the email BrokkrPay sends them, which carries the disclosure about how the purchase appears on their statement.

If the email doesn’t arrive

Because the link is email-only, a bounced or mistyped address strands the customer. Two things to handle: customerEmailSent: false in the create response means the send failed outright — tell the customer at once rather than showing “check your email”. And a send that succeeded can still land in spam. Either way, resend:

bash
curl -X POST https://api.brokkrpay.com/api/payment-links/<orderId>/resend \
  -H "Authorization: Bearer bpk_test_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{ "customerEmail": "corrected@example.com" }'
# customerEmail is optional — omit it to resend to the original address.

Works while the order is PENDING and the link is unexpired; pass a corrected customerEmail to change the recipient. Once the link has expired, create a new payment link instead. Your support team can do the same from the Orders table in your dashboard, without touching code.

The return trip goes through our storefront and out again. Stripe hands the customer to the storefront, which immediately forwards them to your returnUrl — they only pass through it, they never browse it.

Hosted payment pageOur storefront (pass-through)your returnUrl

Both outcomes come back to the same URL, distinguished by the status param:

Paid…/checkout/complete?orderId=<uuid>&status=success
Cancelled…/checkout/complete?orderId=<uuid>&status=cancelled
javascript
// Your checkout page, where the customer comes back to.
app.get("/checkout/complete", async (req, res) => {
  const { orderId, status } = req.query;
  // status is "success" or "cancelled" — a hint for what to render, never
  // proof of payment. Read the order state you stored from the webhook.
  const order = await db.orders.findByBrokkrpayId(orderId);

  if (order.state === "SUCCESS") {
    return res.render("checkout-complete", { order });
  }
  // Cancelled, or the webhook hasn't landed yet — show a pending/retry state.
  res.render("checkout-pending", { order, status });
});

Treat status as a display hint only — the webhook in the next step is what tells you an order is paid. A cancelled payment leaves the order PENDING and the link still valid, so the customer can reopen it from their email and finish. Links expire after 24 hours, at which point the order becomes CANCELLED.

Single-page apps: use a path, not a hash

We append ?orderId=…&status=… to your returnUrl exactly as you wrote it. If your URL contains a #, the query string ends up inside the fragment where location.search can’t read it. Put your own reference in the path and redirect server-side:

javascript
// Hash-routed SPA? We append ?orderId=…&status=… to your returnUrl
// verbatim. On a hash URL that lands INSIDE the fragment, where
// location.search cannot see it:
//
//   https://store.com/#/checkout/done  →  https://store.com/#/checkout/done?orderId=…
//                                                              ^ unreadable
//
// Use a path-based returnUrl and redirect server-side instead:
returnUrl = "https://your-store.com/checkout/complete/" + cartId;

app.get("/checkout/complete/:cartId", (req, res) => {
  const { orderId, status } = req.query;   // readable — plain path, real query
  res.redirect(`/?status=${status}#/order/${req.params.cartId}`);
});
4

Read an order's state directly

A real endpoint, not just a fallback: your status page needs it whenever a webhook is delayed, and support needs it to answer 'did this actually go through?'.

GET https://api.brokkrpay.com/api/orders/<orderId> — authenticated with the same site token as everything else. The token scopes the lookup, so it only ever returns your own orders.

bash
curl https://api.brokkrpay.com/api/orders/<orderId> \
  -H "Authorization: Bearer bpk_test_your_token_here"

Responds with:

json
{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "state": "SUCCESS",
  "amount": 180,
  "currency": "USD",
  "storeId": "your-site-id",
  "clientReference": "order-1042",
  "paymentMethod": "link",
  "lineItems": [ … ],
  "sessionExpiresAt": "2026-07-22T12:00:00.000Z",
  "statusDetail": null,
  "createdAt": "2026-07-21T12:00:00.000Z",
  "updatedAt": "2026-07-21T12:04:11.000Z"
}

Keep the orderId ↔ reference mapping

This endpoint is keyed by our orderId, while your own records are keyed by your reference. Store the two against each other when the create call returns — without it you can receive a webhook you cannot look up, or hold a reference you cannot query.

Never call this from the browser — the token is a server-side secret. Proxy it from your own server, and poll no more than once every few seconds per order. It is a supplement to the webhook, not a replacement: the webhook is still what tells you to fulfill.

5

Set up your webhook

BrokkrPay POSTs to your server on every order state change — this is how you reliably fulfill orders. Save your endpoint URLs per site in your BrokkrPay dashboard.

Saved per site, and per environment: you configure a separate test and live endpoint. Test orders only ever reach the test endpoint and live orders only ever reach the live one, so a test payment can never trigger a real fulfillment. Point both at the same URL if you would rather branch on the mode field yourself. Your dashboard has a Send test event button and a delivery log showing the status each attempt returned.

Every state change arrives as a JSON POST:

json
{
  "type": "order.state_changed",
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "storeId": "your-site-id",
  "state": "SUCCESS",
  "previousState": "PENDING",
  "amount": 180,
  "currency": "USD",
  "mode": "test",
  "reference": "order-1042",
  "timestamp": "2026-07-05T12:00:00.000Z"
}

Handle it and acknowledge quickly (within 5 seconds):

javascript
app.post("/webhook", express.json(), async (req, res) => {
  const event = req.body; // order.state_changed

  // Acknowledge FIRST, then do the work. We give you 5 seconds and do not
  // retry, so a slow fulfilment must not hold the response open.
  res.sendStatus(200);

  if (event.state !== "SUCCESS") return;

  // Reconcile on orderId — it is always present. "reference" is only there if
  // you sent one when creating the link, so treat it as a convenience, not a key.
  const order =
    (await db.orders.findByBrokkrpayId(event.orderId)) ??
    (event.reference ? await db.orders.findByReference(event.reference) : null);

  // Unknown order? The create call may not have returned to you yet. Buffer and
  // retry briefly rather than dropping the event — see "Ordering" below.
  if (!order) return bufferForRetry(event);

  fulfillOrder(order, event.amount);
});

Order states

PENDINGOrder created — the payment link is live, awaiting the customer.
PROCESSINGCharge in progress (e.g. a delayed payment method is confirming).
SUCCESSPayment captured — fulfill the order.
FAILEDPayment did not go through.
CANCELLEDOrder was cancelled, or the payment link expired unpaid (24h).

Ordering: a webhook can arrive before the create call returns

The first PENDING event is emitted as soon as the order exists — which is before POST /api/payment-links has finished responding to you. If your handler persists the order only after that response, there is a window in which an arriving event refers to an order you have never heard of. Usually that is the harmless PENDING, but a fast payer or a slow response can make it SUCCESS. Persist your order before calling us, and buffer events for unknown orderIds for a few seconds rather than dropping them.

Retries and acknowledgement

Reply within 5 seconds. We treat any 2xx as delivered and do not currently retry — a non-2xx, a timeout or a connection failure is recorded in your dashboard’s delivery log and not sent again. So acknowledge first and do your fulfillment work after, and use the delivery log (plus GET /api/orders/:orderId) to catch anything your endpoint missed while it was down.

Security: webhooks are unsigned, so use a long, unguessable URL. Test and live orders go to separate endpoints, but still check the mode field (test or live) as a second line of defence. Treat the webhook — never the customer returning to your site — as the signal to fulfill.

6

Test, then go live

Run an end-to-end payment in test mode. Going live needs BrokkrPay approval.

Test card

4242 4242 4242 4242

Any future expiry, any CVC. Test orders never charge a real card.

Going live

Once BrokkrPay approves your site for live payments, switch to Live and swap your bpk_test_… token for the bpk_live_… token on the same site. Everything else stays identical.

Prefer to explore first? Your BrokkrPay dashboard has a demo playground that creates a real payment link with live webhook delivery, no code required.

Same-day onboarding for high-risk merchants.

Tell us your vertical and your monthly volume. Approved operators are live on their own Stripe account within a day.

Sign up
No setup fee Your Stripe, your funds Cancel anytime