Skip to main content

Events and monitoring

What an element reports​

A method's own signals reach you through the PaymentElementListener you pass to paymentElement(for:listener:). Every member is defaulted: implement only what your screen reacts to.

@MainActor
final class ItemListener: PaymentElementListener {
func onReady(_ ready: Bool) { model.payEnabled = ready }
func onRequiresAction() { model.leavingAppHint = true }
func onPaymentId(_ paymentId: String) { model.paymentId = paymentId }

func onOutcome(_ outcome: PaymentOutcome) {
switch outcome {
case let .completed(result): model.finish(result)
case .returnedFromRedirect: model.confirmAgainstTheSession() // not an outcome
case .canceled: model.allowRetry()
case let .failed(error): model.show(error)
@unknown default: model.allowRetry()
}
}
}
MemberMeaning
onReady(_:)The item's own step is complete and valid. Gate your Pay button on it, or on element.isReady
onRequiresAction()A 3-D Secure or PSP page is being opened. Informational
onPaymentId(_:)This payment's payment_id, when the flow could read one. Informational
onOutcome(_:)How the attempt ended. The only terminal signal

Everything about a method's availability (a partner client that failed to start, a method the split has outgrown, a filtered method) reaches you as that item's disabled state instead, on the published paymentMethods list (Method states).

Partner events​

Since 1.2.0, onPluginEvent relays what a partner's native step (Apple Pay, PayPal, Klarna, card tokenization) reports, after the checkout has reacted to it. It is the iOS peer of Android's pluginEvents:

checkout.onPluginEvent = { event in
switch event.event {
case let .initFailed(message), let .clientError(message):
log("\(event.itemId) disabled: \(message ?? "")") // now PARTNER_UI_FATAL_ERROR
case let .partnerError(message, recoverable):
log("\(event.itemId) partner error, recoverable: \(recoverable), \(message ?? "")")
case .endOfSession:
model.sessionOver() // sessionState is .terminated
case .ready, .redirect, .cancelled:
break
@unknown default:
break
}
}

The method state has already changed when the callback runs: read it from paymentMethods. The checkout's monitoring event bus stays internal, with no merchant-facing subscription: send your own analytics from the listener, onPluginEvent and the @Published state.

What the SDK reports to Purse​

Monitoring is automatic and needs no setup. It is scoped to one checkout instance, not a process-wide singleton, so two checkouts alive at once never cross their envelopes. The events, names and envelope are the web Headless Checkout's, plus platform: "ios", so iOS journeys appear in the dashboards Purse already runs.

Events are batched (16 events, 10 seconds, or 64 KB) and flushed when the app goes to the background and on teardown(). A failed batch is dropped silently: telemetry never breaks a payment. Nothing is sent when the session carries no API key.

Each message carries the merchant entity, the payment session id, the wallet session id when there is one, the checkout instance id (kept across setSession(_:) and restore(_:)), the SDK version, the environment and the platform.

CodeWhen
HEADLESS_CHECKOUT_CREATE, _READYCreation or restore, with the OS version, the device model and the screen size
HEADLESS_CHECKOUT_SET_SESSION, _EXPIRED_SESSIONA session is set or refreshed (how many methods, how many survived), or expires
HEADLESS_CHECKOUT_SET_WALLET_SESSION, _SET_WALLET_SESSION_FAILEDThe wallet session is attached, or could not be
HEADLESS_CHECKOUT_METHOD_DISABLEDA method enters a disabled state: partner, method, code, whether filtered or recoverable
HEADLESS_CHECKOUT_METHOD_SET_PRIMARY, _PRIMARY_TOKEN_SET_AS_PRIMARY, _CLEAR_PRIMARY_SPLITSelection
HEADLESS_CHECKOUT_METHOD_GET_PAYMENT_ELEMENT, _PRIMARY_TOKEN_GET_PAYMENT_ELEMENTAn element is built for an item
HEADLESS_CHECKOUT_METHOD_GET_SECONDARY_TOKEN, _SECONDARY_TOKEN_TAKE, _SECONDARY_TOKEN_REMOVE_FROM_SPLIT, _SECONDARY_TOKEN_REGISTERGift cards (whether a slice was partial, never its amount)
HEADLESS_CHECKOUT_TOKEN_EDIT, _TOKEN_DELETE, _TOKEN_SET_FAVORITE, _DELETE_ALL_TOKENSWallet management
HEADLESS_CHECKOUT_METHOD_LOAN_SIM, _METHOD_REGISTERBNPL simulation, save decision
HEADLESS_CHECKOUT_PARTNER_UI_EVENTA partner reported a plugin event (its code, partner, method, a capped reason), or a plugin refused the device at creation
HEADLESS_CHECKOUT_PARTNER_API_CALL_START, _PARTNER_API_CALL_ENDAround every Payment API call: method and path, then duration and success
HEADLESS_CHECKOUT_PAYMENT_FULFILLEDThe split starts covering the amount, or stops to
HEADLESS_CHECKOUT_SUBMIT_PAYMENT, _SUBMIT_BLOCKEDA submit entered, or a second one was refused
HEADLESS_CHECKOUT_VALIDATION_STARTED, _VALIDATION_SUCCESS, _VALIDATION_FAILEDAround /validate, with each attempt's statuses
HEADLESS_CHECKOUT_PAYMENT_ERRORA refused /validate or a thrown submit error, with its code and the failing API path and status
HEADLESS_CHECKOUT_ON_BEFORE_VALIDATE_HOOK_FAILED, _CLIENT_HOOK_ERROROne of your hooks threw
HEADLESS_CHECKOUT_PAGE_UNLOADteardown()

CheckoutEventCode carries the web SDK's full vocabulary for parity; the codes above are the ones this SDK emits today.

What never leaves the device​

Not in any event, by construction:

  • card data (the card form belongs to Secure Fields and its values never reach the SDK), form content or shopper identity;
  • raw amounts: events say how many methods, how many splits, or whether a gift card slice was partial, never a figure;
  • transaction ids, PANs and partner payloads: a /validate answer travels as per-attempt statuses (state, code, a capped description, the partner's own status string) and an API error never quotes the response body. A freshly typed gift card's id is deliberately left out of its events, because that id is built from the card's last four digits;
  • a disabled state's message, which can quote amounts: only its code.

Wallet events do carry a saved token's id, which is a Purse wallet identifier and not an instrument number.

No opt-out today

Telemetry is on whenever the session carries an API key. It is scoped to the payment journey and carries no advertising identifier.