Error codes
Every PurseError carries one PurseErrorCode. PurseErrorCode declares 33 codes, the same set as the Android SDK. The names match the web SDK's error codes exactly, and error.documentationLink points at the matching anchor of that page. This page lists what each code means on iOS and the action expected from your app.
PurseError is a protocol with five conforming types: PurseSessionError (decoding the session), PurseCheckoutError (the checkout's own rules), PartnerUIError (a partner's native step), PaymentAPIError (HTTP and transport) and, in the Klarna product, KlarnaAuthorizationDeclined. All five expose code, message, underlying and documentationLink. Several PurseCheckoutError cases take an explicit PurseErrorCode, so one case covers several codes.
Session
| Code | Case | Meaning | Expected action |
|---|---|---|---|
SESSION_MALFORMED | PurseSessionError.sessionMalformed | The widgetData string is not a client session, or an unknown shape | A bug in the hand-over from your backend. Do not show a checkout; log and fix |
EXPIRED_SESSION | PurseCheckoutError.sessionExpired | The session expired, or setSession was handed an expired one. sessionState is .expired | Mint a new client session and call setSession(_:) |
SUBMIT_BLOCKED | PurseCheckoutError.submitBlocked | A submit is in flight, or the session already paid, was declined, expired (once the expiry watchdog has set .expired) or terminated. state says which. Native only | Do nothing on .submitting; otherwise confirm the payment status on your backend and move on |
CHECKOUT_TORN_DOWN | PurseCheckoutError.checkoutTornDown | A call that touches the network or changes the session after teardown(). Native only | A lifecycle bug: the checkout outlived its screen. Build a new one |
Payment
| Code | Case | Meaning | Expected action |
|---|---|---|---|
PAYMENT_NOT_FULFILLED_ON_SUBMIT | PurseCheckoutError.paymentNotFulfilled | The split does not cover the amount, or no primary is set | Guard your Pay button with isPaymentFulfilled; highlight the missing amount |
ELEMENT_NOT_FULFILLED | PurseCheckoutError.itemNotFulfilled | A form in the split is incomplete or invalid (the card form, a CVV-only field) | Same guard; focus the form, its fields show their own errors |
METHOD_DISABLED | PurseCheckoutError.methodDisabled | setPrimary or setPrimaryToken on a disabled item, or paymentElement(for:) on a technically disabled one. disabled says why | Render the item as disabled from its disabled state; see Method states |
METHOD_NOT_IMPLEMENTED | PurseCheckoutError.noPluginRegistered, .noSecondaryConfig | No plugin for the chosen method (usually a partner module that was never installed), or no config for a gift card issuer | PluginRegistry.shared.install(…), or disabledMethods; register a SecondaryConfig |
ON_BEFORE_VALIDATE_HOOK_FAILED | PurseCheckoutError.hookFailed(hook: "onBeforeValidate", …) | Your onBeforeValidate threw; nothing was sent, the split is intact | Your own failure is underlying; fix or show it, the shopper can submit again |
POST_VALIDATE_FAILED | PurseCheckoutError.hookFailed(hook: "onRedirection", …), .postValidationFailed | Your onRedirection threw and the redirect was not opened; or /validate answered and a plugin's postValidation then threw | For the hook: /validate already answered, so the session is spent and a new submit throws SUBMIT_BLOCKED; confirm the payment status on your backend. For postValidationFailed: the payment is real: result holds /validate's answer and the session is spent |
VALIDATE_FAILED | PurseCheckoutError.validateFailed | The /validate call itself failed. The transport error is kept as underlying | If the chain holds a PaymentAPIError with outcomeUnknown, confirm the payment status on your backend before showing anything; else show an error |
PARTNER_UI_ERROR | PartnerUIError, and KlarnaAuthorizationDeclined | A partner's native step failed: its SDK errored, a tokenization was refused, a coordinator was driven before it was prepared, a module is missing what only your app can give it. The partner's error is underlying. Native only | Show an error for this method and let the shopper pick another |
UI_VALIDATION_FAILED_ERROR | PurseCheckoutError.hookFailed can carry it | Declared for parity with the web SDK; no iOS code path raises it |
Gift cards (secondary methods)
| Code | Case | Meaning | Expected action |
|---|---|---|---|
SECONDARY_METHOD_WRONG_PAN_FORMAT | PurseCheckoutError.invalidSecondaryCard | The card number does not match the issuer's format | Validate with secondaryConfig(for:)'s validatePAN first; show a field error |
SECONDARY_METHOD_WRONG_CVV_FORMAT | .invalidSecondaryCard | The CVV does not match the issuer's format | Same with validateCVV |
SECONDARY_METHOD_REQUIRED_CVV | .invalidSecondaryCard | This card needs a CVV and none was given | Ask for it when the config's requiresCVV(pan) is true |
FAILED_TO_INIT_SECONDARY_TOKEN | PurseCheckoutError.secondaryTokenizationFailed | The issuer refused the card | Show "card not accepted"; let the shopper correct it |
SECONDARY_TOKEN_EMPTY_BALANCE | .secondaryTokenizationFailed | The card has no balance, or a saved card's balance is not resolved yet | Show the empty balance; for a saved card wait for balanceResolved |
INVALID_TAKE_AMOUNT | PurseCheckoutError.invalidTake | applySecondary(_:amount:) with a negative amount, above the balance or above what is left | Clamp the slice to min(balance, remainingAmountToPay) |
MAX_AMOUNT_REACHED | .invalidTake | The slice exceeds the issuer's maximum per payment | Clamp to the method's maxAmount |
USAGE_LIMIT_ERROR | .invalidTake | The issuer's per-session usage limit is spent by the cards already in the split | Tell the shopper no more cards of this issuer can be added |
Wallet and saved instruments
| Code | Case | Meaning | Expected action |
|---|---|---|---|
MISSING_WALLET_SESSION | PurseCheckoutError.walletOperationFailed | Saving, renaming, favoriting or deleting needs a wallet session the client session does not carry | Hide wallet features, or create the client session with a wallet session |
MISSING_TOKEN | .walletOperationFailed | The token is not in the wallet anymore | Refresh the list from paymentTokens |
INVALID_TOKEN_NAME | .walletOperationFailed | The label does not match ^[a-zA-Z0-9\s]{1,25}$ | Show a field error |
TOKEN_NAME_EXISTS | .walletOperationFailed | Another saved instrument carries that label | Show a field error |
TOKEN_NOT_REGISTERABLE | .walletOperationFailed | registerSecondary on a gift card the issuer does not let you keep, or on one that is already saved | Check canBeRegistered before offering to save |
METHOD_NOT_REGISTERABLE | .walletOperationFailed | register on a card method whose save_token_mode forbids saving | Check canBeRegistered before offering to save |
SET_WALLET_SESSION_FAILED, FAILED_TO_DELETE_ALL_TOKENS | .walletOperationFailed can carry either | Declared for parity with the web SDK; no iOS code path raises them. A failing wallet call surfaces as API_ERROR, and a wallet session that could not be attached is reported to monitoring only |
Instalment simulation
| Code | Case | Meaning | Expected action |
|---|---|---|---|
METHOD_IS_NOT_SIMULABLE | PurseCheckoutError.methodNotSimulable | simulateLoan on a method without the simulable label | Offer the simulation only when method.simulable |
INVALID_LOAN_SIMULATION | PurseCheckoutError.loanSimulationFailed | The simulation API refused the request or returned no usable schedule | Hide the schedule; the method stays payable |
Transport
| Code | Case | Meaning | Expected action |
|---|---|---|---|
API_ERROR | PaymentAPIError | A Payment API call failed: transport failure (status == nil), non-2xx/3xx status, or an unusable response. errorCode is the API's own reason, path the request, requestSent whether any byte left | isSessionTerminal: new session. isRetryable: offer a retry. outcomeUnknown: confirm, never report as failed. Otherwise show an error |
UNKNOWN_ERROR | Declared for parity with the web SDK. This SDK never raises an untyped error, and a meta-test in the repository keeps it the only uncovered code |
Codes that exist only on the web
BRAND_NOT_BELONGS_TO_DETECTED, ELEMENT_RENDER_FAILED, PAYMENT_TOKEN_NOT_FOUND_IN_SESSION, INVALID_INIT_PARAMS, FAILED_TO_ADD_TOKEN and TOKEN_ALREADY_EXISTS describe situations that cannot happen with a native element, and are not declared in PurseErrorCode at all. SUBMIT_BLOCKED, CHECKOUT_TORN_DOWN and PARTNER_UI_ERROR go the other way: the web SDK only logs them, or has no lifecycle end to report; both native SDKs raise them.