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()
}
}
}
| Member | Meaning |
|---|---|
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.
| Code | When |
|---|---|
HEADLESS_CHECKOUT_CREATE, _READY | Creation or restore, with the OS version, the device model and the screen size |
HEADLESS_CHECKOUT_SET_SESSION, _EXPIRED_SESSION | A session is set or refreshed (how many methods, how many survived), or expires |
HEADLESS_CHECKOUT_SET_WALLET_SESSION, _SET_WALLET_SESSION_FAILED | The wallet session is attached, or could not be |
HEADLESS_CHECKOUT_METHOD_DISABLED | A method enters a disabled state: partner, method, code, whether filtered or recoverable |
HEADLESS_CHECKOUT_METHOD_SET_PRIMARY, _PRIMARY_TOKEN_SET_AS_PRIMARY, _CLEAR_PRIMARY_SPLIT | Selection |
HEADLESS_CHECKOUT_METHOD_GET_PAYMENT_ELEMENT, _PRIMARY_TOKEN_GET_PAYMENT_ELEMENT | An element is built for an item |
HEADLESS_CHECKOUT_METHOD_GET_SECONDARY_TOKEN, _SECONDARY_TOKEN_TAKE, _SECONDARY_TOKEN_REMOVE_FROM_SPLIT, _SECONDARY_TOKEN_REGISTER | Gift cards (whether a slice was partial, never its amount) |
HEADLESS_CHECKOUT_TOKEN_EDIT, _TOKEN_DELETE, _TOKEN_SET_FAVORITE, _DELETE_ALL_TOKENS | Wallet management |
HEADLESS_CHECKOUT_METHOD_LOAN_SIM, _METHOD_REGISTER | BNPL simulation, save decision |
HEADLESS_CHECKOUT_PARTNER_UI_EVENT | A 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_END | Around every Payment API call: method and path, then duration and success |
HEADLESS_CHECKOUT_PAYMENT_FULFILLED | The split starts covering the amount, or stops to |
HEADLESS_CHECKOUT_SUBMIT_PAYMENT, _SUBMIT_BLOCKED | A submit entered, or a second one was refused |
HEADLESS_CHECKOUT_VALIDATION_STARTED, _VALIDATION_SUCCESS, _VALIDATION_FAILED | Around /validate, with each attempt's statuses |
HEADLESS_CHECKOUT_PAYMENT_ERROR | A 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_ERROR | One of your hooks threw |
HEADLESS_CHECKOUT_PAGE_UNLOAD | teardown() |
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
/validateanswer 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.
Telemetry is on whenever the session carries an API key. It is scoped to the payment journey and carries no advertising identifier.