Skip to content

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:

  1. Sagio API — auth, Fasstap config fetch, payment request create/confirm
  2. 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):

SAGIO_API_KEY=sagio_live_...
SAGIO_API_SECRET=...

Mint keys:

cd apps/api
bun run mint-merchant-key -- --email <merchant@example.com>

Merchant prerequisites:

  • Row in merchants + business_profiles
  • Terminal key pair stored in APK at build time

On device: open SettingsWorldline 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 / dev that calls ensureTokenRefreshed() 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 / wrong uniqueId or developerId.

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

  1. Package: io.sagio.merchant
  2. SHA-256 of the keystore used for this APK (debug vs release vs Play — see table above)
  3. Environment: UAT
  4. MUID (FASSTAP_UNIQUE_ID / portal) and company SSO ID (FASSTAP_DEVELOPER_ID)
  5. 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