- Checkout
- Get started
Confirmation & push
Handle the confirmation redirect and the push callback. Read the order, acknowledge it idempotently from both routes, fit it to your fields and sweep for a lost push.
On this page you'll
- Build a confirmation route and a push handler that do the same work, idempotently
- Compare order status case-insensitively and read the order instead of trusting the query string
- Acknowledge from both routes, and sweep Order Management for the rare order whose push never arrives
Answering the callbacks left you holding a purchase
Kustom is allowed to complete. From here, two independent things tell you it actually did:
a browser redirect and a server-to-server push, and either one can arrive alone. This page
builds the two routes that record the order from whichever one shows up, why both have to
tolerate running twice for the same order_id and the sweep for the rare order neither one
ever reaches.
- The customer presses Pay orderThe customer
In the checkout, once they have picked a payment method. Nothing for you to build here.
- The payment goes throughKustom
Kustom authorizes the payment with the customer's payment method, and the order's status becomes checkout_complete. The order is read-only from here.
- Kustom redirects the browser to your confirmation pageKustom
A GET to merchant_urls.confirmation, with the order id in the query string. Your route reads the order, records it, acknowledges it and shows the receipt.
If the customer closes the tab before the redirect lands, this never arrives.
- About two minutes later, the pushKustom
A POST to merchant_urls.push, server-to-server, whether or not the redirect arrived. Your handler does the same recording, so it has to be harmless the second time.
Kustom retries it at 5, 15, 30 and 60 minutes, then every four hours, until you acknowledge the order or 48 hours pass. It can also arrive before the redirect.
The push in full →
Why neither path is optional
Section titled “Why neither path is optional”| Path | How it arrives | When |
|---|---|---|
| Browser redirect | GET to merchant_urls.confirmation, {checkout.order.id} substituted in | Right after the customer presses Pay order |
| Server-to-server push | POST to merchant_urls.push | Usually about two minutes later, whether or not the browser got there. It can also arrive first |
A customer who closes the tab before the redirect lands isn’t an edge case — a banking app that switches away for the payment step and never switches back, a browser the OS kills, a connection that drops, all leave the push as the only notice you get. The common mistake is triggering fulfillment only from the confirmation page: it passes every manual test and silently drops exactly the orders you were paid for, on the day a customer’s tab doesn’t make it back. Treat the push as the other half of confirmation, not a fallback you can skip.
Both routes must be safe to run twice for the same order_id, because they will be. The
push retries on its own schedule until you acknowledge it, and a customer can reload the
confirmation page after it already ran once. Recording the same order a second time has to be
a no-op, not a duplicate.
Build both routes
Section titled “Build both routes”Both URLs carry the order id in a query parameter you name when you register them —
order_id here:
"confirmation": "https://shop.example/confirmation?order_id={checkout.order.id}","push": "https://shop.example/push?order_id={checkout.order.id}"Each route makes sure the purchase completed in its own way, then hands the id to one function that does the recording, so “idempotent” is a property of the code rather than something each handler has to remember separately.
// merchant_urls.confirmation -- the browser redirect. May never arrive.app.get('/confirmation', async (req, res) => { const order = await readFromKustom(`/checkout/v3/orders/${req.query.order_id}`); // The API returns this in lowercase; the OpenAPI document's example is uppercase. if (String(order?.status).toLowerCase() !== 'checkout_complete') { return res.redirect('/checkout'); // not paid: no receipt, nothing recorded } await recordAndAcknowledge(order.order_id); res.send(`<div id="confirmation">${order.html_snippet}</div>`);});
// merchant_urls.push -- server-to-server, usually about two minutes later. Independent of the redirect.app.post('/push', async (req, res) => { res.sendStatus(200); // answer first, then do the work // Order Management holds an order only once the purchase has completed. const order = await readFromKustom(`/ordermanagement/v1/orders/${req.query.order_id}`); if (order) await recordAndAcknowledge(order.order_id);});
// A GET to Kustom. null for a 404, so "no such order" is an answer rather than an exception.async function readFromKustom(path) { const r = await fetch(`${KUSTOM}${path}`, { headers: { Authorization: auth } }); if (r.status === 404) return null; if (!r.ok) throw new Error(`${path} answered ${r.status}`); return r.json();}
// Safe to call from any route, any number of times, for the same order.async function recordAndAcknowledge(orderId) { await db.orders.upsert({ id: orderId, confirmed: true }); // keyed on order_id await fetch(`${KUSTOM}/ordermanagement/v1/orders/${orderId}/acknowledge`, { method: 'POST', headers: { Authorization: auth }, });}The two routes read the order from different APIs on purpose. The confirmation route needs the checkout order, because that’s what carries the receipt. The push handler doesn’t, and Order Management keeps the order long after the checkout order is gone. Neither route checks whether the acknowledge went through: an order that stays unacknowledged gets its push sent again, and the next pass acknowledges it.
The confirmation route renders the same html_snippet field the order already carries, now
showing a receipt instead of a checkout — copied into the page the same way the checkout
snippet is, scripts re-executed rather than dropped by innerHTML. Put it on the
page covers that mechanism once, and it applies
here unchanged.
The confirmation snippet is available for 2 hours. The confirmation html_snippet loads for two hours after the order is completed. The order
itself stays readable through the Checkout API for 48 hours, so a reload after that point
still returns the order even though the snippet no longer loads.
Fit the order into your own fields
Section titled “Fit the order into your own fields”recordAndAcknowledge writes Kustom’s order into your system, and if your fields are shorter
than Kustom’s, trim there rather than letting the record fail downstream.
- Address lines. For a line longer than your limit with an empty second line, look for a
whole-word unit designator and split there:
APT,BSMT,BLDG,DEPT,FL,FRNT,HNGR,LBBY,LOT,LOWR,OFC,PH,PIER,REAR,RM,SIDE,SLIP,SPC,STOP,STE,TRLR,UNIT,UPPR. Truncate afterwards if it’s still too long. - Names. Truncate the first name at 15 characters and the last name at 100, and strip
@ $ ! % ^ & * ( ) ~ + #and angle brackets from the first name — then apply the same validation to the last name. - Phone numbers arrive in E.164 format: a plus sign, country code, then the subscriber number including area code, up to fifteen digits.
- US postal codes come in both five-digit and five-plus-four forms. Accept both.
When it breaks
Section titled “When it breaks”| Symptom | What’s happening |
|---|---|
Your status check never matches checkout_complete | The API returns it in lowercase, but the OpenAPI document’s example shows it uppercase. Compare case-insensitively |
| The confirmation page renders a blank checkout | The snippet needs the same <script> re-execution as the checkout page. See Put it on the page |
| A cancelled or stale order confirms successfully | The route trusted order_id from the query string instead of reading the order and checking its own status |
| Pushes keep arriving for an order you already have | You aren’t acknowledging it. Acknowledge from both routes — see Server-side callbacks |
None of these fail loudly. A case-sensitive comparison just never matches, and a query string you trusted confirms whatever id was in it. Read the order rather than the URL, and compare its status case-insensitively.
Recover orders whose push never arrives
Section titled “Recover orders whose push never arrives”Most orders confirm through the redirect within seconds or through the push within minutes, and the push keeps retrying for 48 hours, so an endpoint that’s down for an afternoon costs you nothing. What the retries can’t recover is a push URL that’s wrong, or an endpoint that stays broken past the last attempt. Those orders show up in only one place: your own records.
- List the orders your own system still shows short of confirmed, created more than an hour ago — long enough that the redirect and the first pushes have had their chance.
GETeach one from Order Management —GET /ordermanagement/v1/orders/{order_id}, the same read the push handler makes. A404means the purchase never completed, and there’s nothing to record.- Run it through
recordAndAcknowledge, the same function both routes use, and an order that completed gets recorded and acknowledged on this pass instead of never.
Read from Order Management here, not the Checkout API. A checkout order is only readable for 48
hours, so a sweep that reads it would get 404 for exactly the late orders it exists to find —
and a 404 that means no longer readable looks just like one that means not paid.
A late push can land on the same order the sweep is reading. That’s harmless for the same reason
a reload is: recordAndAcknowledge upserts on order_id, and it’s one function called from
three places rather than three copies that can drift apart.
Prove it
Section titled “Prove it”Complete a purchase in any checkout on these docs, using Kustom’s Playground test data for the
payment step. The redirect lands you on the docs’ confirmation page with order_id attached,
and that page does what the confirmation route above does: it reads the order back and
acknowledges it. Both calls show up above with the status Kustom answered. The push goes to the
docs’ own endpoint, which answers it without showing it here; in your own integration you’ll
find it in Developer Logs.
What you’re left with is a recorded, acknowledged order — the same order_id Capturing an
order expects when it moves the money,
and the last piece the end-to-end test cases check for before your
integration handles a customer who is actually paying.
Related
Section titled “Related”- Server-side callbacks — every callback’s payload, response shape and the push retry schedule in full.
- Order lifecycle — every status an order moves through, including
checkout_complete. - Merchant URLs explained — where
confirmationandpushare registered. - Capture an order — move the money once the goods ship.
- End-to-end test cases — the launch checks this guide has been building toward.
This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.