Skip to content

Integrating the Hosted Payment Page

Create an order, wrap it in a hosted session, get the customer to the page Kustom hosts and read the outcome — the integration model for integrations that can't run any client-side code.

The Hosted Payment Page (HPP) puts Kustom Checkout on a page Kustom hosts, so your integration is server calls only — no snippet, no iframe, no JavaScript of yours anywhere. You create the order exactly as you would for the embedded checkout, create a hosted session on top of it and then get the customer to that session by redirect, e-mail or QR code. This guide covers the whole flow. The field-by-field contract lives in the session reference.

You need a working Checkout integration — HPP doesn’t replace it, it distributes it. Three Kustom services are involved in a full purchase:

ServiceWhat it does hereBase path
CheckoutCreates the order — the cart, the totals, the callback URLs/checkout/v3
Hosted Payment PageWraps that order in a session the customer can reach/hpp/v1
Order ManagementCaptures and refunds afterwards/ordermanagement/v1

All three take the same HTTP Basic credentials. Playground is https://api.playground.kustom.co, production is https://api.kustom.co. Credentials don’t cross between them.

Decide up front which of the three shapes you’re building, because it changes how you distribute the session in step 3:

  • E-commerce. The customer is already in a browser on a site you own, so you can redirect them.
  • Telesales or e-mail. The customer is on the phone or in their inbox. You have to send them a link.
  • Shared device. A kiosk or an in-store screen. Privacy rules mean you must not leave a session open on a shared browser, so you distribute rather than redirect.

This is the ordinary Checkout call. Nothing about it is HPP-specific:

curl -X POST https://api.playground.kustom.co/checkout/v3/orders \
--header "Authorization: Basic <token>" \
--header "Content-Type: application/json" \
--data @order.json

See Create and update an order for what belongs in the body. The response carries the full order, but for HPP you need only one field from it: order_id. The html_snippet in the response is for the embedded checkout — with HPP you never render it yourself, and you should not try to.

POST /hpp/v1/sessions, pointing payment_session_url at the order you just created:

{
"payment_session_url": "https://api.playground.kustom.co/checkout/v3/orders/8cf27b55-53e8-6aba-9fb4-7c692e56ddee",
"merchant_urls": {
"success": "https://example.com/done?order_id={{order_id}}",
"back": "https://example.com/cart",
"failure": "https://example.com/declined",
"error": "https://example.com/error",
"status_update": "https://example.com/hpp-callback?session={{session_id}}&token=<your secret>"
}
}

The response gives you a session_id and a set of URLs — redirect_url for the customer, session_url for you to poll, distribution_url to send the link by e-mail or SMS and qr_code_url for a screen. The reference lists all of them, and every option you can set on the session, including branding.

Use the URLs as returned. Their structure isn’t part of the contract and Kustom changes it. Only session_id is stable enough to store.

Redirect if the customer is already in a browser you control. Send them to redirect_url, and set merchant_urls so they come back to you afterwards:

URLWhen the customer lands there
successPayment was authorized. You must show your own confirmation — Kustom’s isn’t enough on its own.
failurePayment was refused. The customer has already seen why.
backThe customer pressed Back. The session stays open, so they can return to it. Disabling it is your call.
cancelThe customer confirmed a cancellation, which moves the session to CANCELLED permanently.
errorSomething went wrong. Without this URL, errors go to failure and you can’t tell a decline from a fault.

Which of the two exit controls the page draws follows from which of the two URLs you set. A Back control leaves the session reachable. A Cancel control prompts for confirmation and then moves the session to CANCELLED permanently. Set both and the page draws both. Set neither and the customer gets a Cancel leading to Kustom’s own page.

Set only one and the order decides. With just back, you get Back. With just cancel, you get Cancel when the order declares an options.acquiring_channel and Back when it doesn’t — the discriminator is whether the field is set at all, not which channel it names.

Both URLs also stand in for each other. back falls back to cancel when it’s missing, and error falls back to failure — which is why a missing error URL leaves you unable to tell a decline from a fault rather than losing the customer entirely.

Those five are every redirect merchant_urls carries. The object takes two more, both callbacks rather than destinations — status_update, in step 4 below, and distribution_update, which fires once when Kustom has successfully sent a link by e-mail or SMS and carries session_id, distribution_id, method and the contact information it was sent to.

Distribute if you can’t redirect. POST to the distribution_url with an e-mail address or a phone number and Kustom sends a localized message containing the link, or display qr_code_url on a screen for the customer to scan. Both are covered in the reference. If an operator — a clerk or a telesales agent — is driving the flow by hand, use the Distribution Module rather than building your own interface on the API.

Don’t rely on the redirect. The customer can close the window before it happens, and then you have taken a payment you don’t know about. Track the session instead, by one of two mechanisms, ideally both:

  • Callbacks. Set merchant_urls.status_update at session creation and Kustom POSTs the session to you on every status change. This is the mechanism to build on.
  • Polling. GET the session_url from the create response. Use it as the backstop for callbacks your endpoint missed, not as the primary path.

A callback is attempted three times in total — the first send plus two retries, three seconds apart — and each attempt is given three seconds to answer. That’s the whole budget, about nine seconds from the first attempt to the last, which is precisely why the polling backstop is worth having.

The statuses and the callback body are in the reference. The one you’re waiting for is COMPLETED.

Unless the order was created with auto_capture, no money moves until you capture it through Order Management — see Capture an order.

If the purchase is off — the offer expired, the customer paid another way — DELETE the session. Its status becomes DISABLED, permanently, and the link stops working.

Why the hosted session expires before the order

Section titled “Why the hosted session expires before the order”

The session and the order have tied lifetimes. A session never outlives its order: it takes the order’s expiry, capped at 48 hours after the session is created, and then expires up to an hour earlier than that. The early part is a stagger of between 0 and 59 minutes that Kustom applies to spread expiries out, so two sessions created a minute apart on identical orders can expire at different times.

The practical consequence is that expires_at on the session is the deadline that matters to you, and the order’s own expiry isn’t something to distribute a link against.

You may attach several sessions to one order — for instance to try two brandings — but it’s a bad practice and Kustom treats it as one. Prefer one session per order.

Run the whole flow in Playground before you build the production path:

  1. Create an order, then a session on it. You get a session_id and a redirect_url.
  2. Open redirect_url in a browser. Your status_update endpoint receives IN_PROGRESS.
  3. Pay with Playground test data. Your endpoint receives COMPLETED, carrying the order_id.
  4. GET the session_url and confirm it agrees with the callback.
  5. DELETE a second, unused session and confirm its status is DISABLED and its link is dead.

Auto-capture. Create the order with auto_capture and money moves at authorization, with no Order Management call from you. Use it when goods are handed over immediately.

Place-order mode. A session on a Checkout order always completes with an order_id, never an authorization_token, so {{order_id}} is usable in success whatever you set. The one options.place_order_mode value that changes anything here is CAPTURE_ORDER, which sets options.auto_capture on the order for you — the auto-capture variation above, from the session side. PLACE_ORDER is a no-op on a Checkout order.

Generic confirmation pages. Leave merchant_urls empty and Kustom shows its own success, failure and cancellation pages. Acceptable for a pilot. For anything real, send the customer somewhere you control.

The customer paid but never came back. Expected, and the reason step 4 exists. The redirect is best-effort. The callback and the read endpoint are the record.

A callback never arrived. Kustom gives up after three attempts against an endpoint that doesn’t answer within three seconds each time. Each callback is retried on its own, so losing one doesn’t stop the next: a session whose IN_PROGRESS never landed can still deliver COMPLETED. Poll session_url to recover the state, then fix the endpoint’s latency — three seconds is the whole budget per attempt, including TLS.

The customer sees “Page Not Found”. The session was disabled while they were on it. That’s what DISABLED looks like from their side.

You can’t tell declines from faults. You didn’t set error, so both went to failure. Set both URLs.

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