Send to another user
Four calls, in order.1. Create the transfer proposal
Pass either
toUserId or toAddress. Passing neither returns a 400.
The recipient must already hold an active smart wallet in the currency you are sending.
toUserId resolves to the recipient’s smart wallet for that currency — the same wallets GET /v1/users/{recipientId}/smart-wallets/account/wallets returns — so sending CNGN to a user who only has a USDB wallet fails with The recipient does not have an active NGN account to receive this transfer. That is a 400, not a missing endpoint. Check the recipient’s wallets first, and note that currency takes the token code (CNGN) while that listing reports the display currency (NGN).The recipient must also be one of your own users. A toUserId belonging to another partner returns a 404, the same as a user that does not exist.id you use for the remaining three calls:
Shorthand: let the server pick the wallet
If you do not want to name the wallet to debit,account/send creates the same proposal and resolves the wallet for you — the account’s wallet in currency:
currency is required when the account holds more than one wallet, and optional when it holds exactly one. note becomes the proposal description. To name the wallet to debit while still passing a recipient, use POST /v1/users/{userId}/smart-wallets/fund with fromWalletId.
Both are shortcuts for step 1 only. They return the same proposal, so steps 2 to 4 still apply — nothing moves until the threshold is met and the owner key signs.
2. Approve
status moves to PENDING_SIGNATURES once the wallet’s approval threshold is met.
How many approvals you need depends on the wallet’s approval mode — ANY_ADMIN, ADMIN_THRESHOLD, or OWNER_APPROVAL. Read or change it with PATCH /v1/users/{userId}/smart-wallets/account/approval-policy.
3. Fetch the signing payload
hashToSign — the 32-byte digest you sign — plus a deadline, and whichever of safeTxHash or userOpHash applies to this proposal. The full EIP-712 object is included as typedData when the upstream provides it, but you sign hashToSign, not typedData.
This is only available once the proposal reaches PENDING_SIGNATURES, so poll it rather than calling it once — a 404 here usually means the approval threshold has not been met yet, not that the proposal is missing.
4. Sign and submit
hashToSign with the wallet owner key — the same key registered as userOwnerAddress at wallet creation — and submit the 65-byte hex signature. Poll GET …/proposals/{proposalId} for the terminal status.
Use the method that signs a raw hash, not the one that signs a message: signingKey.sign() in ethers, sign() in viem, unsafe_sign_hash() in eth-account. A message-signing method applies the EIP-191 prefix and produces a signature that recovers to a different address, which the backend rejects.
The signature must come from the registered owner key. Reuse the same SDK instance you used for
initWallet / walletAddress, or the recovered signer will not match the proposal’s signer snapshot.POST …/proposals/{proposalId}/reject with an optional { "reason": "…" }.
Swap inside the wallet
Same four calls, with aSWAP proposal in step 1:
Supported stablecoins:
USDB, CNGN, CADC, EURe, GBPe, MEXe.
For a user-level currency conversion outside the proposal flow, use POST /v1/users/{userId}/exchange/quote for a firm time-boxed quote followed by POST /v1/users/{userId}/exchange/convert.
Tracking a proposal
status progresses PENDING_APPROVALS → PENDING_SIGNATURES → COMPLETED. A FAILED proposal can be retried by calling approve again, which restarts the workflow.
Related
- Integration flow — every wallet-home endpoint in one table
- NGN deposits & withdrawals — the same proposal → sign pattern, applied to a bank payout
- Rails — which currencies and rails are available per region

