Skip to content

What to send

The order payload, block by block — why one line per cart item rather than a total, the four amount identities Kustom validates but never computes, which options to pass and what belongs in a reference.

Starting playground session…
  1. Build the payload
  2. Create order
  3. Render snippet
  4. Validate · optional
  5. Confirm & push

On this page you'll

  • Know which of the four blocks each field belongs to
  • Send order lines that survive the payment method's risk check, the customer's payment app, your TMS and a partial refund
  • Get the four amount identities right the first time
Three order lines from the example payload, and the order totals they sum to
referencequantityunit_pricetax_ratetotal_amounttotal_tax_amount
tshirt-00122500025005000010000
chinos-00117000025007000014000
discount-10pct1-120002500-12000-2400
order10800021600

Three lines, one of them a negative discount. The last row is order_amount and order_tax_amount — the two columns above it, summed. Kustom validates both and rejects the order when they disagree; it never computes them for you. tax_rate carries two implicit decimals, so 2500 is 25%.

The shape. One order object, four blocks, each answering a different question. What’s being bought — order_lines plus the totals that have to reconcile with them. Where — purchase_country, purchase_currency and locale. Only the country is fixed at creation; the other two can be changed on a later update. Where Kustom calls you back — merchant_urls. What the order can do — options, the capability switch covered further down this page. Everything else is optional. Get these four right and the rest is detail.

Nothing in the API requires order_lines to be granular — Kustom accepts a single line summing an entire cart. Nothing downstream forgives it.

What one aggregated line costs each consumer.

order_lines[]

The payment method's risk check
What it does with them: It reads the lines when deciding whether to accept
Cost of one aggregated line: Thinner input, lower acceptance
The customer's payment app
What it does with them: Where they review the purchase afterwards
Cost of one aggregated line: "1 item · 250.00" is all they see
Your TMS, via the Shipping Assistant
What it does with them: Carriers, pickup points, delivery windows
Cost of one aggregated line: Cannot route a box it cannot describe
Kustom tax validation
What it does with them: Validates tax per line
Cost of one aggregated line: One rate only — a mixed-rate cart is rejected
Capture and refund
What it does with them: Both calls carry order_lines
Cost of one aggregated line: No vocabulary for a partial refund of one item

Two of the five consumers above sit outside Kustom’s own validation entirely, which is why the argument starts with conversion rather than mechanics. The payment method — Klarna today, and potentially others tomorrow — reads order_lines to assess risk and decide whether to accept the purchase at all. There’s no Kustom risk engine standing between a merchant and the payment method, only the lines sent to it. The same lines are also what the customer reviews afterwards, inside their payment app rather than your confirmation page.

What breaks on one aggregated lineWhy
Lower acceptance from the payment methodThe payment method’s risk assessment reads the array, and a flattened cart is thinner input
A receipt the customer doesn’t recognizeTheir payment app shows the line you sent, not your product catalog
A box the TMS can’t routeThe Shipping Service Callback API hands it order_lines, when the Kustom Shipping Assistant (KSA) is in use
A rejected order, not a warningKustom’s tax validation checks one tax_rate per line, and a mixed-rate cart has nowhere to put two
No partial refundCapture and refund both carry order_lines, and an aggregated line has no vocabulary for “one of three”

The recommendation follows directly: one order line per cart item, always. Shipping and fees aren’t incidental to the cart — give each its own typed line rather than folding it into a product line or into the order-level totals, for the same reason a product line stays separate: something downstream needs to identify it as what it is. Line mechanics — required fields, the sales_tax type, anonymizing a product name — are covered in Order lines and tax.

Kustom doesn’t compute these numbers for you — it validates the following and rejects the order if any one fails:

  • Amounts are integers in minor units (250.00 SEK is 25000).
  • order_amount equals the sum of every order_lines[].total_amount.
  • order_tax_amount equals the sum of every order_lines[].total_tax_amount.
  • Per line, total_amount equals quantity × unit_price − total_discount_amount.
  • Per line, total_tax_amount sits within ±1 of total_amount − total_amount × 10000 / (10000 + tax_rate).
  • tax_rate carries two implicit decimals, so 25% is 2500.

The worked example at the top of this page is three lines that satisfy all six — including a negative discount line, which is where the arithmetic is easiest to get wrong. A broken total is the single largest source of a 400 on order creation, and the strictness follows directly from the previous section: once a line’s numbers leave the building — into the payment method’s risk assessment, a customer’s payment app, a TMS, a partial refund — Kustom has no way to quietly fix them the way it might if they stayed internal. See What an order is for the object these amounts sit on, and Troubleshooting Checkout for the exact error a broken identity returns.

What belongs on the order, and what belongs on each line?

Section titled “What belongs on the order, and what belongs on each line?”

A field belongs to the order once, or to every line — never both, and the two are easy to confuse on a first pass.

Per order:

  • Who is buying
  • Whether they are old enough
  • Which addresses apply
  • What they’ve consented to
  • Where to call you back

Per line:

  • What it is
  • How many
  • What it costs
  • Its tax
  • Its own reference

The mistake worth naming here: looking for a per-line age flag. There isn’t one — age verification, like selling to businesses, lives in options, at the order level. Both are covered under What options to pass below.

Three places on an order hold data of yours, and they answer different questions.

SlotLives onUse it for
order_lines[].referenceeach lineYour SKU or catalog id for that item
merchant_reference1, merchant_reference2the orderYour own order number, so a Kustom order can be found from your system and back
merchant_datathe order, and each lineAnything of yours that only your own code reads

The rule that matters more than the table: anything you’ll join on later has to be stable and minted by you before the order exists, and must never be regenerated per cart. A merchant_reference1 that’s a fresh value on every retry looks fine until a customer double-submits, and you can no longer tell which of the two orders is the one your own system already knows about.

The escape hatch, for an order created before your own order number exists yet: merchant_reference1 can be set afterward, through Order Management, once you have something to put there. See Your own data on an order for the third slot — attachment — and the full detail on all three.

merchant_urls carries four required URLs, and two of them cost money when they’re wrong. confirmation is where the browser redirects the customer after purchase. push is the server-to-server call that tells you an order exists even when the browser never gets there — a closed tab, a crashed app, a customer who never sees your confirmation page at all. Both need to be publicly reachable HTTPS with a real certificate. A self-signed one that worked in a local test fails silently in Kustom’s hands, not yours.

The other two required URLs, terms and checkout, matter for a working checkout but not for whether the purchase itself completes. Full detail, including the optional callback URLs, is at Merchant URLs explained.

The example above sends a fifth. validation isn’t required, and it’s the only callback that can still refuse a purchase: an order is created when the customer opens the checkout and bought whenever they get around to pressing the button, and stock, a price or a fraud check can turn between the two. Send it if anything about the order can stop being true in that window — and note that skipping it isn’t the same as opting out, because an unanswered validation request completes the purchase after ten seconds either way. Answer the callbacks is where that decision, and the flag that reverses the default, are covered. Server-side callbacks has the payload and response shape for every URL in the list.

Pass itWhen
allowed_customer_types: ["person","organization"]You sell to businesses. Availability is narrower than it looks
verify_age: { minimum_age: 18 }Age-restricted goods, NO/SE. Account must be enabled first. Prefer this over the two older identity-number options
allow_separate_shipping_addressYou ship somewhere other than you bill
vat_removedB2B summaries where VAT should not show
a custom checkboxLegal requires explicit consent
a delivery note (≤255 chars)You have something to say on the confirmation page

Colors and corner radius are set in Kustom Portal. See the Checkout options reference for the rest of the field table, and Sell to businesses (B2B) and Age verification for the two rows above that need your account enabled first.

  • Addresses from your own guest-checkout form. Kustom drops them the moment the customer types their own into the checkout, so shipping to the form’s address rather than the customer’s is a bug waiting for the first mismatch.
  • A reference regenerated on every cart. Mint it once and reuse it — see the mistake under references, above.
  • Personal data in merchant_data. It’s returned to you exactly as sent and stored like any other field of yours, not a place built for anything a customer would expect Kustom, rather than you, to be careful with.
POST
/checkout/v3/orders
Line 1 quantity2
curl -X POST 'https://api.playground.kustom.co/checkout/v3/orders' \
  -u "$KUSTOM_MERCHANT_ID:$KUSTOM_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
  "purchase_country": "SE",
  "purchase_currency": "SEK",
  "locale": "en-SE",
  "order_amount": 108000,
  "order_tax_amount": 21600,
  "order_lines": [
    {
      "type": "physical",
      "reference": "tshirt-001",
      "name": "Cotton T-Shirt",
      "quantity": 2,
      "quantity_unit": "pcs",
      "unit_price": 25000,
      "tax_rate": 2500,
      "total_amount": 50000,
      "total_discount_amount": 0,
      "total_tax_amount": 10000
    },
    {
      "type": "physical",
      "reference": "chinos-001",
      "name": "Slim Fit Chinos",
      "quantity": 1,
      "quantity_unit": "pcs",
      "unit_price": 70000,
      "tax_rate": 2500,
      "total_amount": 70000,
      "total_discount_amount": 0,
      "total_tax_amount": 14000
    },
    {
      "type": "discount",
      "reference": "discount-10pct",
      "name": "10% off",
      "quantity": 1,
      "quantity_unit": "pcs",
      "unit_price": -12000,
      "tax_rate": 2500,
      "total_amount": -12000,
      "total_discount_amount": 0,
      "total_tax_amount": -2400
    }
  ],
  "options": {
    "allow_separate_shipping_address": true
  }
}'

The docs proxy injects your session's credentials. In your own code, use your Playground API username and API password.

A 201 hands back an order_id and an html_snippet — Create the order is where both get used.

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