# Server-side callbacks

Every HTTP call Kustom makes to your server during and after a Checkout session — one section per callback with what fires it, what it sends, what a valid answer looks like and a worked example of the cart correction each one exists to let you make.

Kustom Checkout is not a one-way embed. While the customer is filling it in, Kustom calls your
server whenever something changes that only you can price — an address, a country, a shipping
option — and gives you a short window to send a corrected cart back. After the purchase it
calls you again, server-to-server, so you learn about the order even if the customer's browser
never reaches your confirmation page.

Those two moments behave differently, and the difference is the only thing you need to hold on
to while reading this page:

- **In-session callbacks** run while the customer is watching. You have ten seconds, your
  response body *is* the update and a failure stops the purchase with a generic error.
- **After-purchase callbacks** are notifications. The order already exists. Nothing the customer
  sees depends on your answer, and Kustom retries until you acknowledge.

Every one of them is opt-in. Kustom calls the URLs you name in `merchant_urls` on the order and
nothing else.

| Callback | Fires when | You answer with |
| --- | --- | --- |
| [`address_update`](#address_update) | The customer enters or edits an address | A corrected cart, a shipping options list or both |
| [`country_change`](#country_change) | The customer switches purchase country | The same, plus the new `purchase_currency` and `locale` |
| [`shipping_option_update`](#shipping_option_update) | The customer picks a shipping option inside the iframe | A corrected cart |
| [`validation`](#validation) | Just before the purchase completes | `200` to allow, `303` to deny |
| [`upsell`, `upsell_validation`, `upsell_notification`](#upsell) | The confirmation page offers add-ons | Recommendations, then an approval |
| [`push`](#push) | About two minutes after the purchase completes | `200`, then fetch and acknowledge the order |
| [`notification`](#notification) | A pending order's status resolves | `200` |

`shipping_address_update` is one more key, and an older name for `address_update` — see
[below](#address_update).

Four more keys in `merchant_urls` — `terms`, `checkout`, `confirmation` and
`cancellation_terms` — are browser destinations rather than callbacks. Nobody `POST`s to them.
[Merchant URLs explained](/checkout/integrate/merchant-urls/) covers the whole object.

## How every callback works

Register the ones you want on the order. You can register all of them in the same object.

```json
{
  "merchant_urls": {
    "terms": "https://shop.example/terms",
    "checkout": "https://shop.example/checkout?kustom_order_id={checkout.order.id}",
    "confirmation": "https://shop.example/confirmation?kustom_order_id={checkout.order.id}",
    "push": "https://shop.example/api/kustom/push?kustom_order_id={checkout.order.id}",
    "address_update": "https://shop.example/api/kustom/address-update",
    "country_change": "https://shop.example/api/kustom/country-change",
    "shipping_option_update": "https://shop.example/api/kustom/shipping-option",
    "validation": "https://shop.example/api/kustom/validate",
    "notification": "https://shop.example/api/kustom/notification?kustom_order_id={checkout.order.id}"
  }
}
```

Everything below is true of all of them:

- **`POST`, JSON, HTTPS.** Every callback URL must be `https` with a real certificate. A
  self-signed certificate may survive the in-session callbacks and will fail the push.
- **`{checkout.order.id}` is substituted** into the URL before it's called, so a handler can
  read the order id from its own query string.
- **Amounts are minor units** — `4900` is 49.00 — and `tax_rate` carries two implicit decimals,
  so 25% is `2500`. The identities Kustom validates are on
  [Order lines and tax](/checkout/tax-and-order-lines/).
- **Ten seconds, for the in-session three.** Past that the response is discarded. For
  `address_update`, `country_change` and `shipping_option_update` that stops the purchase. For
  `validation` it allows it.
- **Anything other than `200` from `address_update`, `country_change` or
  `shipping_option_update` stops the purchase.** The customer is shown a generic error and told
  to try again, and the failure is logged for merchant support. The one exception is a
  deliberate `400` or `3xx`, described per callback below. `validation` has its own rules.
- **Don't `GET` or `PUT` the order inside an in-session handler.** The response body *is* the
  update. A separate write will fight with it.
- **Developer Logs in the Playground Portal** lists every callback Kustom made for an order and
  the status code you returned. Search by order id. It's the fastest way to see what you
  actually sent rather than what you meant to send.

## `address_update`

**What it does.** Lets you reprice the cart for a destination. Kustom `POST`s the current cart
plus the addresses the customer has entered, and replaces the order with whatever you answer.

**When it fires.** Whenever the customer enters or edits the billing or shipping address. If the
customer only enters one address, it serves as both.

**Use it when** the totals depend on where the parcel is going — sales tax that varies by
address, shipping that costs more to reach or options that aren't available everywhere. A
catalog with one flat shipping rate and uniform tax has no reason to register it.

**What you get.** `order_amount`, `order_tax_amount`, `order_lines`, `billing_address`,
`shipping_address`, `selected_shipping_option`, `purchase_currency` and `customer`, plus
`locale`, `merchant_data`, `merchant_reference1`, `merchant_reference2` and `tags` when the
order has them.

**What you answer with.** A `200` carrying any of:

- **A cart** — `order_amount`, `order_tax_amount` and `order_lines`, recalculated for the new
  address.
- **A shipping options list** — `shipping_options`, holding the options valid for this address,
  with one marked `preselected`.
- **Nothing.** A `200` with an empty JSON object, `{}`, means nothing changes. A `200` with no
  body at all is not valid JSON and fails like any other bad response.

A body is applied part by part, and each part stands on its own: send `shipping_options`
alone and only the options change. The cart is the one group that has to be complete —
`order_amount`, `order_tax_amount` and `order_lines` together, or none of the three, because
a cart missing any of them is ignored in full. `purchase_currency`, `locale` and
`merchant_data` each apply on their own.

To reject the address instead, answer `400` with an `error_type` of `address_error`,
`unsupported_shipping_address` or `approval_failed`. The first and last also require
`error_text`, which is shown to the customer, who stays in the checkout and can correct it. A
`303` with a `Location` header sends them out to a page of yours instead.

**Worked example.** A customer moves the shipping address from Stockholm to a remote postcode in
northern Sweden. Same-day delivery doesn't reach it, and the carrier surcharges the rest, so
you answer with a shorter options list and a corrected fee.

```json
// Kustom sends
{
  "order_amount": 84800,
  "order_tax_amount": 16960,
  "order_lines": [ /* … */ ],
  "shipping_address": { "postal_code": "981 31", "city": "Kiruna", "country": "SE" }
}
```

```json
// You answer 200
{
  "order_amount": 88800,
  "order_tax_amount": 17760,
  "purchase_currency": "SEK",
  "order_lines": [ /* … the shipping_fee line now 8900 */ ],
  "shipping_options": [
    { "id": "standard", "name": "Standard 3–5 days", "price": 8900,
      "tax_amount": 1780, "tax_rate": 2500, "preselected": true }
  ]
}
```

Full field list: [Address update](/api/checkout/callbacks/update-address/).

**`shipping_address_update` is the same callback under an older name.** Kustom looks for
`address_update` first and falls back to `shipping_address_update` only when the first is
absent — same trigger, same request, same response. It exists so that integrations written
before `address_update` keep working. Set one or the other, never both, and on anything new
set `address_update`.

## `country_change`

**What it does.** The same job as `address_update`, for the one change that moves more than the
amounts.

**When it fires.** When the customer switches the billing or shipping country. It takes the same
request and the same response shape, so most of `address_update` above applies unchanged.

**Use it when** you sell into more than one market. If you allow several shipping countries on
an order, you **must** handle this one: currency and language have to move with the country, and
a customer left in the wrong currency has a broken purchase rather than a cosmetic fault.

**What is different.** Answer with `purchase_currency` and `locale` as well as the cart. Send
the prices you actually charge in the new market — a currency-converted figure is a different
price, and it's the one the customer will be charged.

**Worked example.** A customer switches from Sweden to Finland.

```json
// You answer 200
{
  "purchase_currency": "EUR",
  "locale": "fi-FI",
  "order_amount": 7500,
  "order_tax_amount": 1524,
  "order_lines": [ /* … priced in EUR, tax_rate 2550 for FI … */ ]
}
```

Full field list: [Country change](/api/checkout/callbacks/country-change/).
[Selling into several markets](/checkout/use-cases/multi-market/) covers the rest of the
multi-market setup.

## `shipping_option_update`

**What it does.** Whenever the customer changes the shipping option, this callback lets you
override the shipping fee. Kustom sends you the cart, and the `shipping_fee` line in your answer
is the one the order carries.

Without this callback registered, Kustom adds the `shipping_fee` order line itself — the
selected option's name, price and tax, merged into the cart for you. Registering
`merchant_urls.shipping_option_update` turns that off: Kustom copies the choice onto the order
as `selected_shipping_option` and calls you, and the cart you answer with is the cart. If your
response carries no `shipping_fee` line, the order has no shipping line.

**When it fires.** When the customer picks a different shipping option *inside the checkout
iframe*. If your own page owns shipping selection, Kustom already handles the cost update and
you don't need this callback at all.

**Use it when** the option's list price isn't the price you mean to charge — a discount code
that also covers shipping, a free-shipping threshold the new option crosses or a surcharge that
depends on what is in the cart rather than on the option itself.

**Worked example.** The cart holds a 799.00 SEK sweater and the code `SAVE50`, which halves
shipping as well as giving money off the goods. The customer switches from Standard to Express at
49.00. Because the callback is registered, no `shipping_fee` line has been added — you own it.
You answer with the cart carrying that line at half price, and with the totals moved to match.

```json
// Kustom sends
{
  "order_amount": 79900,
  "order_tax_amount": 15980,
  "selected_shipping_option": {
    "id": "express", "name": "Express 1–2 days",
    "price": 4900, "tax_amount": 980, "tax_rate": 2500
  },
  "order_lines": [
    { "reference": "SWEATER-M", "name": "Wool sweater", "quantity": 1,
      "unit_price": 79900, "tax_rate": 2500,
      "total_amount": 79900, "total_discount_amount": 0, "total_tax_amount": 15980 }
  ]
}
```

```json
// You answer 200 — SAVE50 halves the fee
{
  "order_amount": 82350,
  "order_tax_amount": 16470,
  "purchase_currency": "SEK",
  "order_lines": [
    { "reference": "SWEATER-M", "name": "Wool sweater", "quantity": 1,
      "unit_price": 79900, "tax_rate": 2500,
      "total_amount": 79900, "total_discount_amount": 0, "total_tax_amount": 15980 },
    { "type": "shipping_fee", "reference": "express", "name": "Express 1–2 days (SAVE50)",
      "quantity": 1, "unit_price": 4900, "tax_rate": 2500,
      "total_amount": 2450, "total_discount_amount": 2450, "total_tax_amount": 490 }
  ]
}
```

The discount lives in `total_discount_amount` on the line rather than in a lower `unit_price`,
so the customer can see what the option costs and what they saved. Every amount in that response
has to reconcile — `total_amount` against quantity, price and discount, and `order_amount`
against the lines — or Kustom rejects it. See
[Order lines and tax](/checkout/tax-and-order-lines/).

Full field list: [Shipping option update](/api/checkout/callbacks/update-shipping-option/).
[Shipping options](/checkout/shipping/) covers where the options come from in
the first place.

## `validation`

**What it does.** Your last look at the order before the purchase completes. Kustom `POST`s the
whole order while its status is still `checkout_incomplete`, so you can check anything that
could have changed since you created it, and mint your own order number while you're there.

**When it fires.** After the customer presses **Pay order** and before the payment is
authorized.

**Use it when** something about the order can go stale between creation and purchase — stock
running out, a price expiring, a delivery restriction, a fraud signal. Checkout orders live for
48 hours, so "since creation" can be a long time.

**What you answer with.**

- **`200`** — allow the purchase. The customer is redirected to confirmation as usual.
- **`303`**, **`301`**, **`302`** or **`307`** with a `Location` header — deny it, and send the
  customer to a page of yours that explains why.
- **`400`** with an `error_type` of `address_error` or `approval_failed`, plus `error_text` —
  deny it, but keep the customer in the checkout and show them the message. These are the only
  two accepted types. Kustom logs and ignores any other `error_type`, and the purchase
  completes.

**It fails open.** Validation must answer within ten seconds. If it doesn't, Kustom treats the
order as valid and redirects to confirmation anyway. If you want to require a successful
response, set
[`options.require_validate_callback_success`](/api/checkout/create-order-merchant/#body.options.require_validate_callback_success)
to `true` on the order.

**Worked example.** The last medium sweater sold to somebody else while this customer was typing
their address.

```http
HTTP/1.1 303 See Other
Location: https://shop.example/cart?unavailable=SWEATER-M
```

Full field list: [Order validation](/api/checkout/callbacks/validate/).
[Answer the callbacks](/checkout/integrate/validation-callback/) is where the fail-open or
fail-closed decision is argued out, and it's a decision worth making deliberately.

## `upsell`

Three URLs belong to the [confirmation-page upsell](/checkout/use-cases/confirmation-page-upsell/)
feature rather than to the checkout session:

- `upsell` is called to fetch the recommendations to show.
- `upsell_validation` is called before the upsell lines are added to the completed order.
- `upsell_notification` is called after the upsell lines have been added.

Upsell must be enabled for your account before any of them is called.

They are documented with the feature: [Upsell on the confirmation
page](/checkout/use-cases/confirmation-page-upsell/) for what to build, and [The upsell callback
contract](/checkout/use-cases/upsell-callback-contract/) for every field of every request and
response.

## `push`

**What it does.** Tells you a purchase completed, server-to-server, so you learn about the order
even if the customer's browser never reaches your confirmation page.

**When it fires.** After the purchase completes and the order has been queued into Order
Management. Expect roughly a two-minute delay before the first attempt.

**Use it** always. It's one of the two required notifications, and the only one that doesn't
depend on a browser.

**What you answer with.** Answer `200` straight away. The push only tells you that an order
completed. It doesn't carry the order, so read the order id from the query string and fetch the
order, import it into your system and acknowledge it. [Confirmation &
push](/checkout/integrate/confirmation-and-push/) walks through all three steps.

```http
GET /ordermanagement/v1/orders/{order_id}
Authorization: Basic <base64 of username:password>
Content-Type: application/json
```

**Then acknowledge it**, which tells Kustom you have the order and ends the retry schedule.

```http
POST /ordermanagement/v1/orders/{order_id}/acknowledge
Authorization: Basic <base64 of username:password>
Content-Type: application/json
```

Kustom retries the push at 5, 15, 30 and 60 minutes, then every four hours, for a total of 48
hours or until you acknowledge. After 48 hours it stops regardless.

Acknowledging is also the natural moment to write your own references back onto the order.
`merchant_reference1` is the customer-facing order id your shop generated. `merchant_reference2`
is free for a second identifier of your own.

The push is not a backup for the confirmation redirect — either can arrive alone, both are
equally authoritative and either can arrive first. If the order doesn't exist in your system
when the push arrives, create it. If you've decided not to fulfill it, cancel it in Kustom
rather than ignoring it. [Confirmation & push](/checkout/integrate/confirmation-and-push/) has
the browser-side half and the idempotency this implies.

## `notification`

**What it does.** Tells you when an order that didn't resolve at purchase time finally does.

**When it fires.** When the status of a pending order changes. An order comes back with
`fraud_status` of `ACCEPTED`, `PENDING` or `REJECTED`. A `PENDING` order is one Kustom is still
reviewing, and this callback is how you hear the outcome.

**Use it when** any of your payment methods can leave an order pending. If every order you see
resolves immediately you'll never be called, and registering it costs nothing.

**What you answer with.** `200`. As with the push, treat it as a notification rather than a
payload: put `{checkout.order.id}` in the URL, then fetch the order from Order Management and
read its current `fraud_status`. Don't release goods on a `PENDING` order.

## Troubleshooting

- **The purchase stops with a generic error message.** An in-session callback returned a status
  other than `200`, returned an invalid cart or shipping option list, or didn't answer within
  ten seconds. Check Developer Logs for the response you actually sent.
- **The cart in a `200` you sent was ignored.** A cart is applied only when `order_amount`,
  `order_tax_amount` and `order_lines` are all three present — two of them is no update at all.
  Other parts of the body, `shipping_options` and `purchase_currency` among them, apply
  independently of it and of each other.
- **The validation callback never seems to fire.** Validation must complete within ten seconds.
  Past that, Kustom treats the order as valid and redirects to confirmation.
- **The push never arrives.** The push requires a publicly reachable HTTPS endpoint with a valid
  certificate. A self-signed certificate that worked for `address_update` in testing won't
  work here.
- **Pushes keep arriving for an order you already have.** You're not acknowledging it.
  Acknowledge on the confirmation page as well as from the push handler.
- **A 4xx or 5xx from the Checkout API mid-session.** Create a new order. Checkout orders are
  only valid for 48 hours after the last merchant update or customer interaction, and expire
  after that. See [Troubleshooting Checkout](/checkout/troubleshooting/).

## Related

- [Answer the callbacks](/checkout/integrate/validation-callback/) — decide which of these you
  need at all, and whether validation should fail open or closed.
- [Merchant URLs explained](/checkout/integrate/merchant-urls/) — the whole `merchant_urls`
  object, browser destinations included.
- [Order lines and tax](/checkout/tax-and-order-lines/) — build a cart response
  that passes validation.
- [Confirmation & push](/checkout/integrate/confirmation-and-push/) — the browser-side half of
  the push.
- [Selling into several markets](/checkout/use-cases/multi-market/) — why `country_change` is
  not optional once you have more than one.
- [Checkout JavaScript events](/checkout/client-side-events/) — the browser-side events, so
  your own cart panel stays in sync with the iframe.
- [Capture an order](/order-management/capture/) — move the money once the goods leave,
  which is the step the push exists to unblock.
- [Checkout Callback API](/api/checkout/callbacks/) — the generated reference for all six
  in-session and upsell operations.
