- Checkout
- API reference
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 | The customer enters or edits an address | A corrected cart, a shipping options list or both |
country_change | The customer switches purchase country | The same, plus the new purchase_currency and locale |
shipping_option_update | The customer picks a shipping option inside the iframe | A corrected cart |
validation | Just before the purchase completes | 200 to allow, 303 to deny |
upsell, upsell_validation, upsell_notification | The confirmation page offers add-ons | Recommendations, then an approval |
push | About two minutes after the purchase completes | 200, then fetch and acknowledge the order |
notification | A pending order’s status resolves | 200 |
shipping_address_update is one more key, and an older name for address_update — see
below.
Four more keys in merchant_urls — terms, checkout, confirmation and
cancellation_terms — are browser destinations rather than callbacks. Nobody POSTs to them.
Merchant URLs explained covers the whole object.
How every callback works
Section titled “How every callback works”Register the ones you want on the order. You can register all of them in the same object.
{ "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 behttpswith 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 —
4900is 49.00 — andtax_ratecarries two implicit decimals, so 25% is2500. The identities Kustom validates are on Order lines and tax. - Ten seconds, for the in-session three. Past that the response is discarded. For
address_update,country_changeandshipping_option_updatethat stops the purchase. Forvalidationit allows it. - Anything other than
200fromaddress_update,country_changeorshipping_option_updatestops 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 deliberate400or3xx, described per callback below.validationhas its own rules. - Don’t
GETorPUTthe 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
Section titled “address_update”What it does. Lets you reprice the cart for a destination. Kustom POSTs 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_amountandorder_lines, recalculated for the new address. - A shipping options list —
shipping_options, holding the options valid for this address, with one markedpreselected. - Nothing. A
200with an empty JSON object,{}, means nothing changes. A200with 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.
// Kustom sends{ "order_amount": 84800, "order_tax_amount": 16960, "order_lines": [ /* … */ ], "shipping_address": { "postal_code": "981 31", "city": "Kiruna", "country": "SE" }}// 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.
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
Section titled “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.
// 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. Selling into several markets covers the rest of the multi-market setup.
shipping_option_update
Section titled “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.
// 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 } ]}// 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.
Full field list: Shipping option update. Shipping options covers where the options come from in the first place.
validation
Section titled “validation”What it does. Your last look at the order before the purchase completes. Kustom POSTs 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,302or307with aLocationheader — deny it, and send the customer to a page of yours that explains why.400with anerror_typeofaddress_errororapproval_failed, pluserror_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 othererror_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
to true on the order.
Worked example. The last medium sweater sold to somebody else while this customer was typing their address.
HTTP/1.1 303 See OtherLocation: https://shop.example/cart?unavailable=SWEATER-MFull field list: Order validation. Answer the callbacks is where the fail-open or fail-closed decision is argued out, and it’s a decision worth making deliberately.
upsell
Section titled “upsell”Three URLs belong to the confirmation-page upsell feature rather than to the checkout session:
upsellis called to fetch the recommendations to show.upsell_validationis called before the upsell lines are added to the completed order.upsell_notificationis 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 for what to build, and The upsell callback contract for every field of every request and response.
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 walks through all three steps.
GET /ordermanagement/v1/orders/{order_id}Authorization: Basic <base64 of username:password>Content-Type: application/jsonThen acknowledge it, which tells Kustom you have the order and ends the retry schedule.
POST /ordermanagement/v1/orders/{order_id}/acknowledgeAuthorization: Basic <base64 of username:password>Content-Type: application/jsonKustom 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 has the browser-side half and the idempotency this implies.
notification
Section titled “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
Section titled “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
200you sent was ignored. A cart is applied only whenorder_amount,order_tax_amountandorder_linesare all three present — two of them is no update at all. Other parts of the body,shipping_optionsandpurchase_currencyamong 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_updatein 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.
Related
Section titled “Related”- Answer the callbacks — decide which of these you need at all, and whether validation should fail open or closed.
- Merchant URLs explained — the whole
merchant_urlsobject, browser destinations included. - Order lines and tax — build a cart response that passes validation.
- Confirmation & push — the browser-side half of the push.
- Selling into several markets — why
country_changeis not optional once you have more than one. - Checkout JavaScript events — the browser-side events, so your own cart panel stays in sync with the iframe.
- Capture an order — move the money once the goods leave, which is the step the push exists to unblock.
- Checkout Callback API — the generated reference for all six in-session and upsell operations.
This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.