Skip to main content

Error handling

Every SDK exception extends PurseException and carries a stable code (PurseErrorCode), shared with the web SDK's error codes. Write your recovery logic on the code, never on the message. The complete list, code by code with the expected reaction, is the Error codes reference; a method that is listed but cannot be used is not an error but a method state.

Where errors surface​

PathSurface
element.submit()PaymentOutcome.Failed(error) on the element's listener. Most integrations only need this
checkout.submitPayment() and every other suspend functionThe exception is thrown
setPrimary, applySecondary, register, … (synchronous)The exception is thrown
A partner SDK's own failurePartnerUiException (PARTNER_UI_ERROR), the partner's exception as its cause
A rendering that cannot happen (technically disabled method)The element has no UI; submit() reports Failed(MethodDisabled)

Handle a failed outcome​

override fun onOutcome(outcome: PaymentOutcome) {
if (outcome !is PaymentOutcome.Failed) return
when (val error = outcome.error) {
is PurseCheckoutException.SessionExpired -> newSession()
is PurseCheckoutException.SubmitBlocked -> Unit // already submitting, or spent
is PurseCheckoutException.ValidateFailed ->
if (error.outcomeUnknown) showProcessing() else showError(error.code) // unknown: ask your backend
is PaymentApiException ->
if (error.isSessionTerminal) newSession() else if (error.isRetryable) offerRetry() else showError(error.code)
is PartnerUiException -> showError(error.code) // the partner's step failed; pick another method
else -> showError(error.code)
}
}

Catch by family​

When you call the checkout directly:

try {
checkout.submitPayment() // suspend, runs its network call on Dispatchers.IO
} catch (e: PurseCheckoutException) { // checkout logic
when (e.code) {
PurseErrorCode.PAYMENT_NOT_FULFILLED_ON_SUBMIT -> highlightMissingAmount()
PurseErrorCode.ELEMENT_NOT_FULFILLED -> focusInvalidForm()
PurseErrorCode.EXPIRED_SESSION -> newSession()
else -> showError(e.code)
}
} catch (e: PaymentApiException) { // transport / HTTP
if (e.isSessionTerminal) newSession() else if (e.isRetryable) offerRetry() else showError(e.code)
}

PaymentApiException carries the HTTP status (null for a transport failure), the API's own errorCode (SESSION_EXPIRED, …) and the request path. isSessionTerminal says the session is spent for good; isRetryable says the same call may succeed (a 5xx or a transport failure, never a 4xx).

Unknown outcome​

A ValidateFailed whose outcomeUnknown is true means the /validate request left the device and no answer came back. The payment may exist server-side: do not tell the shopper it failed. Show a processing state and get the final status from your backend, through webhooks or Get a client session.

A return from a redirect after Android killed the app lands on PaymentOutcome.ReturnedFromRedirect, never on Canceled: the same backend check applies.

A declined payment is not an error​

PaymentOutcome.Completed with a status other than SUCCESS is the API's verdict on a payment that went through the whole flow: the issuer declined, the partner refused. Show the shopper a decline message and let them pick another method. Nothing is thrown and no Failed is reported. The session is spent: sessionState is ERROR and a new submit() throws SubmitBlocked. A retry needs a new client session.

A method is listed but cannot be used​

The method's disabled state says why and what to do, and moves as the split moves. See Method states for the codes, the five behaviours and the reaction to each.

A missing partner artifact​

If the session offers Google Pay, PayPal or Klarna and the matching artifact is not on the classpath, the SDK logs a warning at init naming the dependency, the method's element has no UI, and submit() reports a Failed(NoPluginRegistered) (METHOD_NOT_IMPLEMENTED) whose message reads:

no plugin registered for paypal/wallet — add the `eu.purse:headless-checkout-paypal` dependency to offer this method

Add the artifact, or pass the method in init(disabledMethods = …): Payment methods.

Do not retry /init yourself

Transient /init failures are retried by the SDK. Add no retry loop of your own; react to the resulting disabled state instead.