Skip to content

Reserve and update the shipment

Implement POST /shipment and PUT /shipment/{shipment_id} for the Kustom Shipping Assistant: reserve on Buy, update on a retry, refuse with failure_reason.

When the customer presses Buy, Kustom asks your TMS to reserve the delivery they chose. Pressing Buy isn’t the end of the purchase: the payment can still be declined, and the customer can then change anything and press Buy again. So the first attempt creates a reservation with POST /shipment, and every later attempt updates that same reservation with PUT /shipment/{shipment_id}. Both are required.

The customerKustom calls
Presses Buy for the first timePOST /shipment
Presses Buy again, after the payment failed or they changed somethingPUT /shipment/{shipment_id}, with the id you returned
Presses Buy again within 5 minutes, with nothing changedNothing. Kustom reuses your last answer

Kustom doesn’t call either for a checkout that has fallen back to the merchant’s options, or for a digital-only order.

Neither call means the purchase happened. You’ll learn that from POST /shipment/{shipment_id}/confirm, which may never come: a customer who gives up after a declined payment leaves a reservation that is never confirmed. Hold reservations, don’t book them.

POST /shipment
Content-Type: application/vdn.klarna.shipping.select_option-v1+json
Authorization: Bearer <token>

The body is the same session_id, locale, currency, recipient and order as on POST /shippingoptions, with the address now complete, plus the option the customer chose. order is shortened here:

{
"session_id": "f7b7dda8-c9a6-405d-8cca-1c3cbdfda1c0",
"locale": "sv-SE",
"currency": "SEK",
"recipient": {
"given_name": "Klara",
"family_name": "Joyce",
"street_address": "Sveavägen 42",
"postal_code": "111 34",
"city": "Stockholm",
"country": "SE",
"phone": "+46708800000",
"email": "customer@example.com"
},
"order": {
"id": "17d4cd21-99cd-4ffa-97d6-257b258cf053",
"total_amount": 79900,
"total_weight": 890
},
"selected_shipping_option": {
"id": "pn-pickup",
"type": "pickup-point",
"class": "standard",
"carrier": "postnord",
"name": "PostNord pickup point",
"price": 2900,
"tax_rate": 2500,
"location": {
"id": "loc-4",
"name": "Odenplan parcel locker",
"address": {
"street_address": "Odenplan 1",
"postal_code": "113 22",
"city": "Stockholm",
"country": "SE"
},
"coordinates": { "lat": 59.3431, "lng": 18.0492 }
},
"addons": [
{
"type": "sms",
"required": false,
"price": 0,
"data": { "text": "+46708800000", "selected": true }
},
{
"type": "carbon-compensated",
"required": false,
"price": 500,
"data": { "selected": false }
}
]
}
}

selected_shipping_option is the option as you returned it, narrowed to the customer’s choice:

FieldMeaning
id, type, class, carrier, name, tax_rate, free_above, featuresAs you returned them
priceWhat the customer pays, tax included: the chosen time slot’s or location’s price if it has one, otherwise the option’s, plus every add-on they selected. Check it against your own calculation
locationThe pickup point they chose: id, name, address, coordinates
timeslotThe time slot they chose: id, start, end, cutoff, price
addonsEvery add-on the option offered. data.selected says whether the customer took it, and data.text holds what they typed, or the phone number or email for a notification

An add-on carries the id and url you gave it, if any. location and timeslot are absent when the option has none.

Answer 200 or 201 with your reservation:

{
"shipment_id": "SHP-100234",
"selected_shipping_option": {
"carrier": "postnord",
"carrier_product": {
"name": "MyPack Collect",
"identifier": "19",
"addon_identifiers": ["A1"]
}
},
"shipments": [
{ "carrier": "postnord", "tracking_id": "00370712345678901234" }
]
}
FieldRequiredMeaning
selected_shipping_optionYesWithout it, the reservation counts as failed
selected_shipping_option.carrierNoDefaults to generic. Must be one of the carrier values if present
selected_shipping_option.carrier_productNoThe carrier’s product code, name, and any addon_identifiers. Passed on to the merchant’s order with the delivery details
shipment_idYes, in practiceYour id for the reservation. Kustom uses it for PUT and /confirm, and the merchant receives it as tms_reference to book against. Without it, every retry is a new POST, and nothing after the purchase reaches you
shipmentsNoOne entry per parcel: carrier, tracking_id, tracking_url. A malformed tracking_url rejects the response

A shipment_id must be unique per session_id. Kustom doesn’t read the Location header, so you don’t need to send one.

Kustom may repeat a POST /shipment after a network error, so the same request can reach you twice, and it sends no idempotency key. Key reservations on session_id: when a second POST /shipment arrives for a session you already hold a reservation for, update that reservation and return the same shipment_id instead of creating another.

PUT /shipment/{shipment_id}
Content-Type: application/vdn.klarna.shipping.select_option-v1+json
Authorization: Bearer <token>

Same body, same response. Anything can have changed since the POST: the address, the cart, the option, the pickup point or the add-ons. Replace the reservation with what you receive. Return the same shipment_id.

If you answer 404, Kustom sends the request as a new POST /shipment instead. That’s the escape hatch for a reservation you’ve already discarded, not a way to skip implementing PUT: every retry would leave the previous reservation behind in your system.

When you can’t reserve what the customer picked, answer with a 4xx and a reason:

{ "failure_reason": "shipping_option_not_available" }
failure_reasonThe customer sees
shipping_option_not_available“Your shipping option is unavailable” — pick another option and buy again
cutoff_time_expired“Cutoff time is expired” — pick another option and buy again
shipping_address_invalid“Check your delivery address” — fix the address and buy again
anything else, or none“Your shipping option is unavailable”

The purchase stops there. Kustom asks you for options again with POST /shippingoptions, so return options that reflect why you refused, and the customer picks again. If that request fails too, the checkout falls back to the merchant’s own options.

A 5xx, a timeout or a response Kustom can’t read is treated the same way, as a reason-less refusal. A 401 makes Kustom fetch a new token and try once more first.

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