Hosted Payment Page
Kort's Hosted Payment Page (HPP) lets you take card payments through Kort without building or PCI-scoping a card form.
You call one endpoint on your backend with your Kort API credentials,
embed the URL it returns in an iframe, and listen for the resize and
result messages it posts back.
Integration flow
-
Create a session: your server calls
POST /checkout/hosted-payment-page/sessionswith your Kort API credentials and the payment amount. You get back a sessionid, apayment_intent, and aurl. -
Embed the iframe: drop that
urlinto an<iframe>on your checkout page. Kort's hosted page collects the card number, expiry, and CVV directly into an embedded card form. Card data never touches your servers. -
Listen for the result: the iframe reports what happened via
window.postMessage.
Step 1: create a session (server-side)
Call the endpoint below from your backend. Never call it from the
browser: payments-api-key is a secret and must not reach client code.
Your payments-api-key needs at least these permissions:
api_keys:read, api_keys:write, payment_intents:read,
payment_intents:write, payment_methods:read, payment_methods:write.
curl -X POST https://test-payments.kortapi.com/v1/checkout/hosted-payment-page/sessions \
-H "Content-Type: application/json" \
-H "payments-api-key: sk_live_..." \
-H "payments-account: acct_..." \
-d '{
"amount": 200,
"currency": "usd",
"capture_method": "automatic",
"billing_details": {
"name": "Jane Doe",
"address": { "city": "Chicago", "state": "IL", "zip": "60640", "country": "US" }
}
}'
The body is the same shape as
Create a Payment Intent:
amount, currency, capture_method, metadata, and every other
payment-intent field are accepted and validated exactly as they are
there. Kort only intercepts two fields on top of that: it pins
payment_method_types to ["card"] (the only method the hosted page
supports today), and it lifts billing_details out before forwarding
the rest, since billing details aren't part of the payment-intent shape
(see the note under Models → BillingDetails).
Pass the returned url down to your frontend: it's the iframe src.
It expires in 1 hour.
Step 2: embed the iframe (client-side)
<iframe
id="kort-hpp"
src="${createSessionResponse.url}"
allow="payment 'src'; publickey-credentials-get 'src'"
referrerpolicy="origin"
style="width: 100%; height: 520px; border: 0;"
></iframe>
Step 3: handle the result (client-side)
Every message has source: "kort-hosted-payment-page" and a type.
Filter on both before trusting a message, and check event.origin
against the hosted-page origin. There are only two message types:
resize, and result (which covers every way the checkout can end,
via its outcome field).
const HPP_ORIGIN = new URL(createSessionResponse.url).origin;
window.addEventListener("message", (event) => {
if (event.origin !== HPP_ORIGIN || event.data?.source !== "kort-hosted-payment-page") {
return;
}
if (event.data.type === "resize") {
const iframe = document.getElementById("kort-hpp");
iframe.style.height = `${event.data.height}px`;
return;
}
if (event.data.type === "result") {
const { outcome, payment_intent, attempts, last_error } = event.data;
switch (outcome) {
case "completed":
// Check payment_intent.status ("succeeded", "requires_capture", "processing", "requires_action", ...)
console.log("completed", payment_intent?.status);
break;
case "canceled":
// The customer left before finishing. payment_intent may still be
// set, if an earlier attempt failed before they gave up.
console.log("canceled", attempts, last_error, payment_intent?.status, payment_intent?.last_payment_error);
break;
case "expired":
// Session expired. Create a fresh session and swap the iframe's src to retry.
console.log("expired", attempts, last_error);
break;
case "error":
// The session never loaded at all (no card form was ever shown),
// Treat it as terminal: e.g. close the iframe, log the error and create a fresh session
console.error("error", last_error);
break;
}
}
});
Full field-by-field detail on each outcome is under Models → HppResultMessage.