Skip to content

Hosted Payment Page session reference

The /hpp/v1 endpoints — create, read, distribute and disable a hosted session — plus every branding option, session status, callback body and distribution error code.

The Hosted Payment Page API lives at /hpp/v1 and has four operations — create a session, read it, distribute it, disable it — plus one callback Kustom makes to you. Every customization of the hosted page is a parameter of the create call, so they are documented here rather than separately. For the flow these calls sit in, read Integrating the Hosted Payment Page.

OperationCallAuth
Create a sessionPOST /hpp/v1/sessionsBasic
Read a sessionGET /hpp/v1/sessions/{session_id}Basic
Distribute a sessionPOST to the distribution_url from the create responseBasic
Disable a sessionDELETE /hpp/v1/sessions/{session_id}Basic
Status callbackPOST from Kustom to your merchant_urls.status_updateYours to secure

Base URLs are https://api.playground.kustom.co and https://api.kustom.co.

curl -X POST https://api.kustom.co/hpp/v1/sessions \
--header "Authorization: Basic <token>" \
--header "Content-Type: application/json" \
--data @session.json

payment_session_url points at the order the session renders. Build it from the order_id the Checkout API returned:

https://<base url>/checkout/v3/orders/<order_id>

Use the same environment in this URL as the credentials you are calling with.

Where the customer goes after acting on the page, and where Kustom notifies you. All are optional. Leave success, failure, back, cancel and error empty and Kustom shows its own generic pages instead.

KeyPurpose
successRedirect after a successful authorization
failureRedirect after a refusal
backRedirect from the Back button — the session stays reachable
cancelRedirect from the Cancel button — the session becomes CANCELLED. Set without back on an order that has no options.acquiring_channel, it drives a Back button instead
errorRedirect after an error. Without it, errors are indistinguishable from refusals
status_updateWhere Kustom POSTs status callbacks. Must be HTTPS
distribution_updateWhere Kustom POSTs once a link has been sent by e-mail or SMS. Must be HTTPS

Those seven are the whole object. Anything else you send is accepted and discarded.

A distribution_update callback reports the send, not the customer — it carries session_id, distribution_id, method (email or sms) and the contact information the link went to, and it fires once per successful distribution. It follows the same retry policy as status_update, below.

Two placeholders are substituted into these URLs before the customer is sent to them, and which one resolves depends on the URL:

PlaceholderValueWhere it resolves
{{session_id}}The hosted session’s identifierEvery URL in the object
{{order_id}}The order identifiersuccess only

On a checkout session order_id is always populated, so success can carry it unconditionally. A placeholder that doesn’t resolve for a given URL is removed rather than left in place — put {{order_id}} in failure and the customer lands on a URL with an empty parameter, not a literal {{order_id}} you could notice in a log.

Everything under options. All optional, all defaulting to something sensible.

A list of images the page may use as its background. Kustom picks the one that best fits the customer’s viewport, using the width you declare. Images must be served over HTTPS — the create call rejects any other url with a 400.

FieldTypeMeaning
urlStringThe image, over HTTPS
widthIntThe screen width this image works best at
{
"options": {
"background_images": [
{ "url": "https://example.com/background.jpg", "width": 1280 },
{ "url": "https://example.com/background_small.jpg", "width": 480 }
]
}
}

The heading on the page and the browser tab’s title. Because you supply it, you are responsible for translating it to the session’s locale — Kustom does not.

Type String. Defaults to Complete your purchase.

{
"options": {
"page_title": "Complete your purchase"
}
}

Configured on your merchant account in the Portal, not per session. Where they appear depends on the customer’s viewport, since the page is responsive. Changes take a few minutes to show up, in Playground as well as production.

Supplying a back URL is itself a customization: it gives the page a Go back button, which leaves the session alive. Set back alone and that’s the only exit. Set cancel too and the page draws a Cancel button beside it, which ends the session for good. With neither, the customer gets a Cancel button leading to Kustom’s own page.

{
"merchant_urls": {
"back": "https://example.com/back?token=<random_uuid>&sid={{session_id}}"
}
}
FieldTypeMeaning
session_idStringThe session’s identifier. The one field worth storing
session_urlURLRead the session here. Needs your credentials
distribution_urlURLDistribute the session here. Needs your credentials
redirect_urlPublic URLSend the customer here
qr_code_urlPublic URLA QR code image leading to the same page
expires_atDateWhen the session dies — never after the order, at most 48 hours from creation, less a stagger of up to 59 minutes
distribution_moduleObjectToken and URLs for the standalone Distribution Module

Use these URLs as returned rather than reconstructing them. Their shape isn’t a stable contract.

curl https://api.kustom.co/hpp/v1/sessions/<session_id> \
--header "Authorization: Basic <token>" \
--header "Content-Type: application/json"

Just created, waiting for the customer:

{
"session_id": "39a1c773-bafd-754d-af1f-b30c592f1267",
"status": "WAITING",
"updated_at": "2019-05-13T14:51:46.288Z",
"expires_at": "2019-05-15T13:51:43.507Z"
}

Completed — you get the order identifier. A session on a Checkout order never carries an authorization_token, whatever place_order_mode says; that field belongs to sessions on other payment providers:

{
"session_id": "39a1c773-bafd-754d-af1f-b30c592f1267",
"status": "COMPLETED",
"order_id": "a1a8f727-2756-6058-bd3c-40069be0994b",
"klarna_reference": "X438HG0Q",
"updated_at": "2019-05-13T14:54:04.675Z",
"expires_at": "2019-05-15T13:51:43.507Z"
}

klarna_reference is the short human-readable reference on the order, the one that appears on statements and in support conversations. The field name is what the API actually returns.

updated_at always carries the moment the status last changed.

StatusFinalWhat happened
WAITINGNoThe session exists, and the customer hasn’t opened it
IN_PROGRESSNoThe customer opened the page
COMPLETEDYesThe payment was authorized. Carries order_id
FAILEDNoThe payment was refused. The customer may retry
CANCELLEDYesThe customer confirmed a cancellation. Irreversible
BACKNoThe customer pressed Back. They may return
ERRORNoSomething failed during payment. The customer may retry
DISABLEDYesYou disabled it with a DELETE. Irreversible

Only three statuses are final. FAILED, BACK and ERROR all leave the session usable, so don’t treat them as the end of the purchase.

Set merchant_urls.status_update when creating the session and Kustom POSTs to it on every status change. This is cheaper and faster than polling, and is the mechanism to build on.

FieldTypeMeaning
event_idStringEvent identifier. De-duplicate on it — you may see the same event twice
sessionObjectThe session, in exactly the shape the read endpoint returns
{
"event_id": "27ba32b0-644b-4b22-94a9-dac503bcae18",
"session": {
"session_id": "39a1c773-bafd-754d-af1f-b30c592f1267",
"status": "COMPLETED",
"order_id": "a1a8f727-2756-6058-bd3c-40069be0994b",
"klarna_reference": "X438HG0Q",
"updated_at": "2019-05-13T14:54:04.675Z",
"expires_at": "2019-05-15T13:51:43.507Z"
}
}

Kustom doesn’t wait on your response before carrying on with the customer. It expects a 2xx within three seconds. If none arrives it retries twice more, three seconds apart — three attempts in total, each with its own three-second deadline — and then gives up on that callback.

It gives up on the callback, not on the session. Every status change is dispatched and retried independently, so a missed IN_PROGRESS doesn’t cost you the COMPLETED that follows. Still, one status change getting three attempts inside ten seconds is a thin margin, which is why a polling backstop is worth having.

The URL must be HTTPS. Kustom sends no signature or authorization header, so put a secret of your own in the URL and check it on arrival — and treat the callback as a trigger to read the session, not as trusted state:

{
"merchant_urls": {
"status_update": "https://example.com/hpp-callback?hppSessionId={{session_id}}&secretToken=7d1cbc3b-b30c-4be2-a8c4-dc76482d7bf6"
}
}

POST to the distribution_url from the create response. Three methods.

By e-mail. Kustom sends the customer a message, localized to the session’s language, containing the link. If the address matches a customer who has used Kustom before and consented to pre-filling, their details are pre-filled, which measurably shortens the payment.

{
"method": "email",
"contact_information": { "email": "user@example.com" }
}

Kustom validates the address and returns 4xx for a bad one, so put validation in your own UI too. Delivery is asynchronous and can take minutes. Playground is capped at 100 e-mails per day.

By SMS. Kustom texts the link to a phone number. It validates the number against phone_country (ISO 3166-1 alpha-2) and answers an invalid one with CONSTRAINT_VIOLATION:

{
"method": "sms",
"contact_information": { "phone": "07000212345", "phone_country": "SE" }
}

SMS is sent only when the order’s purchase country is one of AT, DE, DK, FI, FR, GB, NL, NO or SE. Anywhere else Kustom sends an e-mail instead. A repeat SMS for the same session to the same number within 10 minutes (1 minute in Playground) is answered as a success but not sent.

By token. For a customer who has scanned the QR code on a shared screen. Pass the access_id the code yielded, unmodified and correctly JSON-escaped:

{
"method": "token",
"contact_information": { "access_id": "5a5e8e70-1a96-4651-b3b6-b774012a5bbb" }
}
StatusCodeMeaning
400UNKNOWNUndefined error. Contact Kustom support
400MISSING_CONTACT_DATAThe contact block is incomplete
400CONSTRAINT_VIOLATIONThe request is malformed
400UNSUPPORTED_COUNTRYThe phone number’s country is not valid in this region
400BLACKLIST_REFUSEDThe recipient is blocked
429EMAIL_LIMIT_EXCEEDEDThe Playground daily e-mail cap was hit
404—The session no longer exists. No body, so there is no code to branch on
502—Distribution failed. No body either; retry, and escalate if it persists

Only the 400 and 429 responses carry a body with an error_code. A 404 and a 502 are bodiless, so for those the status is the whole signal.

curl -X DELETE https://api.kustom.co/hpp/v1/sessions/<session_id> \
--header "Authorization: Basic <token>"

No request body, no response body. The session’s status becomes DISABLED, permanently. A customer who follows the old link gets an explicit error. A customer who was already on the page gets a “Page Not Found”.

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