Developer docs

Accept card payments with one API call.

One authenticated POST returns a checkout URL. You redirect the customer to it, they pay on a Stripe Checkout page and return to your store, and a signed webhook confirms the order. Then swap the test token for live.

On WordPress? Skip the code — install the WooCommerce plugin instead.

Using WooCommerce? There is a plugin — no code needed.

Same flow as the API below: upload the zip, paste one API key, and the Pay with Stripe method appears on your checkout. Everything after this card is for building the integration yourself.

How it works

Test examples

Your server creates a payment link and gets back a checkout URL; you redirect the customer to it, they pay on a Stripe Checkout page and come back to your store, and a signed webhook tells you the order is paid. The examples below use a placeholder test token; nothing is charged.

1. Create a link

One POST returns a checkoutUrl

2. Customer pays

On a Stripe Checkout page

3. Back to your store

Returned to your returnUrl

4. Order confirmed

Signed webhook completes it

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

One authenticated POST with the order total. Send an Idempotency-Key so a retry can never create a second order. The response carries the checkoutUrl you redirect the customer to.

bash
curl -X POST https://api.brokkrpay.com/api/payment-links \
  -H "Authorization: Bearer bpk_test_your_token_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "amount": 180,
    "currency": "USD",
    "reference": "order-1042",
    "returnUrl": "https://your-store.com/checkout/complete"
  }'
# amount 180 charges $180.00 — whole dollars, NOT cents.
# The response carries a checkoutUrl you redirect the customer to.

The response carries the URL you redirect the customer to:

json
{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "state": "PENDING",
  "mode": "test",
  "amount": 180,
  "currency": "USD",
  "delivery": "direct",
  "expiresAt": "2026-07-22T12:00:00.000Z",
  "checkoutUrl": "https://…/pay/aBcD…"
}
Body fieldRequirementDescription
amountRequiredOrder total in whole currency units — not cents. 180 charges $180.00. Decimals are rejected.
currencyRequiredISO currency code. Currently USD.
customerEmailOptionalNot needed — the customer enters their email on the payment page, and we record it on the order once they pay. Send it anyway to prefill 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

Redirect the customer, then bring them back

The create response carries checkoutUrl. Send the browser there; the customer pays on a Stripe Checkout page and is returned to the returnUrl you set, back inside your own store.

Redirect the top-level page

window.location.href = checkoutUrl, or an ordinary link. Not a popup — it gets blocked — and not an iframe, which breaks Apple Pay and Google Pay. The URL is opaque and single-use; treat it as the whole integration, because there is no BrokkrPay script to load.

html
<!-- Your checkout page. It never talks to BrokkrPay directly: it calls
     YOUR OWN endpoint, which runs the server-side create above. -->
<button id="pay">Pay with Stripe</button>

<script>
  document.getElementById("pay").addEventListener("click", async (e) => {
    e.target.disabled = true;                     // no double-submits

    const res = await fetch("/create-payment", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ cartId: CART_ID }),
    });
    const { checkoutUrl } = await res.json();

    // Full-page redirect. Not a popup (blocked) and not an iframe (breaks
    // Apple Pay and Google Pay).
    window.location.href = checkoutUrl;
  });
</script>

Show the retail-partner disclosure before the customer commits

You own the page the customer clicks from, so the disclosure about how the purchase appears on their statement has to be rendered next to your pay button. Skipping it is what turns a legitimate charge into a dispute.

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.

Stripe Checkout 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 for 24 hours, so the customer can be sent to it again 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.

Verify the signature on every delivery

Every webhook carries a Brokkr-Signature header: an HMAC over the raw request body, keyed by the whsec_… secret shown next to your endpoint in the dashboard. Verify it before parsing the JSON — most frameworks consume the body during parsing and leave you nothing to verify against. Secrets are per environment, so a test delivery can never validate against your live secret. Also check the mode field (test or live) as a second line of defence, and 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