Skip to main content
Cresora Commerce
Integration Guides

Hosted Payment Page (HPP) Guide

Collect card and bank payments on a Cresora-served hosted page, so payer card and bank details never touch your servers.

The Hosted Payment Page (HPP) is the integration where the payer enters card or bank account details on a Cresora-served hosted page. Your servers create a session, send the payer to the page, and learn the outcome from a webhook — you never receive, store, or transmit the payer's card or bank data.

🔒Why the hosted page keeps card data off your systems

The payment fields are served and submitted by the hosted page, not your application, so cardholder data never reaches your infrastructure. That is what keeps cardholder data out of your environment — commonly the SAQ A shape, though which SAQ applies is your QSA's call. Collect card fields yourself and that changes.

The rendering mode is a server-side field

HPP has two rendering modes, redirect and iframe, and the mode is the rendering_mode field on the session you create — not a client-side decision about how you use the returned URL. The server validates the rest of the request against the mode you declare: parent_origin is required in iframe mode and rejected in redirect mode, and postMessage emission is enabled only by a valid parent_origin.

Create a session (redirect mode)

POST https://api.cresoracommerce.ai/api/v1/hpp/sessions

Idempotency-Key is required. amount is a decimal string, not a number. The only required body property is merchant_id, but amount must be present and positive on a payment session. Unknown properties are rejected, so a misspelled field errors rather than being ignored. Card and ACH are both always on at creation — no rail feature flag.

curl
curl -X POST https://api.cresoracommerce.ai/api/v1/hpp/sessions \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_$(uuidgen)" \
  -d '{
    "merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
    "amount": "50.00",
    "currency": "USD"
  }'
Python
import uuid, requests

body = {"merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd", "amount": "50.00", "currency": "USD"}
headers = {
    "Authorization": "Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": f"idem_{uuid.uuid4()}",
}
url = "https://api.cresoracommerce.ai/api/v1/hpp/sessions"
session = requests.post(url, headers=headers, json=body).json()
Node.js
import { randomUUID } from "node:crypto";

const body = { merchant_id: "0190a1b2-c3d4-7e5f-8901-23456789abcd", amount: "50.00", currency: "USD" };
const res = await fetch("https://api.cresoracommerce.ai/api/v1/hpp/sessions", {
  method: "POST",
  headers: {
    Authorization: "Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": `idem_${randomUUID()}`,
  },
  body: JSON.stringify(body),
});
const session = await res.json();

The session response

A successful create returns 200 with the session in state PENDING.

{
  "id": "0190a1c4-5d6e-7f80-9123-456789abcdef",
  "merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
  "state": "PENDING",
  "hpp_url": "<hosted page URL returned by the API>",
  "iframe_url": null,
  "rendering_mode": "redirect",
  "capture_mode": "sale",
  "amount_mode": "customer_entered",
  "page_purpose": "payment",
  "payment_method": null,
  "amount": "50.00",
  "surcharge_amount": "0.00", "convenience_fee_amount": "0.00", "total_amount": "50.00",
  "currency": "USD", "expires_at": "2026-08-12T15:15:00Z", "created_at": "2026-08-12T15:00:00Z"
}
  • id is a UUID — session identifiers are not prefixed strings. hpp_url is the URL the payer must reach; take it from the response, never construct it. iframe_url equals hpp_url in iframe mode and is null in redirect mode.
  • total_amount = amount + surcharge_amount + convenience_fee_amount. A surcharge and a convenience fee are mutually exclusive, so at most one of the two is non-zero.
  • payment_method is card, ach, or null, and stays null until the rail is known. Do not treat null as card.

Redirect mode

Send the payer to hpp_url. On completion the browser lands on the hosted page's configured success-redirect destination, which is page-level configuration.

⚠There is no per-session callback URL

You cannot vary that destination per session, and the browser's arrival back on your site is not proof of payment. Treat the hpp_session.completed webhook as the authoritative outcome.

Iframe mode

Set rendering_mode to iframe, supply parent_origin, then load iframe_url as the iframe src.

curl
curl -X POST https://api.cresoracommerce.ai/api/v1/hpp/sessions \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_$(uuidgen)" \
  -d '{
    "merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
    "amount": "50.00",
    "currency": "USD",
    "rendering_mode": "iframe",
    "parent_origin": "https://checkout.example.com"
  }'

parent_origin must match ^https://[^/]+$ (scheme and host only, no path) and is at most 256 characters. Its host must be covered by the page's allowed embedding domains; the check is fail-closed and an uncovered host returns 422 hpp_parent_origin_not_allowed. https://localhost, with or without a port, is accepted only when localhost is an explicit allow-list entry; other loopback addresses return 422 parent_origin_unsafe.

postMessage lifecycle events

The hosted page emits lifecycle events to the parent frame: ready, session_loaded, payment_succeeded, payment_failed, cancelled, expired, session_invalid, and height_changed. The vendor's list ends with "etc", so treat the set as open and ignore names you do not know. Every message is posted with your parent_origin as the target origin, never *; with no parent_origin — that is, in redirect mode — emission is disabled fail-closed. Your parent page must validate event.origin against the hosted page URL's origin.

⚠Payload shapes are not contractual

The message payloads are produced by the gateway's hosted page and are not part of Cresora's published contract. Key your UX on the event names only and confirm every outcome server-side. Cresora publishes no HPP JavaScript client library, and there is no enable_field_events parameter on session create.

Browser
// `session` is the JSON body returned by POST /api/v1/hpp/sessions
const hppOrigin = new URL(session.iframe_url).origin;
const frame = document.createElement("iframe");
frame.src = session.iframe_url;
document.querySelector("#checkout").appendChild(frame);

window.addEventListener("message", (event) => {
  if (event.origin !== hppOrigin) return; // never skip this check

  // Payload shapes are not published: read what your page sends, extract the name yourself.
  switch (extractLifecycleEventName(event.data)) {
    case "ready":
    case "session_loaded":
      hideYourOwnSpinner(); break;
    case "height_changed": resizeFrame(frame); break; // dimensions are not contractual either
    case "payment_succeeded": showPendingConfirmation(); break; // UX only - fulfil on the webhook
    case "payment_failed": keepFrameOpen(); break; // stays PENDING, the payer can retry
    case "cancelled":
    case "expired":
    case "session_invalid":
      closeFrameAndOfferRestart(); break;
    // any other name: no-op, the vendor set is open
  }
});

Session parameters

PropertyType / valuesNotes
merchant_iduuidRequired — the only required property.
amountdecimal stringRequired and positive on payment sessions; omitted or "0.00" on save_card.
currencyUSDUSD only.
expires_in_secondsinteger 60–900Default 900.
rendering_moderedirect | iframeDefault redirect. Server-side mode selector.
parent_origin^https://[^/]+$, max 256Required on iframe, rejected on redirect. See Iframe mode.
allowed_embedding_domainshostnames, max 20Wildcards OK; raw IPs rejected; localhost as explicit entry.
tokenizeboolean, default falseVaults the card; cvt_… arrives on hpp_session.completed.
page_purposepayment | save_card | save_card_with_initial_chargeSee the save-card sections.
recurring_planobject or nullRequired with, exclusive to, save_card_with_initial_charge.
credential_usagesarray, max 4 uniqueLowercase usages; requires tokenize: true. See save-card.
capture_modesale | authorizeDefault sale. Card rail only; incompatible with save-card purposes.
amount_modecustomer_entered | suggested | lockedsuggested/locked need a positive amount.
require_consentboolean or nullTri-state consent-tick override; requires tokenize: true.
custom_fieldsobjectNames must be enabled, HPP-visible merchant definitions.
customer_emailemail, max 254Strongly recommended with tokenize: true.
customer_iduuidThe Cresora customer id, not your CRM reference.
level3_dataobjectRejected on save-card purposes. No level2_data on this surface.

Create-time errors

StatusCodeMeaning
422hpp_tokenization_not_supportedTokenization is unavailable for this configuration.
422hpp_parent_origin_not_allowedparent_origin host is not covered by the allowed embedding domains.
422parent_origin_unsafe / allowed_embedding_domain_unsafeLoopback origin other than an allow-listed localhost / an unacceptable embedding domain.
422hpp_customer_rejectedThe gateway rejected the referenced customer.
404not_foundcustomer_id is unknown or belongs to another merchant.
400multi_usage_requires_recurring_planMore than one credential_usages entry outside save_card_with_initial_charge.
400credential_usage_contradicts_plancredential_usages on save_card_with_initial_charge must include recurring.
422hpp_page_not_card_onlysave_card_with_initial_charge requires a card-only page.
422hpp_convenience_fee_line_not_shownThe convenience fee line is not displayed on the page.
503feature_disabled / gateway_circuit_openNo PROVISIONED hosted page for the merchant / gateway calls are open-circuited.

Save a card

Set page_purpose to save_card with tokenize true, and omit amount (or send "0.00"). The hosted page runs a zero-dollar verification and vaults the instrument. Incompatible with capture_mode authorize.

curl
curl -X POST https://api.cresoracommerce.ai/api/v1/hpp/sessions \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_$(uuidgen)" \
  -d '{
    "merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
    "page_purpose": "save_card",
    "tokenize": true,
    "customer_email": "payer@example.com"
  }'

The vault token arrives as data.cresoraVaultToken on hpp_session.completed, prefixed cvt_. Re-saving a card already vaulted under the same consent returns the existing cvt_… token and fires no stored_credential.vaulted.

Save a card with an initial charge

save_card_with_initial_charge requires recurring_plan and tokenize true, and amount must be omitted. recurring_plan requires recurring_amount, frequency (daily, weekly, monthly, yearly), and number_of_payments (2–999, including the initial charge); interval is 1–365 and defaults to 1; initial_charge_amount is optional. The merchant needs a card-only page, otherwise you get 422 hpp_page_not_card_only.

curl
curl -X POST https://api.cresoracommerce.ai/api/v1/hpp/sessions \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_$(uuidgen)" \
  -d '{
    "merchant_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
    "tokenize": true,
    "page_purpose": "save_card_with_initial_charge",
    "credential_usages": ["recurring"],
    "recurring_plan": {
      "recurring_amount": "25.00",
      "frequency": "monthly",
      "interval": 1,
      "number_of_payments": 12
    }
  }'

Authorize now, capture later

Set capture_mode to authorize on a payment session. The completed session materializes an AUTHORIZED auth-only transaction that you capture later with POST /api/v1/transactions using type CAPTURE. Card rail only, and incompatible with both save-card purposes. The capture call's request shape is documented in Card Transactions.

Session lifecycle

HppSessionState is PENDING, COMPLETED, EXPIRED, or CANCELLED.

PENDING ──> COMPLETED | EXPIRED | CANCELLED   (all three terminal)
ℹA declined attempt does not resolve the session

A decline leaves the session PENDING and the payer can retry on the hosted page until expires_at. Sessions are not single-use per attempt, so do not tear down your checkout on the first hpp_session.attempt_failed.

Poll session status

GET https://api.cresoracommerce.ai/api/v1/hpp/sessions/{sessionId} — no Idempotency-Key on this read.

curl
SESSION_ID="0190a1c4-5d6e-7f80-9123-456789abcdef"
curl "https://api.cresoracommerce.ai/api/v1/hpp/sessions/$SESSION_ID" \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx"

The response always carries id, state, expires_at, rendering_mode, capture_mode, amount_mode, and page_purpose. It may also carry transaction_id (null until COMPLETED), completed_at, last_attempt with at and reason (one of reversed, rejected, declined, failed, policy_rejected, unknown), card with a required last4 plus expiry_month, expiry_year, brand, token_capture_failure with at and reason, recurring_plan, customer_id, and level3_data. state is effective, not merely stored: a session past expires_at reports EXPIRED at read time even before the sweep persists it. Polling is the fallback; the webhook is primary.

Cancel a session

POST https://api.cresoracommerce.ai/api/v1/hpp/sessions/{sessionId}/cancel — Idempotency-Key is required. It returns the session status with state CANCELLED, emits hpp_session.cancelled, and is idempotent on a session that is already CANCELLED.

curl
SESSION_ID="0190a1c4-5d6e-7f80-9123-456789abcdef"
curl -X POST "https://api.cresoracommerce.ai/api/v1/hpp/sessions/$SESSION_ID/cancel" \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: idem_$(uuidgen)"
⚠A 502, 503, or 504 on cancel leaves the session payable

Cancel is gateway-first. On 502, 503, or 504 the session is still PENDING and still payable, and Cresora does not retry internally — it is safe, and necessary, to retry the cancel yourself.

Two rejections are expected rather than transport failures: 422 invalid_state_transition when the session is not PENDING, and 422 hpp_session_cancel_rejected when the payer completed the payment first. The second is not an authentication problem — do not rotate credentials over it.

Webhooks

Event typeFires when
hpp_session.createdA session is created.
hpp_session.completedThe payment or save-card flow succeeded. Carries data.cresoraVaultToken when tokenize is true. This is the authoritative completion signal.
hpp_session.attempt_failedOnce per declined attempt. The session stays PENDING.
hpp_session.expiredThe session passed expires_at without completing.
hpp_session.cancelledThe session was cancelled.
hpp_session.token_capture_failedVaulting the instrument failed.
hpp_session.payment_unresolvedThe payment's outcome could not be established — reconcile via GET /hpp/sessions/{sessionId} before retrying.

The envelope names the event in event_type, and business fields are nested under data.

{
  "envelope_version": 1,
  "event_id": "0190d8a1-2b3c-7d4e-9f12-7a8b9c0d1e2f",
  "event_type": "hpp_session.completed",
  "occurred_at": "2026-08-12T14:32:11.847Z",
  "partner_id": "0190a1b2-c3d4-7e5f-8901-23456789abcd",
  "idempotency_key": "evt_0190d8a1-2b3c-7d4e-9f12-7a8b9c0d1e2f",
  "data": { "cresoraVaultToken": "cvt_01885fec-4444-7a31-9bbb-3e5b9a1f8c42" }
}

data.cresoraVaultToken is present when the session was created with tokenize: true; use idempotency_key (or the event_id it embeds) as your deduplication key.

Managing embedding domains

Two endpoints maintain the allow-list that gates parent_origin. Both take allowed_embedding_domains with at least 1 and at most 20 hostnames — on the add endpoint the cap applies to the resulting union. localhost is accepted as an explicit entry; raw IP addresses are rejected.

curl
MERCHANT_ID="0190a1b2-c3d4-7e5f-8901-23456789abcd"
BASE="https://api.cresoracommerce.ai/api/v1/config/hpp-page/merchants"
# PUT replaces the whole list
curl -X PUT "$BASE/$MERCHANT_ID/embedding-domains" \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"allowed_embedding_domains": ["checkout.example.com", "*.example.com"]}'
# POST .../add appends to it
curl -X POST "$BASE/$MERCHANT_ID/embedding-domains/add" \
  -H "Authorization: Bearer csk_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"allowed_embedding_domains": ["localhost"]}'

Both configuration endpoints require an Idempotency-Key header, like every other write.