Skip to content

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.

CallbackFires whenYou answer with
address_updateThe customer enters or edits an addressA corrected cart, a shipping options list or both
country_changeThe customer switches purchase countryThe same, plus the new purchase_currency and locale
shipping_option_updateThe customer picks a shipping option inside the iframeA corrected cart
validationJust before the purchase completes200 to allow, 303 to deny
upsell, upsell_validation, upsell_notificationThe confirmation page offers add-onsRecommendations, then an approval
pushAbout two minutes after the purchase completes200, then fetch and acknowledge the order
notificationA pending order’s status resolves200

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.

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

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

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

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.

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.

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, 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 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 Other
Location: https://shop.example/cart?unavailable=SWEATER-M

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

Three URLs belong to the 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 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/json

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

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 has the browser-side half and the idempotency this implies.

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.

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

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