Merchant Terminal: Card Payments & Attestation Setup¶
Audience: Engineers and partners integrating the Sagio terminal (apps/terminal)
As of: 2026-05-21
Related: merchant-terminal-attestation-investigation.md (full investigation log), merchant-terminal-launch.md, 003-fasstap-configuration.md
Short answer¶
SAGIO API credentials alone are not enough for card tap. Card payments use two layers:
- Sagio API — auth, Fasstap config fetch, payment request create/confirm
- Soft Space / Worldline (Fasstap SDK) — attestation, key loading, NFC authorization
If you see “attestation failed” when charging a card, the failure is almost always in layer 2 (Worldline / Play Integrity), not missing SAGIO_API_KEY.
What happens when you tap Card → Charge¶
Terminal (apps/terminal)
→ POST /merchant-app/auth (SAGIO_API_KEY + SAGIO_API_SECRET in APK)
→ GET /merchant-app/fasspay-config (Bearer token → 11 SDK fields)
→ POST /payment-requests (WORLDLINE rail)
→ Soft Space SDK init + refreshToken (attestation → mpos-uat.fasspay.com:9001)
→ Soft Space startTransaction (NFC tap)
→ POST /payment-requests/:txnId/confirm-worldline
Attestation runs inside the SDK’s refreshToken() call to Soft Space’s attestation host (https://mpos-uat.fasspay.com:9001/v2 in UAT). That happens after the Sagio payment request is created. A failure labeled “attestation” therefore usually means Worldline / device trust configuration—not a bad Sagio API key.
Code path: PaymentViewModel creates the payment request first, then FasstapManager.initialize() and ensureTokenRefreshed() before NFC (apps/terminal/app/src/main/java/io/sagio/merchant/viewmodel/PaymentViewModel.kt).
Configuration beyond Sagio API credentials¶
1. Fasstap secrets on the API worker (required)¶
These must exist on the deployed Cloudflare Worker (api.sagio.io), not only in local apps/api/.env.local:
| Env var | Source |
|---|---|
FASSTAP_ACCESS_KEY |
Soft Space integrator onboarding |
FASSTAP_SECRET_KEY |
Same |
FASSTAP_UNIQUE_ID |
Soft Space merchant portal MUID (per onboarded user) |
FASSTAP_DEVELOPER_ID |
Soft Space merchant portal company SSO identifier |
FASSTAP_ENVIRONMENT |
UAT (or PROD when live) |
Deploy via Wrangler (Cloudflare account nzsagio@gmail.com):
cd apps/api
bun run deploy:fasstap-secrets -- .env.local # sagio-api-prod
bun run deploy:fasstap-secrets -- .env.local sagio-api-dev
Verified on prod 2026-05-22: fasspay-config returns success: true with all required Fasstap fields.
Smoke test after POST /merchant-app/auth:
TOKEN="<accessToken from auth>"
curl -sS "https://api.sagio.io/api/merchant-app/fasspay-config" \
-H "Authorization: Bearer $TOKEN"
# Expect: success true, environment UAT, all credential fields populated
Baked into the API for UAT (merchants do not configure these in the app): attestation host, keyloading host, cert pinning, keyloading CA cert, Google Play project number 757874674469. See apps/api/src/config/index.ts and docs/specs/tap2pay/003-fasstap-configuration.md.
2. Terminal APK build (required)¶
In apps/terminal/local.properties (gitignored):
Mint keys:
Merchant prerequisites:
- Row in
merchants+business_profiles - Terminal key pair stored in APK at build time
On device: open Settings → Worldline SDK credentials should read READY and Environment: UAT. If MISSING, tap Refresh SDK Credentials (calls GET /merchant-app/fasspay-config).
| Signal | Meaning |
|---|---|
| Settings READY | API returned accessKey, secretKey, uniqueId, developerId |
| Settings MISSING | Auth failed, worker secrets missing, or network error |
Logcat SDK initialized: version=3.6.5.0 |
SSMPOSSDK.init() succeeded |
| Attestation / refresh fails after READY | Play Integrity, cert whitelist, or wrong MUID/SSO |
READY does not guarantee attestation success—only that Sagio returned the four core Soft Space fields.
3. Soft Space device whitelist (common attestation blocker)¶
Even when Settings show READY, attestation can fail if Soft Space has not whitelisted:
| Item | Value |
|---|---|
| Android package | io.sagio.merchant |
| Signing cert SHA-256 | Your debug or release keystore fingerprint |
| Play Integrity project | Soft Space’s 757874674469 |
Debug keystore fingerprint:
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android
Send the SHA-256 fingerprint to Soft Space (Worldline NZ) for whitelist. Without it, the SDK may fail on device even when credentials show READY.
4. Device and runtime (required for card)¶
- NFC-capable Android device (minSdk 29), NFC enabled
- Grant runtime permissions when the first Card charge prompts (location, etc.)
- Google Play services available (Play Integrity)
- Use a current APK from
master/devthat callsensureTokenRefreshed()before tap (fixes “Missing parameters, please perform refresh token”)
5. Optional / per-merchant¶
| Item | When needed |
|---|---|
merchants.fasstap_unique_id / fasstap_developer_id |
Portal MUID/SSO differ from global env vars |
| Wallet on merchant profile | SAGIO QR rail only—not required for card |
FASSTAP_SSO_ID on API |
Backend Worldline REST calls only—not sent to terminal |
Quick triage checklist¶
| Check | Expected |
|---|---|
POST /merchant-app/auth with terminal key |
success: true |
GET /merchant-app/fasspay-config |
success: true, not “configuration missing: …” |
| Settings: Worldline READY, env UAT | Yes |
| Logcat filter | adb logcat \| rg -i "FasstapManager\|attestation\|refreshToken\|SDK initialized" |
Interpretation:
- Failure before SDK “Initializing” / tap UI → Sagio API (auth, merchant row, payment request validation).
- Failure during refresh after
SDK initialized→ attestation / key load / Play Integrity / cert whitelist / wronguniqueIdordeveloperId.
UAT reference values (team .env.local)¶
Rotate if Soft Space reissues keys. Do not commit secrets.
FASSTAP_ACCESS_KEY=<from Soft Space>
FASSTAP_SECRET_KEY=<from Soft Space>
FASSTAP_UNIQUE_ID=nydtubnqssautomationtester4 # MUID
FASSTAP_DEVELOPER_ID=yjOItT3r3MKqRZE # company SSO ID
FASSTAP_SSO_ID=yjOItT3r3MKqRZE
FASSTAP_ENVIRONMENT=UAT
Portal mapping: uniqueId = user MUID, developerId = company SSO identifier (see 003-fasstap-configuration.md).
Settings READY but attestation still fails¶
This is the expected failure mode when Sagio env vars and /fasspay-config are correct but device trust is not. READY only means the API returned accessKey, secretKey, uniqueId, and developerId — see MerchantConfig.isValid() in the Android app. It does not mean Play Integrity passed or Soft Space accepted the device.
Attestation runs on the phone during SSMPOSSDK refreshToken() (after payment request create, before NFC). The SDK talks to mpos-uat.fasspay.com:9001 and uses Play Integrity against Soft Space GCP project 757874674469.
Fix order (most likely first)¶
1. Signing certificate whitelist (most common)¶
Soft Space must whitelist both:
| Item | Value |
|---|---|
| Package name | io.sagio.merchant |
| APK signing cert SHA-256 | Fingerprint of the exact keystore used to build the installed APK |
Critical: the fingerprint must match how the APK was signed, not “any Sagio cert”:
| How APK was built | SHA-256 to send Soft Space |
|---|---|
Local ./gradlew assembleDebug |
Android debug keystore (~/.android/debug.keystore) |
CI / assembleRelease with keystore.properties |
That upload keystore |
| APK from Google Play (internal / production) | Play Console → App signing key certificate (not upload key) |
See apps/terminal/docs/SIGNING.md. If Chris installed a debug APK but Soft Space only whitelisted a release or Play signing cert, attestation fails with READY.
# Debug APK (typical UAT)
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android | rg "SHA256:"
# Release / upload keystore
keytool -list -v -keystore /path/to/upload.keystore -alias io.sagio.merchant | rg "SHA256:"
Email Soft Space: package io.sagio.merchant + SHA-256 + “UAT Play Integrity project 757874674469”. Ask them to confirm whitelist is active for that fingerprint.
2. Wrong MUID / developer ID for this merchant¶
Global env FASSTAP_UNIQUE_ID / FASSTAP_DEVELOPER_ID must match the Soft Space portal row for the user/company being tested. A valid Sagio fetch can still fail attestation if the MUID is for a different portal user than the one Soft Space expects for that device session.
- Check portal MUID (user) and company SSO ID (company).
- Optional per-merchant override:
merchants.fasstap_unique_id,merchants.fasstap_developer_id. - Re-fetch: Settings → Refresh SDK Credentials.
3. Device / environment blocks Play Integrity¶
| Check | Action |
|---|---|
| Google Play services | Installed and up to date |
| Physical device | Prefer real phone; emulators often fail (EMULATOR_DETECTED / PLAY_INTEGRITY_FAILED) |
| Root / debug hooks | Avoid rooted devices; detach USB debugging during tap if failures persist |
| Network | Device must reach https://mpos-uat.fasspay.com:9001 (no corporate firewall blocking :9001) |
4. Confirm SDK got hosts/pinning (not only the four creds)¶
READY does not prove attestationHost, cert pinning, CA cert, or googlePlayProjectNumber were stored. After Refresh, confirm via logcat on charge:
adb logcat -c
# Tap Card → Charge once
adb logcat -d | rg -i "FasstapManager|SDK initialized|Token refresh|attestation|integrity|statusMessage|SSMPOSSDK"
| Log pattern | Meaning |
|---|---|
SDK initialized: version=3.6.5.0 then refresh fails |
Attestation / Play Integrity / whitelist (not Sagio API) |
SDK initialized never appears |
Init failed — check empty attestationHost or bad creds |
Token refresh failed + statusMessage |
Paste full message to Soft Space |
| SSL / pinning errors | Cert pin mismatch — rare if using API UAT defaults unmodified |
Optional API check — response should include non-empty host and googlePlayProjectNumber:
curl -sS "https://api.sagio.io/api/merchant-app/fasspay-config" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {attestationHost, googlePlayProjectNumber, environment, uniqueId}'
5. APK age¶
Rebuild from current master / dev so ensureTokenRefreshed() runs before tap. Older builds show “Missing parameters, please perform refresh token” instead of a clear attestation error.
What to send Soft Space in one email¶
- Package:
io.sagio.merchant - SHA-256 of the keystore used for this APK (debug vs release vs Play — see table above)
- Environment: UAT
- MUID (
FASSTAP_UNIQUE_ID/ portal) and company SSO ID (FASSTAP_DEVELOPER_ID) - Logcat snippet from failed charge: lines containing
FasstapManager,refreshToken,statusMessage,integrity
Sagio cannot fix Play Integrity whitelist from the API side once env vars and fasspay-config succeed.
Troubleshooting (attestation-specific)¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Settings MISSING on prod | Fasstap secrets not on Cloudflare worker | deploy:fasstap-secrets with Sagio Wrangler account |
fasspay-config configuration missing: accessKey, … |
Same | Deploy four required secrets |
| Settings READY, attestation fails | Play Integrity / signing cert / wrong MUID | Section above — whitelist SHA-256 for installed APK |
| “perform refresh token” on charge | refreshToken() not called before tap |
Rebuild APK with ensureTokenRefreshed() |
| Payment request 500 | API/DB issue | Check recipient_address, merchant row (see launch handoff) |
| Charge disabled with amount | NFC off or old APK canCharge logic |
Enable NFC; update APK |
Related docs¶
- softspace-play-integrity-whitelist.md — HSM app signing key, debug vs upload vs Play cert, Soft Space email template
- merchant-terminal-launch.md — full launch checklist and E2E tests
- 003-fasstap-configuration.md — 11 SDK fields and API contract
- 004-fasstap-visual-architecture.md — sequence diagrams
apps/api/.env.example— Fasstap env var reference