- The wallet is issued by the Wallet API and stays under the end customer’s signing method.
- Fiat that arrives on the account’s bank details is converted and settles straight into that wallet.
- A pay-out is funded by the customer sending crypto themselves — Venly observes the deposit and pays the fiat leg.
What you’ll build
BASE, settling in USDC, receiving EUR by SEPA and paying
out USD by ACH. Swap the values; the shape doesn’t change.
Prerequisites
Two products, two tokens
client_credentials. Getting credentials for each is covered in
Become a wallet provider and
Authentication. In the examples below, {wallet_token} is a Wallet
API token and {finance_token} a Finance API token.Tenant configuration
- Wallet type is
SELF_CUSTODY— you register wallet addresses; Venly does not generate them. See Venly-managed vs self-custody. - Pay-ins settle to the customer’s own wallet. Step 6 detects this for you, but knowing it up front avoids a surprise.
A webhook endpoint
Step 1 — Issue the end customer’s wallet
Create the user
result.id as {userId}. Use your own customer identifier as reference so you can
reconcile later.Create a signing method
result.id as {signingMethodId}.Create the wallet
result.id as {walletId} and result.address as {address}. Both are used on the Finance
side: the address to register the account, the id to sign with.secretType, and some names differ from the Finance API’s chain
values — POLYGON is MATIC, AVALANCHE is AVAC. BASE is BASE on both. The full mapping is in
Become a wallet provider.Step 2 — Create the party
ACTIVE immediately —
that is not verification, which happens in step 4.
result.id as {partyId}. Reuse the same reference you gave the Wallet API user as
externalId — one identifier across both products makes reconciliation trivial.
Step 3 — Open the account with the wallet’s address
address from step 1; Finance registers it as the account wallet rather than generating one.
result.id as {accountId}. The party becomes the account’s ACCOUNT_HOLDER.
Two things happen in the background:
- The account’s wallet pair is provisioned — your registered address as the account wallet, plus a
Venly-managed escrow wallet used during settlement. Both are AML-screened, and
ACCOUNT_WALLETS_PROVISIONEDfires when they clear. See Wallets & balances. - The account starts at
kycStatus: VERIFICATION_PENDING. That is the gate on money movement, and the next two steps clear it.
Step 4 — Verify the party
partyType decides whether
they get a KYC (individual) or KYB (organisation) flow — there is no body and no choice to make.
kycStatus, which follows from its account
holder. Read it with Get an account and
wait for VERIFIED.
While that is pending, you can already do step 5 and register the pay-out bank account from step 7 —
verification blocks money movement, not setup. See
Onboarding lifecycle.
Step 5 — Have the customer accept the partner terms
REQUIRED means hand consentUrl to your customer. Once they accept, the status becomes ACCEPTED.
Step 6 — Issue a virtual bank account
targetCryptocurrency and settles to the account wallet — the wallet you issued in step 1.
Because that wallet is under the customer’s control, the rail needs proof that the customer controls
it before it will send funds there. The proof is a message Finance assembles and the customer signs
with the Wallet API wallet itself.
Ask for the message to sign
walletAddress must be the address registered on the account in step 3 — anything else is
404 wallet-not-found.This call is stateless — nothing is stored, there is no id and no expiry — so it is safe to repeat.400 ownership-proof-not-applicable, your tenant’s pay-ins don’t settle to the
customer’s wallet and no proof is needed. Skip to the create call and omit ownershipProof.Sign it with the Wallet API
message to the Wallet API exactly as returned, as a MESSAGE signature from the
customer’s wallet, authorised with their signing method:type: MESSAGE is personal_sign, which is the EIP-191 scheme Finance verifies
against, and result.signature is the concatenated r + s + v that Finance expects. Leave
status out of the request so the signature executes immediately rather than being saved as a
draft.Create the virtual bank account with the proof
depositRails, and make the referenceCode
impossible to miss — the payer must quote it or the credit falls back to manual reconciliation.
On the EUR lane the account can come back status: PENDING with no rails yet. Don’t show instructions
until VIRTUAL_BANK_ACCOUNT_CREATED fires and depositRails is populated.
When fiat arrives, PAY_IN_SETTLED tells you the converted USDC is in the account wallet. Read the
balance with List wallets — or,
since it is a Wallet API wallet, straight from the Wallet API.
Step 7 — Set up the pay-out rail
Give the party the PAYOUT_RECIPIENT role
PAYOUT_RECIPIENT role on the account. In
the self-withdrawal case that’s the account holder themselves:Register the beneficiary bank account
result.id as {payoutBankAccountId}. This is the only endpoint that accepts raw bank
credentials; reads expose only the last four digits. bankAddress is the bank’s address, not
the customer’s, and all five components are required.US_ACH, which requires fiatCurrency: USD. Other rails are available; the
rail values and the railDetails shape each one expects are in
Register a payout bank account.Create the route
result.id as {routeId}. The route is created PENDING and registers asynchronously:
PENDING → REGISTERING → ACTIVE, or REJECTED.It may also stop at AWAITING_OWNERSHIP_PROOF. That means the rail needs a Travel-Rule proof
that the customer controls the wallet the funds will come from — and since that wallet is
self-custody, only the customer can sign it. Registration doesn’t fail; it waits for the next
step.If AWAITING_OWNERSHIP_PROOF — sign again with the Wallet API
result.message with the same POST /api/signatures call as step 6 — type: MESSAGE,
the customer’s walletId, the Signing-Method header, data set to the message verbatim. Then
submit the pair:200 means the proof was accepted, not that the route is ready. It continues to ACTIVE
asynchronously.Wait for ACTIVE and read the deposit address
ACTIVE:depositAddress is the whole point of this step. It is returned only for an ACTIVE route on a
self-custody account, it is fixed for the life of the route, and every pay-out over this route
is triggered by sending to it. Store it against the route.Step 8 — Run a pay-out
USDC from their wallet to the
route’s depositAddress; Venly observes the deposit, creates the pay-out with fundingMode: PUSH, and
the fiat leg goes to the beneficiary bank account.
Show the customer the deposit address and the asset
depositAddress, depositAsset.chain and depositAsset.name together. A transfer of the
right token on the wrong chain, or the wrong token on the right chain, is not a pay-out — it is a
lost transfer.The customer sends from their wallet
Track the pay-out
PAYOUT_PROCESSING carries the payoutId; from there:settledFiatAmount from Get a payout,
not against the crypto amount sent — it is net of fees and conversion.REQUESTED, SENDING, REJECTED and FAILED belong to API-initiated (PULL) pay-outs and never
occur here. List payouts returns both kinds; filter
on fundingMode if you ever mix them.Webhooks to handle
What this recipe leaves out
Everything skipped follows from Venly never moving funds out of the customer’s wallet.Common pitfalls
Next steps
Become a wallet provider
Virtual bank accounts
depositRails, and the US lane.
