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

[Answering the callbacks](/checkout/integrate/validation-callback/) 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 order** — _The customer._ In the checkout, once they have picked a payment method. Nothing for you to build here.
2. **The payment goes through** — _Kustom._ 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 page** — _Kustom._ 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 push** — _Kustom._ 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. See [The push in full](/checkout/callbacks/#push).

## 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

Both URLs carry the order id in a query parameter you name when you register them —
`order_id` here:

```json
"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.

```js title="routes.js" wrap
// 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](/checkout/integrate/render-checkout-snippet/) 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

`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](https://en.wikipedia.org/wiki/E.164): 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

| 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](/checkout/integrate/render-checkout-snippet/) |
| 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](/checkout/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

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.

## Prove it

> Live page: opens the session console, which lists every call in this session — both the calls the docs made and the ones Kustom made back (`GET /demo-api/session/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](/order-management/capture/) expects when it moves the money,
and the last piece the [end-to-end test cases](/basics/test-data/#end-to-end-test-cases) check for before your
integration handles a customer who is actually paying.

## Related

- [Server-side callbacks](/checkout/callbacks/) — every callback's payload, response shape and
  the push retry schedule in full.
- [Order lifecycle](/basics/order-lifecycle/) — every status an order moves through, including
  `checkout_complete`.
- [Merchant URLs explained](/checkout/integrate/merchant-urls/) — where `confirmation` and
  `push` are registered.
- [Capture an order](/order-management/capture/) — move the
  money once the goods ship.
- [End-to-end test cases](/basics/test-data/#end-to-end-test-cases) — the launch checks this guide has been
  building toward.
