Skip to main content
This recipe combines the Wallet API and the Finance API into one integration. You issue a wallet for each end customer, register that wallet as their Finance account, give the account its own bank details so it can receive fiat, and let the customer pay fiat out to a bank account. Three things hold throughout:
  • 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.
That is why this recipe has no permit step. Nothing here asks Venly to move tokens out of the customer’s wallet. If you later want Venly to do that — API-initiated pay-outs, or transfers between accounts — the permit becomes necessary; see what this recipe leaves out.

What you’ll build

The worked example uses an individual on BASE, settling in USDC, receiving EUR by SEPA and paying out USD by ACH. Swap the values; the shape doesn’t change.

Prerequisites

1

Two products, two tokens

Venly Wallet and Venly Finance are separate products with separate credentials, separate OAuth realms and separate base URLs. A token from one does not authenticate the other — you hold two tokens throughout this recipe.Both use 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.
2

Tenant configuration

Ask your Venly contact to confirm two things about your Finance tenant before you start:
  • 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.
3

A webhook endpoint

Almost everything in this recipe completes asynchronously — verification, bank-detail provisioning, pay-outs. Register a webhook early so the outcomes are pushed to you. Which events to expect is summarised below.

Step 1 — Issue the end customer’s wallet

Wallet API.
Three Wallet API objects, nested in this order: a user (your end customer), a signing method (how they authorise actions — a PIN here), and a wallet that belongs to the user.
1

Create the user

Keep result.id as {userId}. Use your own customer identifier as reference so you can reconcile later.
2

Create a signing method

Keep result.id as {signingMethodId}.
The PIN is the customer’s authorisation secret, and this recipe uses it to sign twice more (steps 6 and 7). Collect it from the customer and hold it only for the duration of the call that needs it. Never hard-code one or reuse a default across users.
3

Create the wallet

Keep 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.
The Wallet API names chains 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

Finance API.
A party is the person or organisation behind the account. It is created ACTIVE immediately — that is not verification, which happens in step 4.
Response (201)
Keep 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

Finance API.
An account is what holds bank details, wallets and history. On a self-custody tenant you pass the wallet address from step 1; Finance registers it as the account wallet rather than generating one.
Response (201)
Keep 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_PROVISIONED fires 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.
chain here must match the secretType you created the wallet with. A BASE wallet registered on an ETHEREUM account is a valid address on the wrong chain, and nothing will tell you until funds don’t arrive.

Step 4 — Verify the party

Finance API.
Mint a hosted verification link and hand it to your customer. The party’s partyType decides whether they get a KYC (individual) or KYB (organisation) flow — there is no body and no choice to make.
Response (200)
verificationUrl is a credential — it grants access to that customer’s onboarding session. Redirect them to it or deliver it over a channel you trust. Don’t log it.
The verdict is asynchronous and human-paced. How you learn about it differs by party type: Either way, the field you gate on is the account’s 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

Finance API.
Verification proves who your customer is. It does not cover the terms our banking partners require them to accept — and they must accept those themselves, on a hosted page. Until they do, steps 6 and 7 are both blocked.
Response (200)
REQUIRED means hand consentUrl to your customer. Once they accept, the status becomes ACCEPTED.
Don’t wait for this to block you. POST /parties/{partyId}/partner-terms/link mints the link immediately, so you can collect consent in the same session as verification.
ACCEPTED is not permanent — a new partner or a new terms version moves the party back to REQUIRED. Re-read it before steps 6 and 7 rather than recording it once. See Partner-terms consent.

Step 6 — Issue a virtual bank account

Finance API, with one Wallet API signature.
A virtual bank account gives the account its own IBAN. Fiat sent to it is converted to 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.
1

Ask for the message to sign

Response (200)
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.
If the response is 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.
2

Sign it with the Wallet API

Pass message to the Wallet API exactly as returned, as a MESSAGE signature from the customer’s wallet, authorised with their signing method:
Response (200)
Why this works: 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.
Put message into data byte-for-byte. It embeds an opaque token, so don’t trim, re-encode, pretty-print or reconstruct it — the signature is checked against those exact bytes. The safest implementation copies the string from one response into the next request without touching it.
3

Create the virtual bank account with the proof

Response (201)
Show your customer the payment instructions from 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. The full field reference is in Virtual bank accounts.

Step 7 — Set up the pay-out rail

Finance API, with possibly one more Wallet API signature.
A pay-out sends crypto from the account wallet and has a bank beneficiary receive fiat. Three objects carry the configuration; you create them once per beneficiary bank account and reuse them for every pay-out.
1

Give the party the PAYOUT_RECIPIENT role

The party that owns the destination bank account needs a PAYOUT_RECIPIENT role on the account. In the self-withdrawal case that’s the account holder themselves:
The role is checked when you create the route and again on every pay-out, so revoking it stops pay-outs immediately.
2

Register the beneficiary bank account

Keep 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.
This example uses 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.
3

Create the route

A route pairs the bank account with the crypto asset the customer will send:
Keep 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.
4

If AWAITING_OWNERSHIP_PROOF — sign again with the Wallet API

Same pattern as step 6, different message:
Response (200)
Sign 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:
This message is date-bound. Prepare, sign and complete in one session — a message prepared yesterday is rejected today.
A 200 means the proof was accepted, not that the route is ready. It continues to ACTIVE asynchronously.
5

Wait for ACTIVE and read the deposit address

No webhook event is published for route status, so poll List payout routes until the route is ACTIVE:
Response (200)
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

Finance API — but there is no request to make.
In this recipe a pay-out is not an API call. The customer sends 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.
1

Show the customer the deposit address and the asset

Present 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.
2

The customer sends from their wallet

Send from the account wallet — the address registered in step 3 — so the source matches the wallet the ownership proof was signed for. Since that wallet is a Wallet API wallet, the transfer is a Wallet API token transaction authorised with the customer’s signing method; see the Wallet API documentation for the transaction endpoints.
3

Track the pay-out

You never receive a pay-out id from a request, so the webhook is how you learn one exists. PAYOUT_PROCESSING carries the payoutId; from there:Reconcile against 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

Not covered by an event, so poll: KYB verdicts (step 4) and pay-out route status (step 7).

What this recipe leaves out

Everything skipped follows from Venly never moving funds out of the customer’s wallet. Adding the permit later doesn’t change anything above — it is one more signature (EIP-712 this time) from the same Wallet API wallet.

Common pitfalls

Next steps

Become a wallet provider

Wallet API credentials, chain-name mapping and the full first-wallet flow.

Virtual bank accounts

Every field on the response, depositRails, and the US lane.

Pay-outs

The full lifecycle including the API-initiated variant.

Webhooks

Registering, authenticating and testing your endpoint.