Endpoints
Every request/response is Zod-validated. Schemas live in packages/shared/src/.
Health
GET /health
Unauthenticated. Returns { "status": "ok" }.
Auth
POST /auth/me
Idempotent user provisioning. Called on every app launch after Privy login.
Request:
{
"walletAddress": "0x…",
"signerAddress": "0x…"
}
walletAddress— the user's Privy smart wallet address on Base.signerAddress— optional EOA signer for the smart wallet.
Response: { user, wallet, kyc } — the persisted GrinGo user row, wallet row, and KYC row (nullable).
KYC
POST /kyc/start
Creates a Bridge KYC link. Idempotent per user.
Request (KycStartRequestSchema):
{
"fullName": "Jane Traveller",
"email": "jane@example.com",
"type": "individual"
}
type defaults to individual. business is not currently used.
Response: { kycLinkUrl, tosLinkUrl, status }. Mobile opens the Bridge-hosted URLs in an in-app browser.
GET /kyc/status
Poll to check KYC state. Returns { status: "pending" | "approved" | "rejected", ... }. The API also updates this asynchronously via the customer.updated webhook.
Virtual accounts
POST /virtual-accounts
Creates a Bridge virtual account for the user in the requested currency. One per currency.
Request (CreateVirtualAccountRequestSchema):
{ "currency": "gbp" }
Response (VirtualAccountResponseSchema):
{
"id": "uuid",
"currency": "gbp",
"paymentRail": "fps",
"bankName": "…",
"accountNumber": "12345678",
"sortCode": "12-34-56",
"iban": null,
"bic": null,
"status": "active",
"createdAt": "2026-07-15T…"
}
EUR responses fill iban + bic instead of accountNumber + sortCode.
GET /virtual-accounts
Returns all virtual accounts for the current user, most recent first.
Bank details
The user's own bank account, used as the withdraw destination.
POST /bank-details
Discriminated on currency.
GBP request (CreateBankDetailsRequestSchema):
{
"currency": "gbp",
"accountHolderName": "Jane Traveller",
"accountNumber": "12345678",
"sortCode": "12-34-56"
}
EUR request:
{
"currency": "eur",
"accountHolderName": "Jane Traveller",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX"
}
Unique on (userId, currency) — repeated POSTs overwrite the existing row for that currency.
Response (BankDetailsResponseSchema): the stored bank-details row.
GET /bank-details
Returns all stored bank details for the current user.
Transactions
GET /transactions
Ledger of the user's deposits + withdrawals. Returns an array of TransactionResponse:
[
{
"id": "uuid",
"type": "deposit",
"amountUsdc": "100.000000",
"status": "completed",
"bridgeTransferId": "…",
"createdAt": "2026-07-15T…"
}
]
POST /transactions/:id/confirm
Called by the mobile client after Privy signs a USDC transfer, to prove the on-chain leg happened.
Request (ConfirmTransactionRequestSchema):
{ "txHash": "0x…64hex…" }
The API fetches the tx receipt from Base, decodes the USDC Transfer event (apps/api/src/chain/usdc.ts), and verifies the amount + recipient match the transaction row. On success sets status = "confirmed".
Authoritative status transitions to completed only via Bridge webhook (drain_completed).
PIX
GET /pix/rate
Current USD → BRL rate from Bridge (used for on-screen quotes before creating a payment).
Response (PixRateResponseSchema):
{ "rate": 5.42, "fetchedAt": "2026-07-15T…" }
502 if Bridge is unreachable.
POST /pix/payments
Creates a PIX payment intent + Bridge liquidation address.
Request (CreatePixPaymentRequestSchema):
{
"amountBrl": 150.00,
"pixKey": "12345678901",
"pixKeyType": "cpf",
"recipientName": "Merchant Ltda"
}
pixKeyvalidated perpixKeyType(isValidPixKey).recipientNameoptional; defaults to"PIX recipient"on the Bridge external account.
Response (CreatePixPaymentResponseSchema):
{
"transactionId": "uuid",
"pixPaymentId": "uuid",
"liquidationAddress": "0x…",
"amountUsdc": "28.130000",
"exchangeRateAtCreate": 5.42
}
The client then asks Privy to sign USDC.transfer(liquidationAddress, amountUsdc).
GET /pix/payments
List of the user's PIX payments, most recent first. Array of PixPaymentResponse.
GET /pix/payments/:id
Single PIX payment. Polling this from the receipt screen is the current way to observe status transitions.
Withdraws
POST /withdraws
Off-ramp USDC to the user's stored bank account.
Request (CreateWithdrawRequestSchema):
{ "amountUsd": 100.00, "currency": "gbp" }
Requires the user to have posted bank_details for that currency first.
Response (WithdrawResponseSchema):
{
"id": "uuid",
"type": "withdrawal",
"amountUsdc": "100.000000",
"toAddress": "0x…",
"txHash": null,
"liquidationAddress": "0x…",
"fiatAmount": null,
"fiatCurrency": "gbp",
"status": "pending",
"failureReason": null,
"createdAt": "2026-07-15T…"
}
Same pattern as PIX: the client then signs a USDC transfer to liquidationAddress. Withdraw addresses are cached on bank_details.bridgeLiquidationAddress and reused across withdraws.
GET /withdraws
List of the user's withdrawals.