Inbound Payments
Besides charges you start with HaloSdkUi.launch, the SDK can be opened by a payment that arrives from outside your app — another app handing over a transaction, a halo:// link, an App Link, or a scanned QR code.
These are handled entirely by the SDK. Its own activity owns the intent filters, restores the branding and language cached by HaloSdkUi.prepare, brings the SDK up, and drives the transaction. No host code runs, and on a Flutter host no Flutter engine is started.
Because an inbound payment can arrive on a cold start, the appearance it paints itself with is the one cached by prepare. A host that never calls prepare gets Halo defaults on that path.
The Three Doors
| Door | Shape | What identifies it |
|---|---|---|
| App-to-app | Intent with transaction_id + jwt extras | Action za.co.synthesis.halo.transaction |
| Custom scheme | halo://pay?amount=…¤cy=…&merchantReference=… | Your scheme — see Build Configuration |
| App Link | https://<your-domain>/… | Your App Link host — see Build Configuration |
A URL comes in two shapes. The long form describes the payment outright — …?id=…&amount=…&configJwt=eyJ… — and the SDK has everything it needs from the URL alone. The short form is a pointer — https://<domain>/<reference> with nothing chargeable in the query — standing for a payment held server-side, which the SDK resolves against the Halo kernel before it brings itself up.
Query parameters accept both spellings a link may carry: merchantReference or reference, transactionId / id / uuid, and configJwt or jwt.
The old halompos.page.link filter is gone. FDL was shut down in August 2025, nothing in the SDK resolves a short link into its payment payload, and a bare page.link URL carries no query to read — so the filter only ever offered the app in a chooser for a link it could not act on.
Setup
Your URL scheme and App Link domain are set at build time, since both live in the manifest — read before any of your code runs, so no HDConfig call could set them. See Build Configuration for halo_url_scheme and halo_applink_host.
One more thing is set at runtime, in HDConfig:
Your kernel, for short links
A short App Link carries no configJwt, and resolving it is what produces the token — so there is no credential to take the kernel's address from, and the host has to name it. Every other call still reads its kernel off the credential it presents.
val config = HDConfig(
// ... other props
kernel = "kernelserver.qa.haloplus.io",
kernelPins = setOf("sha256/CNOtjib4NAlSqDZDY5aknDcVbcfLEWBgnGl/dgec4aA="),
)
kernelPins are SHA-256 SPKI fingerprints of that kernel's TLS certificate — the same values an aud_fingerprints claim carries. More than one is normal, so a certificate rotation cannot brick the SDK. Leaving it empty runs the link resolve unpinned, which is logged; anything authorised by a JWT is pinned by that JWT regardless.
Leave kernel null if your links always carry their own JWT.
If a payment link reaches your own app's activity instead of the SDK's, nothing here matched it — check the host and scheme against the table above rather than routing it by hand. The query carries a live configJwt, and anything that renders an unmatched URL renders that credential along with it.
Presentation
An inbound payment arrives with no launch call to say how it wants to be presented, so it uses HDConfig.presentation. A brand that presents its own charges as a sheet should set it, so a payment looks the same however it arrived.
DebiCheck Mandates
A DebiCheck mandate — the scheme's TT3 — is a debit order the payer authorises by tapping their card, rather than a payment. It arrives through the doors above and needs nothing extra from you: the SDK recognises it, runs the same tap flow, and charges it with the kernel's TT3 call.
| Entry point | How it says it is a mandate |
|---|---|
| App-to-app | is_tap extra set to false |
| URL / App Link | type=TT3 in the query |
Whatever the intent or link carries, a mandate with a transaction id is always read back from the kernel before it is charged: the account and identity number it is registered against are the kernel's, and the charge is presented with that record's paymentJwt rather than the link's own token.
What the payer sees. The tap screen leads with the max collection amount — the ceiling the debit order may collect up to, not a sum being taken now, and also what the kernel authorises against and what the PIN pad shows. Under it sit the four things that identify the order: the debit order day, the account it will be collected from, the creditor's description and the contract reference — so the payer can recognise what they are agreeing to. The DebiCheck mark is shown alongside the accepted-scheme marks.
An approved mandate reports Mandate Approved with those same details, rather than a card receipt: nothing has been collected yet, so there is no card, scheme or authorisation code to show.
The Fourth Door
A payment sent to a named device — no link, no QR, nothing for the payer to scan — arrives through a different door entirely: see Push to Terminal.