Skip to main content

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"
}
  • pixKey validated per pixKeyType (isValidPixKey).
  • recipientName optional; 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.