Onboarding & KYC
First launch: sign in with native Apple or Google OAuth, choose home currency, complete Infinia HOSTED KYC. All money-moving features gate on kyc.status === "completed".
Sequence
Payload detail for the Infinia calls:
POST /v1/accounts/owners→{ type: "INDIVIDUAL", kyc_mode: "HOSTED" }POST /v1/accounts→{ owner_id, country: "GB", currency: "GBP", products: ["PAYINS", "PAYOUTS", "INTERNAL_TRANSFER"] }(EUR variant usescountry: "EU",currency: "EUR")- Owner status poll →
GET /v1/accounts/owners/{uuid}/
Screens
apps/mobile/app/login.tsx— Apple + Google buttons; single tap flow.apps/mobile/app/onboarding/home-currency.tsx— GBP / EUR picker, one-time choice.apps/mobile/app/onboarding/kyc.tsx— kicks off KYC start, opens the Infinia widget, polls status.apps/mobile/app/_layout.tsx—AuthGatechooses which screen to show based onsessionToken → user.home_currency → kyc.status → account.statusstate.
What each side owns
| Concern | Owner |
|---|---|
| Auth session | GrinGo (session JWT we sign, verified per-request) |
| OAuth identity verification | Apple / Google (their JWKS, our JWT verification) |
| KYC documents & selfies | Infinia (via SumSub under the hood — the HOSTED widget) |
| Verified identity storage | Infinia + GrinGo (we store a copy on kyc_verifications for portability) |
| Virtual account provisioning | Infinia (with our POST /v1/accounts orchestration) |
| Funding instructions display | GrinGo mobile (from provider_accounts.funding_instructions) |
Country allowlist (MVP)
MVP is restricted to UK + EU-27 senders. Enforcement is a post-KYC check: after Infinia's HOSTED widget completes and returns verified_identity.individual.address.country, we compare against the allowlist:
const MVP_COUNTRY_ALLOWLIST = [
"GB",
// EU-27:
"AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE",
"IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE",
];
Flow when a user's verified country is not on the allowlist:
kyc_verifications.statusis set tocountry_not_supported(a terminal state distinct fromfailed).- No
provider_accountsrow is provisioned — the user has no way to deposit or spend. - Mobile shows a "not yet available in your country — join the waitlist" screen with an email capture that writes to a
waitlisttable (user_id,country,email). - The Infinia account owner record is retained for audit but marked
country_not_supported; no further Infinia calls are made against it.
Expansion is roadmapped — see product/roadmap § Geographic scope for the phased plan. US, Singapore, and LatAm sender markets are in the pipeline, gated on rail parity at Infinia (deposit + withdraw both supported before we open the country).
Why post-KYC rather than pre-signup: we can't reliably determine the user's country before OAuth returns and KYC runs. IP geolocation is unreliable (VPNs); OAuth doesn't include country. The trade-off is that we run KYC on some users we ultimately can't serve — a real cost, but small at MVP scale.
Notes
- Home currency is immutable at signup for MVP. If the user needs a different one later, that's an out-of-band support flow. Multi-currency users are deliberately out of scope.
- Apple's "Hide My Email" means we may only see
<random>@privaterelay.appleid.comon first sign-in and no email at all thereafter. Real email is captured during Infinia HOSTED KYC and stored onkyc_verifications.verified_identity. - KYC widget branding. The user sees Infinia's HOSTED widget (SumSub-powered). Whether that widget is branded as SumSub, Infinia, or white-labeled to look like GrinGo is tracked as an open question with Infinia.
- Owner status webhook. Infinia's public OpenAPI doesn't list an
owner.status_updatedwebhook. Until confirmed with Infinia, we detect KYC completion by pollingGET /v1/accounts/owners/{uuid}/every ~5s while the webview is open, backing off to every 60s after it closes. - Home-currency account provisioning happens automatically as soon as KYC status flips to
COMPLETED. Theaccount.status_updatedwebhook flips ourprovider_accounts.statusfromprovisioningtoactive. The user can start depositing as soon as we display the funding instructions. - BRL account is not created here. It's lazily provisioned on the user's first PIX payment. See pix-payment.
- KYC data storage. Documents and selfies never enter our systems — the HOSTED widget submits them directly to Infinia. We store a copy of the verified identity fields (name, DOB, address, doc number) on
kyc_verifications.verified_identityfor portability, encrypted at rest. See privacy for the data-protection posture.
Failure modes
| What broke | Where it surfaces |
|---|---|
| Apple / Google id_token verification fails | POST /auth/session → 401 |
| Infinia rejects HOSTED owner creation | POST /kyc/start → 502 with error surfaced to mobile |
| User closes webview mid-KYC | kyc_verifications.status stays pending; polling backs off to 60s; user can resume via same URL until expires_at |
| KYC declined by Infinia | status transitions to failed; mobile shows a support-contact screen (retry flow is post-MVP) |
| Verified address is outside the MVP allowlist | status transitions to country_not_supported; no account provisioned; user is offered the waitlist. See Country allowlist |
Account provisioning stalls in PROVISIONING | Mobile shows a "still setting up your account" state; the account.status_updated webhook resolves it |