> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venlyfinance.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe: issue wallets, receive fiat, pay out fiat

> Issue a wallet with the Wallet API, register it as a self-custody Finance account, give it bank details to receive fiat, and pay fiat out.

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](/guides/finance/permits-and-allowances) becomes necessary; see
[what this recipe leaves out](#what-this-recipe-leaves-out).

## What you'll build

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant App as Your App
    participant WAPI as Wallet API
    participant FAPI as Finance API
    participant Cust as End customer

    App->>WAPI: POST /api/users → signing method → POST /api/wallets
    WAPI-->>App: walletId, address
    App->>FAPI: POST /parties
    App->>FAPI: POST /accounts (address = the Wallet API wallet)
    App->>FAPI: POST /parties/{id}/verification
    FAPI-->>Cust: hosted KYC / KYB flow
    App->>FAPI: GET /parties/{id}/partner-terms → consentUrl
    FAPI-->>Cust: accepts partner terms
    App->>FAPI: POST .../virtual-bank-accounts/prepare
    FAPI-->>App: message to sign
    App->>WAPI: POST /api/signatures (MESSAGE)
    WAPI-->>App: signature
    App->>FAPI: POST .../virtual-bank-accounts (+ ownershipProof)
    FAPI-->>App: IBAN + referenceCode
    App->>FAPI: PAYOUT_RECIPIENT role → payout bank account → payout route
    FAPI-->>App: route ACTIVE, depositAddress
    Cust->>WAPI: send USDC from the wallet to depositAddress
    FAPI-->>App: PAYOUT_PROCESSING → PAYOUT_COMPLETED
```

| Step | What                                                   | API     |
| ---- | ------------------------------------------------------ | ------- |
| 1    | Issue the end customer's wallet                        | Wallet  |
| 2    | Create the party                                       | Finance |
| 3    | Open the account, registering the wallet's address     | Finance |
| 4    | Verify the party through the hosted flow               | Finance |
| 5    | Have the customer accept the partner terms             | Finance |
| 6    | Issue a virtual bank account, proving wallet ownership | both    |
| 7    | Set up the pay-out rail — once per beneficiary         | both    |
| 8    | Run a pay-out                                          | Finance |

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

<Steps>
  <Step title="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.

    |                | Wallet API (sandbox)                                                              | Finance API (staging)                                                                   |
    | -------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
    | Token URL      | `https://login-sandbox.venly.io/auth/realms/Arkane/protocol/openid-connect/token` | `https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
    | Base URL       | `https://api-wallet-sandbox.venly.io`                                             | `https://api-staging.venlyfinance.com/v1`                                               |
    | Token lifetime | \~6 minutes                                                                       | \~5 minutes                                                                             |

    Both use `client_credentials`. Getting credentials for each is covered in
    [Become a wallet provider](/getting-started/wallet-provider) and
    [Authentication](/getting-started/authentication). In the examples below, `{wallet_token}` is a Wallet
    API token and `{finance_token}` a Finance API token.
  </Step>

  <Step title="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](/guides/finance/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.
  </Step>

  <Step title="A webhook endpoint">
    Almost everything in this recipe completes asynchronously — verification, bank-detail provisioning,
    pay-outs. [Register a webhook](/api-reference/Finance-API/webhooks/register-a-webhook) early so the
    outcomes are pushed to you. [Which events to expect](#webhooks-to-handle) is summarised below.
  </Step>
</Steps>

***

## Step 1 — Issue the end customer's wallet

<Note>Wallet API.</Note>

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.

<Steps>
  <Step title="Create the user">
    ```bash theme={null}
    curl -X POST https://api-wallet-sandbox.venly.io/api/users \
      -H "Authorization: Bearer {wallet_token}" \
      -H "Content-Type: application/json" \
      -d '{ "reference": "user-12345" }'
    ```

    Keep `result.id` as `{userId}`. Use your own customer identifier as `reference` so you can
    reconcile later.
  </Step>

  <Step title="Create a signing method">
    ```bash theme={null}
    curl -X POST https://api-wallet-sandbox.venly.io/api/users/{userId}/signing-methods \
      -H "Authorization: Bearer {wallet_token}" \
      -H "Content-Type: application/json" \
      -d '{ "type": "PIN", "value": "123456" }'
    ```

    Keep `result.id` as `{signingMethodId}`.

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Create the wallet">
    ```bash theme={null}
    curl -X POST https://api-wallet-sandbox.venly.io/api/wallets \
      -H "Authorization: Bearer {wallet_token}" \
      -H "Content-Type: application/json" \
      -H "Signing-Method: {signingMethodId}:123456" \
      -d '{ "secretType": "BASE", "userId": "{userId}" }'
    ```

    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.
  </Step>
</Steps>

<Note>
  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](/getting-started/wallet-provider#secrettype-and-chain-names).
</Note>

***

## Step 2 — Create the party

<Note>Finance API.</Note>

A **party** is the person or organisation behind the account. It is created `ACTIVE` immediately —
that is not verification, which happens in step 4.

<CodeGroup>
  ```bash Individual theme={null}
  curl -X POST https://api-staging.venlyfinance.com/v1/parties \
    -H "Authorization: Bearer {finance_token}" \
    -H "Content-Type: application/json" \
    -d '{
      "partyType": "INDIVIDUAL",
      "externalId": "user-12345",
      "firstName": "Jane",
      "lastName": "Doe",
      "address": { "addressLine1": "1 Example Street", "city": "Amsterdam", "postalCode": "1011AB", "country": "NL" }
    }'
  ```

  ```bash Organisation theme={null}
  curl -X POST https://api-staging.venlyfinance.com/v1/parties \
    -H "Authorization: Bearer {finance_token}" \
    -H "Content-Type: application/json" \
    -d '{
      "partyType": "ORGANISATION",
      "externalId": "org-67890",
      "name": "Acme B.V.",
      "vatNumber": "NL123456789B01"
    }'
  ```
</CodeGroup>

```json Response (201) theme={null}
{
  "success": true,
  "result": {
    "id": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22",
    "externalId": "user-12345",
    "partyType": "INDIVIDUAL",
    "status": "ACTIVE",
    "createdAt": "2026-01-15T09:30:00Z",
    "version": 0
  }
}
```

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

<Note>Finance API.</Note>

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.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts \
  -H "Authorization: Bearer {finance_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "acct-12345",
    "name": "Jane Doe — Main",
    "chain": "BASE",
    "address": "{address}",
    "partyId": "{partyId}"
  }'
```

```json Response (201) theme={null}
{
  "success": true,
  "result": {
    "id": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
    "externalId": "acct-12345",
    "name": "Jane Doe — Main",
    "kycStatus": "VERIFICATION_PENDING",
    "status": "ACTIVE",
    "createdAt": "2026-01-15T09:30:00Z",
    "version": 0
  }
}
```

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](/guides/finance/wallets).
* The account starts at `kycStatus: VERIFICATION_PENDING`. **That is the gate on money movement**, and
  the next two steps clear it.

<Tip>
  `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.
</Tip>

***

## Step 4 — Verify the party

<Note>Finance API.</Note>

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.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/parties/{partyId}/verification \
  -H "Authorization: Bearer {finance_token}"
```

```json Response (200) theme={null}
{
  "success": true,
  "result": {
    "partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22",
    "verificationUrl": "https://onboard.venly.io/kyc?token=inv-tok-9f2c41ab",
    "status": "VERIFICATION_PENDING"
  }
}
```

<Warning>
  `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.
</Warning>

The verdict is asynchronous and human-paced. How you learn about it differs by party type:

| Party type     | Verdict arrives via                                                                                                           | Then read                |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `INDIVIDUAL`   | `PARTY_VERIFICATION_COMPLETED` webhook                                                                                        | `kycStatus` on the party |
| `ORGANISATION` | No webhook is published for KYB — poll [Get a party](/api-reference/Finance-API/parties/get-party-details) at a human cadence | `kybStatus` on the party |

Either way, the field you gate on is the **account's** `kycStatus`, which follows from its account
holder. Read it with [Get an account](/api-reference/Finance-API/accounts/get-account-details) 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](/guides/finance/onboarding/lifecycle).

***

## Step 5 — Have the customer accept the partner terms

<Note>Finance API.</Note>

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.

```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/parties/{partyId}/partner-terms \
  -H "Authorization: Bearer {finance_token}"
```

```json Response (200) theme={null}
{
  "success": true,
  "result": {
    "partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22",
    "status": "REQUIRED",
    "consentUrl": "https://onboard.venly.io/partner-terms?token=cons-tok-9f3a2b"
  }
}
```

`REQUIRED` means hand `consentUrl` to your customer. Once they accept, the status becomes `ACCEPTED`.

<Tip>
  Don't wait for this to block you.
  [`POST /parties/{partyId}/partner-terms/link`](/api-reference/Finance-API/parties/issue-a-partner-terms-consent-link)
  mints the link immediately, so you can collect consent in the same session as verification.
</Tip>

<Warning>
  `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](/guides/finance/onboarding/partner-terms-consent).
</Warning>

***

## Step 6 — Issue a virtual bank account

<Note>Finance API, with one Wallet API signature.</Note>

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**.

<Steps>
  <Step title="Ask for the message to sign">
    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{ "walletAddress": "{address}", "blockchain": "BASE" }'
    ```

    ```json Response (200) theme={null}
    {
      "success": true,
      "result": {
        "walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
        "blockchain": "BASE",
        "message": "I confirm ownership of 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 2026-07-09 [tok:9f2c41ab...]",
        "signedOnUtc": "2026-07-09"
      }
    }
    ```

    `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.

    <Note>
      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`.
    </Note>
  </Step>

  <Step title="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:

    ```bash theme={null}
    curl -X POST https://api-wallet-sandbox.venly.io/api/signatures \
      -H "Authorization: Bearer {wallet_token}" \
      -H "Signing-Method: {signingMethodId}:123456" \
      -H "Content-Type: application/json" \
      -d '{
        "signatureRequest": {
          "type": "MESSAGE",
          "secretType": "BASE",
          "walletId": "{walletId}",
          "data": "I confirm ownership of 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 2026-07-09 [tok:9f2c41ab...]"
        }
      }'
    ```

    ```json Response (200) theme={null}
    {
      "success": true,
      "result": {
        "type": "HEX_SIGNATURE",
        "r": "0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd",
        "s": "0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029",
        "v": "0x1c",
        "signature": "0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c"
      }
    }
    ```

    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.

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Create the virtual bank account with the proof">
    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "EUR Deposits",
        "inCurrency": "EUR",
        "targetCryptocurrency": "USDC",
        "idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "ownershipProof": {
          "walletAddress": "{address}",
          "blockchain": "BASE",
          "message": "I confirm ownership of 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 2026-07-09 [tok:9f2c41ab...]",
          "signature": "0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c"
        }
      }'
    ```

    ```json Response (201) theme={null}
    {
      "success": true,
      "result": {
        "id": "4d5e6f70-8192-4a3b-9c4d-5e6f7081920a",
        "bankAccountType": "EUR_SEPA",
        "status": "ACTIVE",
        "currency": "EUR",
        "targetCryptocurrency": "USDC",
        "iban": "DE89370400440532013000",
        "bic": "DEUTDEDB",
        "beneficiaryName": "Jane Doe",
        "referenceCode": "VFY-7K2Q-931",
        "depositRails": [
          {
            "railType": "SEPA",
            "iban": "DE89370400440532013000",
            "bic": "DEUTDEDB",
            "bankName": "Example Bank",
            "beneficiaryName": "Jane Doe",
            "paymentReference": "VFY-7K2Q-931"
          }
        ]
      }
    }
    ```
  </Step>
</Steps>

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](/api-reference/Finance-API/wallets/list-wallets-for-an-account) — or,
since it is a Wallet API wallet, straight from the Wallet API.

| Code                                  | Meaning                                                                                                                  |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `409 recipient-verification-pending`  | Step 4 hasn't completed. Retry once verified.                                                                            |
| `422 recipient-verification-rejected` | Verification was rejected. Provisioning cannot proceed.                                                                  |
| `400 ownership-proof-required`        | Your tenant needs the proof and you omitted `ownershipProof`.                                                            |
| `400 signature-mismatch`              | The signature doesn't recover the wallet address — usually the message was altered, or signed by a different `walletId`. |

The full field reference is in [Virtual bank accounts](/guides/finance/virtual-bank-accounts).

***

## Step 7 — Set up the pay-out rail

<Note>Finance API, with possibly one more Wallet API signature.</Note>

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.

<Steps>
  <Step title="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:

    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/party-roles \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{ "partyId": "{partyId}", "roleType": "PAYOUT_RECIPIENT" }'
    ```

    The role is checked when you create the route and again on every pay-out, so revoking it stops
    pay-outs immediately.
  </Step>

  <Step title="Register the beneficiary bank account">
    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/parties/{partyId}/payout-bank-accounts \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{
        "rail": "US_ACH",
        "fiatCurrency": "USD",
        "label": "Jane'"'"'s checking account",
        "accountHolderName": "Jane Doe",
        "railDetails": {
          "accountNumber": "123456789012",
          "abaRoutingNumber": "021000021",
          "accountType": "CHECKING"
        },
        "bankName": "Example National Bank",
        "bankAddress": {
          "street1": "270 Park Avenue",
          "city": "New York",
          "region": "NY",
          "postalCode": "10017",
          "country": "US"
        }
      }'
    ```

    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.

    <Note>
      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](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account).
    </Note>
  </Step>

  <Step title="Create the route">
    A route pairs the bank account with the crypto asset the customer will send:

    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/payout-routes \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{
        "payoutBankAccountId": "{payoutBankAccountId}",
        "depositAsset": { "chain": "BASE", "name": "USDC" }
      }'
    ```

    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.
  </Step>

  <Step title="If AWAITING_OWNERSHIP_PROOF — sign again with the Wallet API">
    Same pattern as step 6, different message:

    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/payout-routes/{routeId}/ownership-proof/prepare \
      -H "Authorization: Bearer {finance_token}"
    ```

    ```json Response (200) theme={null}
    {
      "success": true,
      "result": {
        "message": "I confirm ownership of wallet 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 20/08/2026"
      }
    }
    ```

    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:

    ```bash theme={null}
    curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/payout-routes/{routeId}/ownership-proof/complete \
      -H "Authorization: Bearer {finance_token}" \
      -H "Content-Type: application/json" \
      -d '{
        "message": "I confirm ownership of wallet 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 20/08/2026",
        "signature": "0x9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a01b"
      }'
    ```

    <Warning>
      This message is **date-bound**. Prepare, sign and complete in one session — a message prepared
      yesterday is rejected today.
    </Warning>

    A `200` means the proof was accepted, not that the route is ready. It continues to `ACTIVE`
    asynchronously.
  </Step>

  <Step title="Wait for ACTIVE and read the deposit address">
    No webhook event is published for route status, so poll
    [List payout routes](/api-reference/Finance-API/payout-routes/list-payout-routes) until the route is
    `ACTIVE`:

    ```bash theme={null}
    curl https://api-staging.venlyfinance.com/v1/accounts/{accountId}/payout-routes \
      -H "Authorization: Bearer {finance_token}"
    ```

    ```json Response (200) theme={null}
    {
      "success": true,
      "result": [
        {
          "id": "b2c3d4e5-f607-4819-a2b3-c4d5e6f70819",
          "status": "ACTIVE",
          "depositAsset": { "chain": "BASE", "name": "USDC" },
          "fiatCurrency": "EUR",
          "depositAddress": "0x9A7f4B2c1D3e5F6a8B0c2D4e6F8a0B2c4D6e8F0a",
          "createdAt": "2026-07-14T09:20:00Z",
          "updatedAt": "2026-07-14T09:24:31Z"
        }
      ]
    }
    ```

    `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>
</Steps>

***

## Step 8 — Run a pay-out

<Note>Finance API — but there is no request to make.</Note>

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.

<Steps>
  <Step title="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.
  </Step>

  <Step title="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](https://docs.venly.io/docs/wallet-api-overview) for the transaction
    endpoints.
  </Step>

  <Step title="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:

    ```mermaid theme={null}
    stateDiagram-v2
        [*] --> PROVIDER_PROCESSING: deposit observed at depositAddress
        PROVIDER_PROCESSING --> COMPLETED
        COMPLETED --> RETURNED: fiat leg bounced
    ```

    | Event               | Meaning                                                                                     |
    | ------------------- | ------------------------------------------------------------------------------------------- |
    | `PAYOUT_PROCESSING` | The deposit reached the rail. The crypto leg cannot fail from here.                         |
    | `PAYOUT_COMPLETED`  | The beneficiary has been paid. `settledFiatAmount` is now set.                              |
    | `PAYOUT_RETURNED`   | The receiving bank sent the fiat back — closed account, name mismatch. Treat as a reversal. |

    Reconcile against `settledFiatAmount` from [Get a payout](/api-reference/Finance-API/payouts/get-a-payout),
    not against the crypto amount sent — it is net of fees and conversion.
  </Step>
</Steps>

<Note>
  `REQUESTED`, `SENDING`, `REJECTED` and `FAILED` belong to API-initiated (`PULL`) pay-outs and never
  occur here. [List payouts](/api-reference/Finance-API/payouts/list-payouts) returns both kinds; filter
  on `fundingMode` if you ever mix them.
</Note>

***

## Webhooks to handle

| Event                                  | Step | What to do                                                               |
| -------------------------------------- | ---- | ------------------------------------------------------------------------ |
| `ACCOUNT_WALLETS_PROVISIONED`          | 3    | The wallet pair cleared AML. Informational.                              |
| `PARTY_VERIFICATION_COMPLETED`         | 4    | Individuals only. Read `status`; it can still be `VERIFICATION_PENDING`. |
| `VIRTUAL_BANK_ACCOUNT_CREATED`         | 6    | Rails are populated; show payment instructions.                          |
| `PAY_IN_SETTLED` / `PAY_IN_FAILED`     | 6    | Fiat arrived and converted into the wallet — or didn't.                  |
| `PAYOUT_PROCESSING`                    | 8    | A PUSH pay-out exists. Record the `payoutId`.                            |
| `PAYOUT_COMPLETED` / `PAYOUT_RETURNED` | 8    | Credit your ledger — and handle the reversal.                            |

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.

| Not in this recipe                                                | Why                                                | When you'd need it                                                                                                                                                                                   |
| ----------------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Permit / allowance** on the account wallet                      | Nothing here asks Venly to pull tokens.            | The moment you want either row below.                                                                                                                                                                |
| **API-initiated pay-outs** (`POST /payouts`, `fundingMode: PULL`) | The customer funds pay-outs by sending.            | You want to trigger a pay-out server-side without the customer signing a transaction. Requires a `CONFIRMED` permit — see [Approving transfers without gas](/guides/finance/permits-and-allowances). |
| **Account-to-account transfers**                                  | Not part of this flow.                             | Moving value between two Finance accounts. Also requires the permit.                                                                                                                                 |
| **Hosted pay-in sessions**                                        | The bank details from step 6 are the funding path. | A card/checkout-style pay-in instead of a bank transfer.                                                                                                                                             |

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

| Symptom                                                              | Cause                                                                                       | Fix                                                                                            |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `401` on every Finance call right after a successful Wallet API call | Wallet API token sent to the Finance API                                                    | Two products, two realms, two tokens. Check which `Authorization` header you're sending where. |
| `400 signature-mismatch` on step 6 or 7                              | The message was altered between `prepare` and `data`, or signed with a different `walletId` | Copy the string verbatim; sign with the wallet whose address is on the account.                |
| `404 wallet-not-found` on `prepare`                                  | `walletAddress` or `blockchain` doesn't match the account                                   | Use the address from step 3 and the account's `chain`.                                         |
| Route stuck in `AWAITING_OWNERSHIP_PROOF`                            | Nothing else moves until you sign                                                           | Do the optional step in 7.                                                                     |
| `complete` rejected with a valid signature                           | The message was prepared on a different day                                                 | Prepare, sign, complete in one session.                                                        |
| `409` on step 6, `422 recipient-not-authorized` on step 7            | Verification pending, or partner terms not accepted                                         | Finish steps 4 and 5; re-read partner terms — `ACCEPTED` can revert.                           |
| Customer sent crypto, no `PAYOUT_PROCESSING`                         | Wrong chain or wrong token for the route                                                    | Verify against `depositAsset`. The rail only observes the configured asset.                    |
| Fiat arrived but wasn't matched to the account                       | Payer omitted the `referenceCode`                                                           | Make the reference the most prominent part of the payment instructions.                        |

## Next steps

<CardGroup cols={2}>
  <Card title="Become a wallet provider" icon="wallet" href="/getting-started/wallet-provider">
    Wallet API credentials, chain-name mapping and the full first-wallet flow.
  </Card>

  <Card title="Virtual bank accounts" icon="building-columns" href="/guides/finance/virtual-bank-accounts">
    Every field on the response, `depositRails`, and the US lane.
  </Card>

  <Card title="Pay-outs" icon="money-bill-transfer" href="/guides/finance/payouts">
    The full lifecycle including the API-initiated variant.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/finance/webhooks">
    Registering, authenticating and testing your endpoint.
  </Card>
</CardGroup>
