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

## When Kustom calls it

| The customer | Kustom calls |
| --- | --- |
| Presses Buy for the first time | `POST /shipment` |
| Presses Buy again, after the payment failed or they changed something | `PUT /shipment/{shipment_id}`, with the id you returned |
| Presses Buy again within 5 minutes, with nothing changed | Nothing. 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`](/checkout/shipping/shipping-assistant/after-purchase/),
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.

## Request

```http
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`](/checkout/shipping/shipping-assistant/shipping-options/#request), with
the address now complete, plus the option the customer chose. `order` is shortened here:

```json
{
  "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:

| Field | Meaning |
| --- | --- |
| `id`, `type`, `class`, `carrier`, `name`, `tax_rate`, `free_above`, `features` | As you returned them |
| `price` | **What 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 |
| `location` | The pickup point they chose: `id`, `name`, `address`, `coordinates` |
| `timeslot` | The time slot they chose: `id`, `start`, `end`, `cutoff`, `price` |
| `addons` | **Every** 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.

## Response

Answer `200` or `201` with your reservation:

```json
{
  "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" }
  ]
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| `selected_shipping_option` | **Yes** | Without it, the reservation counts as failed |
| `selected_shipping_option.carrier` | No | Defaults to `generic`. Must be one of the [carrier values](/checkout/shipping/shipping-assistant/shipping-options/#carrier-values) if present |
| `selected_shipping_option.carrier_product` | No | The carrier's product code, `name`, and any `addon_identifiers`. Passed on to the merchant's order with the delivery details |
| `shipment_id` | **Yes, in practice** | Your 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 |
| `shipments` | No | One 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.

## Make `POST /shipment` safe to repeat

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.

## Updating with `PUT`

```http
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.

## Refusing a reservation

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

```json
{ "failure_reason": "shipping_option_not_available" }
```

| `failure_reason` | The 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.

## Related

- [Confirm the shipment](/checkout/shipping/shipping-assistant/after-purchase/) — what happens once the purchase completes
- [Return shipping options](/checkout/shipping/shipping-assistant/shipping-options/) — the option objects this call sends back
- [`POST /shipment` in the API reference](/api/shipping-service-callback/)
