Handling the response

What the wallet sends back to your app, how to parse it, and the trust model for treating it as a payment result.

When the customer finishes in the Scan to Pay wallet, it returns to your app by opening your registered URL scheme with status information in the query string. This page documents every value that can come back, how to parse it, and what to do with it.

The platform-specific mechanics of receiving the return are on App-to-app on Android and App-to-app on iOS. This page is about what to do once you've got the URL.


Return URL format

your.app.scheme://your.host?status={STATUS}&ref={REFERENCE}
Query parameterAlways presentDescription
statusOne of the values below
refWhen supplied by the walletThe wallet's transaction reference. Treat it as correlation data only and verify the outcome through your backend.

Status values

StatusMeaningRecommended action
SUCCESSThe customer finished the flow and the wallet reported success. This is not yet proof of payment.Move UI to "Confirming…" and verify via backend before treating the order as paid.
CANCELEDThe customer cancelled in the wallet (backed out before authorising).Treat as abandoned. Allow the customer to retry.
FAILEDSomething went wrong in the wallet flow itself — for example a validation or network problem.Show a friendly error and let the customer retry. Use the authoritative backend outcome for the final reason.

The trust model — read this once, code accordingly

⚠️

The return URL is a UX trigger, not a payment confirmation.

Any app on the customer's device can construct and call your URL scheme. Nothing in the URL is signed or encrypted. If you treat the return URL as proof of payment, an attacker can defraud you trivially by:

  1. Initiating a real payment for a small amount.
  2. Recording the success URL.
  3. Cancelling at the bank.
  4. Manually invoking your.app.scheme://?status=SUCCESS&ref={forged-reference} on their device.

To you, this looks identical to a successful payment.

The correct pattern

  1. Wallet returns to your app with status=SUCCESS.
  2. Your UI transitions to "Confirming your payment…" — don't release goods yet.
  3. Your app polls your backend for the order's authoritative status.
  4. Your backend has already received (or shortly will receive) the encrypted webhook from Scan to Pay with the real outcome. See Webhooks.
  5. Once your backend has decrypted and validated the webhook, return the result to your app.
  6. Your app shows the success page only then.

The webhook is signed via merchant-key encryption and is impossible to forge without the key. That's the proof.


Parsing the URL

Android (Kotlin)

val data = intent.data ?: return
val status = data.getQueryParameter("status")
val ref    = data.getQueryParameter("ref")

when (status) {
    "SUCCESS"  -> verifyOrderWithBackend(ref)
    "CANCELED" -> showAbandonedScreen()
    "FAILED"   -> showFailureScreen()
    else       -> showGenericErrorScreen()
}

iOS (Swift)

guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
      let queryItems = components.queryItems else { return }

let status = queryItems.first { $0.name == "status" }?.value
let ref    = queryItems.first { $0.name == "ref" }?.value

switch status {
case "SUCCESS":   verifyOrderWithBackend(ref)
case "CANCELED":  showAbandonedScreen()
case "FAILED":    showFailureScreen()
default:          showGenericErrorScreen()
}

Backend verification

When your app receives SUCCESS, it should poll your backend for the order's status. Your backend in turn relies on the webhook:

App         Your backend        Scan to Pay
 │                │                  │
 │  status req    │                  │
 ├───────────────►│                  │
 │                │                  │
 │  pending       │  (webhook        │
 │◄───────────────┤   not yet        │
 │                │   received)      │
 │                │                  │
 │                │   webhook (CB)   │
 │                │◄─────────────────┤
 │                │   ack 200        │
 │                ├─────────────────►│
 │                │                  │
 │  status req    │                  │
 ├───────────────►│                  │
 │  succeeded     │                  │
 │◄───────────────┤                  │
 │                │                  │
 │  show success  │                  │

Most webhooks arrive within 2–5 seconds of the customer authorising. Poll your backend every 1–2 seconds for up to 30 seconds. If you don't get a definitive answer in that window, fall back to a "we'll email you when it's confirmed" UX rather than guessing.

For the full webhook lifecycle, see Webhooks.


Handling slow or missed returns

The customer's phone might not return to your app — battery dies mid-flow, they hit the home button, the wallet app crashes. In any of these cases:

  • Your backend still receives the webhook (the wallet completes the transaction independently of returning).
  • Your app gets stuck on "Awaiting payment…"

Mitigations:

  • Server-push to your app (WebSocket, Apple Push, Firebase Cloud Messaging) the moment your backend gets the webhook. This way the customer's order updates even if they never re-open your app.
  • Reconcile on next app launch — when the customer eventually opens your app, query your backend for any in-flight orders and update the UI.
  • Timeout the UI after ~2 minutes of waiting and direct the customer to check their wallet or contact support.

What's next


Did this page help you?