v1.0.0
OpenAPI 3.1.0

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

  1. Create a session: your server calls POST /checkout/hosted-payment-page/sessions with your Kort API credentials and the payment amount. You get back a session id, a payment_intent, and a url.
  2. Embed the iframe: drop that url into 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.
  3. 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.

Sandbox

Client Libraries