Skip to content

Authenticate the Shipping Assistant's calls

Implement POST /auth for the Kustom Shipping Assistant: verify the sha256 nonce digest, return a bearer token, and answer 401 to make Kustom fetch a new one.

Every call Kustom makes to your TMS carries a bearer token that you issued. Kustom gets it from POST /auth, by proving it holds the merchant’s key without sending the key itself. This page is the contract for that call and for the headers on every other call. It’s required: without a token, nothing else is called and every checkout falls back to the merchant’s own options.

You issue each merchant two values, and the merchant enters them in the Kustom Portal:

Portal fieldSent asWhat it is
IdentifieridentifierWhich merchant account this is, in your system
Keynever sentA shared secret. Kustom sends only a hash of it

The profile also names your TMS, which is how Kustom knows your base URL. Kustom appends /auth, /shippingoptions and /shipment to that one URL.

POST /auth
Content-Type: application/vdn.klarna.shipping.auth-v1+json
{
"identifier": "merchant-4711",
"secret": {
"nonce": "n8Jq2LwXv0ZbT5mA",
"digest": "8f9fc1128e9dc6e85877176a982100f0c8cea09352bfe12e9ff988d8af38eb63"
}
}
FieldMeaning
identifierThe Identifier from the merchant’s profile
secret.nonce16 random letters and digits, new on every call
secret.digestSHA-256 of the nonce followed by the key, as UTF-8, written as lowercase hex. The example is the digest for the key example-key

The content type reads vdn, not vnd. That’s what Kustom sends, so match on it as written or don’t match on it at all.

To check the digest, compute the same hash from your stored key and compare. Compare without regard to case: Kustom sends lowercase, but the API reference’s example is uppercase and some older integrations were written against that.

import { createHash, timingSafeEqual } from 'node:crypto';
function digestMatches(nonce, digest, key) {
const expected = createHash('sha256').update(nonce + key, 'utf8').digest('hex');
const received = digest.toLowerCase();
return received.length === expected.length &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Answer 200 or 201 with the token:

{ "token": "eyJhbGciOiJIUzI1NiJ9.bWVyY2hhbnQtNDcxMQ.k3M2..." }
FieldRequiredMeaning
tokenYesSent back to you as Authorization: Bearer <token> on every other call

Anything else in the body is ignored, expires_in included. A response without token, or with any status other than 200 or 201, is a failed authentication, and the checkout falls back to the merchant’s options. Answer 401 for wrong credentials so the failure is easy to read in your own logs.

Kustom doesn’t authenticate up front. It calls POST /auth right before a call to your TMS that needs a token it doesn’t have, usually the first POST /shippingoptions, and then makes that call. It stores the token per merchant profile and reuses it across checkouts, for up to a day. It doesn’t read an expiry from you. Instead:

  1. Answer 401 to any call whose token you no longer accept.
  2. Kustom drops the token, calls POST /auth again and repeats the call once with the new token.
  3. If that repeat also gets 401, the call fails, the same as any other error.

So expire tokens on whatever schedule suits you. An hour is a reasonable choice. Issue a separate token per merchant, so revoking one merchant doesn’t log out the others.

HeaderValue
AuthorizationBearer <token>, on every call except POST /auth
Content-TypePer call, on calls with a body — see below
Klarna-Correlation-IdKustom’s id for the request, when there is one. Log it, and quote it when you contact Kustom about a call
CallContent-Type
POST /authapplication/vdn.klarna.shipping.auth-v1+json
POST /shippingoptionsapplication/vdn.klarna.shipping.get_options-v1+json
POST /shipment, PUT /shipment/{shipment_id}application/vdn.klarna.shipping.select_option-v1+json

Kustom sends no Accept header and no idempotency key. Respond with JSON. Fields are snake_case in both directions, and Kustom ignores fields it doesn’t recognize in your responses, so you can add your own without breaking anything.

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