View as Markdown

llms.txt

This guide isn't available right now

This instance couldn't load its guide catalog. The guide returns as soon as the catalog is readable again.

Back to the guides

No such guide

This instance publishes no guide under that address. It may have been renamed, or it may belong to a feature this installation hasn't enabled.

Back to the guides

That guide is part of the product documentation

This guide is written for someone operating WinkPG through its screens rather than integrating against it, so it lives in the application's own help section instead of here. Sign in to WinkPG and open Help to read it.

Back to the guides

Guides Integration

Hosted payment page iframe integration

Embed the Hosted Payment Page in an iframe and communicate over the postMessage lifecycle protocol.

The Hosted Payment Page (HPP) supports two embedding modes: redirect mode (you navigate the cardholder to the HPP address) and iframe mode (you embed the HPP inside a parent page and communicate over the postMessage lifecycle protocol). This guide covers iframe mode: authorizing a parent origin, the lifecycle event protocol, the command channel, and the security model.

Two pieces of configuration

Iframe embedding requires two independent settings to line up:

  1. Page-level AllowedEmbeddingDomains: a list of domains (max 20) authorized to embed a given HPP. Wildcards such as *.example.com are supported. Raw IP addresses, localhost, and entries without a TLD are rejected. This is the security envelope: a page without your origin in its allow list can't be framed by your site.
  2. Per-session parentOrigin: set when creating each session. It must be a valid HTTPS origin whose host matches an AllowedEmbeddingDomains entry. If it's omitted, the session is still valid for non-embedded use, but the postMessage channel is disabled fail-closed (no events flow to a parent).

If the host doesn't match, session creation returns 400 with HostedPaymentPage:HppSession:ParentOriginNotAllowed.

The postMessage lifecycle protocol

Once a session has a parentOrigin, the embedded HPP emits a stream of postMessage events to the parent throughout the session lifecycle. Every event shares one envelope shape; the data payload varies by type. Filter on the source discriminator (winkpg-hpp).

A successful payment fires events in this order:

session_loaded -> ready -> payment_started -> payment_succeeded -> navigated(to: 'result')

Subscribe to ready (not session_loaded) when you need the iframe to be visually interactive: ready fires once the form root has mounted in the DOM.

Key events include payment_succeeded, payment_failed, payment_pending, height_changed (for automatic resizing), validation_failed, session_expired, session_invalid, and the save-only pair card_saved (a card was vaulted) and ach_saved (a bank account was vaulted). If no event arrives within a few seconds, fall back to a timeout and treat it like session_invalid: a session that never existed has no parentOrigin to address, so it's intentionally silent.

If your page accepts bank accounts on a save-only session, handle ach_saved. A bank-account save used to arrive as card_saved with the card fields empty; it now arrives as ach_saved with the bank account's last four and account type. A page that listens only for card_saved is no longer told about an ACH save. Card saves are unchanged.

card_saved carries the card's BIN classification beside the last four and the brand: bin (the leading digits), funding (Credit, Debit, Prepaid, or Charge), and panLength. Use them to apply your own funding-type rules at save time instead of making a second server call to read the token back. All three are null when the card's range didn't resolve, so branch on the value rather than on the key. The card's expiration date is never on this envelope: subscribe to the HostedPaymentPage.CardSaved webhook if you need it, which is delivered only to the merchant that owns the card.

What payment_started tells you

payment_started fires when the customer submits, after the page's own field validation and before the payment is created. Its payload carries paymentMethod (card or bank_account) and saveRequested (true when the customer asked for the payment method to be stored). Both keys are always present.

saveRequested is the effective answer rather than the raw checkbox state: a page that never offered the save option reports false, and a save-only session reports true. Treat the paymentMethod value set as open, and handle a value you don't recognize rather than failing on it.

Both values are fixed at submit. They describe the submission now in flight, and nothing re-sends this event, so a customer who changes the checkbox or switches tabs afterward doesn't change what you were told. That lets you record the rail and the save intent straight away instead of holding your record open until the completion webhook arrives.

Digital wallets don't fire this event. Apple Pay, Google Pay, and Paze complete through the wallet sheet rather than the page's own submit, so a wallet payment goes straight to its terminal event. Don't wait on payment_started to move a wallet flow forward: handle payment_succeeded, payment_failed, and payment_pending for those.

What the parent receives on a payment

Payment payloads are restricted to fields the public session endpoint already exposes. No PAN, CVV, expiration, network token, wallet cryptogram, or PII is ever included. A successful payment carries the transaction id, requested and approved amounts (compare them to detect a partial approval), masked last4 and brand, the auth code, and the result status. Token fields (paymentTokenPublicReference, customerId, schemeTransactionId) are present only when the cardholder saved a card; key any card-on-file workflow off the presence of paymentTokenPublicReference.

Locking the amount

A session decides who sets the amount the customer pays. Three choices are available. Customer-entered puts the customer in control: they type the figure themselves, which suits an open balance or an ad-hoc payment. Suggested pre-fills a figure the customer can still change, which suits a suggested donation or a recommended top-up. Locked fixes the figure so the customer can't change it, which suits an invoice or a known order total.

On a locked session, the read-only field is the visible part of the guarantee, not the whole of it. The amount set when the session is created is held with the session on WinkPG's servers, and every submitted payment is checked against it before anything is authorized. A submission whose amount doesn't match the locked value is rejected. Editing the page in a browser, replaying the request with a different figure, and calling the submit endpoint directly all reach the same outcome: the customer pays the amount you set, or nothing is charged. The check doesn't depend on how the customer pays, so card entry and digital wallets such as Apple Pay and Google Pay are all covered.

A locked session fixes the whole payable total, including any tax, shipping, or convenience fee supplied with the session. The clearest setup is a single figure: fold tax and shipping into the locked amount and send one total. The page then shows the customer exactly what will be charged, and one number reconciles against the payment afterward.

Two-way command channel

The parent can send a small, allowlisted set of commands back into the iframe: cancel, set_locale, prefill, and request_height. Inbound commands carry a distinct discriminator (winkpg-hpp-cmd) so an outbound envelope can't be replayed back into the iframe as a command. No programmatic submit exists: payment authorization stays user-initiated.

Hosting in a native app

A native iOS, Android, or Flutter app has no parent page to embed the payment page into. It loads the page in a WebView, which is a top-level document: no parent window, no parent origin, and nothing for the lifecycle events to be posted to. Set hostChannel to NativeWebView when creating the session, and the page delivers the same events to a message handler your app installs instead.

Send no parentOrigin with it. The pair is contradictory, so the request is rejected rather than ignored. You also don't need to send presentationMode: a native session renders the chrome-free surface by default, because the standalone page's full-height layout fights the automatic resizing your app does from the height events.

Your app installs one handler named hppHost. On iOS that's a WKScriptMessageHandler added to the WebView's user content controller, which receives a dictionary. On Android it's addJavascriptInterface with a postMessage(String) method carrying the @JavascriptInterface annotation, which receives a JSON string. Flutter's JavaScript channel gives you the string shape on both platforms. On Flutter for iOS both spellings resolve to the same handler underneath, so the page detects that and delivers each event exactly once: don't install both.

A page configured for Apple Pay or Google Pay still renders those buttons inside a WebView, where the wallet sheet has constraints your app has to satisfy on its own. Restrict the tenders with the allowed_payment_methods session directive until you've done that work: it travels as a prefilledFields entry carrying a comma-separated list of method keys, not as a field of its own, and it can only narrow what the page already offers.

Event names, payloads, and the sensitive-field boundary are identical to the browser channel, so an app can reuse whatever parsing a parent page already had. Two commands are available back into the page, cancel and request_height, sent by calling the global hppHostCommand function with the command envelope as a JSON string. prefill and set_locale aren't offered on this channel. A successful cancel produces a cancelled event whose initiatedBy is "parent", the same value a parent page sees.

The security model changes shape. In a browser the protection is the origin check; a WebView has no origins to compare, so the handler your app installs is the trust boundary. Load only session addresses your own server produced, and don't install the handler on a WebView that can navigate elsewhere. The page's AllowedEmbeddingDomains list and the frame policy that enforces it don't apply, because a WebView load isn't framing. Events carry the session's short token, which is a bearer credential, so never log a whole event: log the type and the transaction id if you need a trail. A page with bot protection enabled runs its challenge inside the WebView, so exercise one while you're still testing in the sandbox.

Security model

A receiver written against the raw addEventListener('message', ...) API must do all three checks before trusting an event:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://hpp.winkpg.com') return;   // (1) origin
  const msg = event.data;
  if (!msg || msg.source !== 'winkpg-hpp') return;          // (2) source discriminator
  if (msg.sessionId !== mySessionId) return;                // (3) session scope
  // handle msg.type
});

Skipping any one of these weakens the model. Prefer the client helper library, which performs these checks and exposes typed on(...) and send(...) APIs.

Quick-start checklist

  • Confirm the HPP page has the embedding feature enabled and add your origin to AllowedEmbeddingDomains.
  • Set parentOrigin to your exact origin on each session create.
  • Embed the iframe using the session response address and serve the parent over HTTPS.
  • Register handlers for at least ready, payment_succeeded, payment_failed, session_invalid, and height_changed.
  • Implement a "no event within N seconds" timeout fallback.
  • Hosting in a native app instead of a browser page? Set hostChannel to NativeWebView, omit parentOrigin, and install the hppHost handler.

See also

  • The embedded payments SDK: the supported browser loader over this protocol, which performs the origin, source, and session checks above and exposes the lifecycle as callbacks.
  • Setting up a hosted payment page: choose a page mode, walk the creation wizard, and configure page options before you get to embedding it.

Reconnecting to the server

Could not reconnect

This session has ended

Attempt 1

Your work on this page is still here. Retrying keeps it; reloading starts the page again.

The server no longer holds this page's state, so it has to be loaded again.