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.
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 -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"
}'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()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"
}idis a UUID — session identifiers are not prefixed strings.hpp_urlis the URL the payer must reach; take it from the response, never construct it.iframe_urlequalshpp_urliniframemode and isnullinredirectmode.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_methodiscard,ach, ornull, and staysnulluntil the rail is known. Do not treatnullascard.
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.
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 -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.
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.
// `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
| Property | Type / values | Notes |
|---|---|---|
merchant_id | uuid | Required — the only required property. |
amount | decimal string | Required and positive on payment sessions; omitted or "0.00" on save_card. |
currency | USD | USD only. |
expires_in_seconds | integer 60–900 | Default 900. |
rendering_mode | redirect | iframe | Default redirect. Server-side mode selector. |
parent_origin | ^https://[^/]+$, max 256 | Required on iframe, rejected on redirect. See Iframe mode. |
allowed_embedding_domains | hostnames, max 20 | Wildcards OK; raw IPs rejected; localhost as explicit entry. |
tokenize | boolean, default false | Vaults the card; cvt_… arrives on hpp_session.completed. |
page_purpose | payment | save_card | save_card_with_initial_charge | See the save-card sections. |
recurring_plan | object or null | Required with, exclusive to, save_card_with_initial_charge. |
credential_usages | array, max 4 unique | Lowercase usages; requires tokenize: true. See save-card. |
capture_mode | sale | authorize | Default sale. Card rail only; incompatible with save-card purposes. |
amount_mode | customer_entered | suggested | locked | suggested/locked need a positive amount. |
require_consent | boolean or null | Tri-state consent-tick override; requires tokenize: true. |
custom_fields | object | Names must be enabled, HPP-visible merchant definitions. |
customer_email | email, max 254 | Strongly recommended with tokenize: true. |
customer_id | uuid | The Cresora customer id, not your CRM reference. |
level3_data | object | Rejected on save-card purposes. No level2_data on this surface. |
Create-time errors
| Status | Code | Meaning |
|---|---|---|
| 422 | hpp_tokenization_not_supported | Tokenization is unavailable for this configuration. |
| 422 | hpp_parent_origin_not_allowed | parent_origin host is not covered by the allowed embedding domains. |
| 422 | parent_origin_unsafe / allowed_embedding_domain_unsafe | Loopback origin other than an allow-listed localhost / an unacceptable embedding domain. |
| 422 | hpp_customer_rejected | The gateway rejected the referenced customer. |
| 404 | not_found | customer_id is unknown or belongs to another merchant. |
| 400 | multi_usage_requires_recurring_plan | More than one credential_usages entry outside save_card_with_initial_charge. |
| 400 | credential_usage_contradicts_plan | credential_usages on save_card_with_initial_charge must include recurring. |
| 422 | hpp_page_not_card_only | save_card_with_initial_charge requires a card-only page. |
| 422 | hpp_convenience_fee_line_not_shown | The convenience fee line is not displayed on the page. |
| 503 | feature_disabled / gateway_circuit_open | No 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 -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 -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 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.
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.
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)"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 type | Fires when |
|---|---|
hpp_session.created | A session is created. |
hpp_session.completed | The payment or save-card flow succeeded. Carries data.cresoraVaultToken when tokenize is true. This is the authoritative completion signal. |
hpp_session.attempt_failed | Once per declined attempt. The session stays PENDING. |
hpp_session.expired | The session passed expires_at without completing. |
hpp_session.cancelled | The session was cancelled. |
hpp_session.token_capture_failed | Vaulting the instrument failed. |
hpp_session.payment_unresolved | The 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.
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.