Skip to content

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.

Starting playground session…
  1. Build the payload
  2. Create order
  3. Render snippet
  4. Validate · optional
  5. Confirm & 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.

  1. The customer presses Pay orderThe customer

    In the checkout, once they have picked a payment method. Nothing for you to build here.

  2. 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.

  3. 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.

  4. 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 →
PathHow it arrivesWhen
Browser redirectGET to merchant_urls.confirmation, {checkout.order.id} substituted inRight after the customer presses Pay order
Server-to-server pushPOST to merchant_urls.pushUsually 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.

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.

routes.js
// 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.

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.
SymptomWhat’s happening
Your status check never matches checkout_completeThe API returns it in lowercase, but the OpenAPI document’s example shows it uppercase. Compare case-insensitively
The confirmation page renders a blank checkoutThe snippet needs the same <script> re-execution as the checkout page. See Put it on the page
A cancelled or stale order confirms successfullyThe 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 haveYou 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.

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.

  1. 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.
  2. GET each one from Order Management — GET /ordermanagement/v1/orders/{order_id}, the same read the push handler makes. A 404 means the purchase never completed, and there’s nothing to record.
  3. 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.

Calls the confirmation page made
0 calls

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.

This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.