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

# Integration Walkthrough

> Onboard a user, get their account verified, fund it, and send a transfer using the Finance API.

This guide walks the Finance API end-to-end — from creating your first party to settling a transfer. Every step uses real request and response shapes. Run it against staging and you'll have a working integration.

<Info>
  **Base URL (staging):** `https://api-staging.venlyfinance.com/v1`
  **Auth:** OAuth2 Bearer token in the `Authorization` header. Tokens expire after 5 minutes — see the [Authentication guide](/getting-started/authentication).
</Info>

## What you'll build

1. Onboard a **party** (your customer) — created `ACTIVE` immediately
2. Open an **account** for them (its custodial wallet is provisioned automatically)
3. Get the account **verified** by a Venly admin
4. Issue a **EUR IBAN** so they can fund via SEPA
5. Send an **outbound transfer**

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant App as Your App
    participant Auth as login-staging.venly.io
    participant API as Finance API
    participant Ops as Venly Admin

    App->>Auth: POST /token (client_credentials)
    Auth-->>App: access_token (5 min TTL)
    App->>API: POST /parties (INDIVIDUAL)
    API-->>App: partyId (status ACTIVE)
    App->>API: POST /accounts (partyId, chain=BASE)
    API-->>App: accountId (kycStatus VERIFICATION_PENDING, wallet auto-created)
    Ops-->>API: Review & verify account
    App->>API: GET /accounts/{id} → kycStatus VERIFIED
    App->>API: POST /accounts/{id}/virtual-bank-accounts
    API-->>App: IBAN + BIC + referenceCode
    App->>API: POST /accounts/{id}/transfers/crypto
    API-->>App: transferId, status COMPLETED
```

<Note>
  The big gate to design around: an **account** is created unverified (`kycStatus: VERIFICATION_PENDING`) and a **Venly admin** must verify it before it can move money. A **party**, by contrast, is `ACTIVE` right away. See [Account verification](/guides/finance/kyc-verification).
</Note>

***

## Prerequisites

<Steps>
  <Step title="Staging credentials">
    A `client_id` and `client_secret` from your Venly contact. Staging calls don't move real money.
  </Step>

  <Step title="An HTTP client">
    All examples use `curl`. Postman, HTTPie, or any SDK works equally well.
  </Step>

  <Step title="A UUID generator">
    Money-moving endpoints require an `idempotencyKey` (UUID v4) — `uuidgen`, `[guid]::NewGuid()`, or `crypto.randomUUID()`.
  </Step>
</Steps>

***

## Step 1 — Authenticate

Exchange your client credentials for a Bearer token. Every subsequent call needs it in the `Authorization` header.

```bash theme={null}
curl -X POST https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
```

```json Response theme={null}
{ "access_token": "eyJhbGciOi...", "expires_in": 300, "token_type": "Bearer" }
```

<Tip>
  Cache the token. Refresh on `401 Unauthorized` or a few seconds before `expires_in` elapses — don't request a new token on every call.
</Tip>

***

## Step 2 — Create a party

A **party** is the person or organisation behind an account — `INDIVIDUAL` (natural person) or `ORGANISATION` (company). It's created **`ACTIVE` immediately**; there's no verification step for a party.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/parties \
  -H "Authorization: Bearer YOUR_ACCESS_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" }
  }'
```

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

For an **organisation**, swap the body:

```json theme={null}
{ "partyType": "ORGANISATION", "externalId": "org-67890", "name": "Acme B.V.", "vatNumber": "NL123456789B01" }
```

***

## Step 3 — Open an account

An **account** holds the wallet, IBANs, and transfer history. Link the party from Step 2 with `partyId` (it becomes the `ACCOUNT_HOLDER`). The custodial wallet is **provisioned automatically** — there's no separate "create wallet" call.

The only difference between company types is the wallet `address`:

* **Venly-managed** — Venly creates and holds the wallet. Omit `address`.
* **Self-custody** — the customer controls the wallet. Send their `address` (required).

<CodeGroup>
  ```bash Venly-managed theme={null}
  curl -X POST https://api-staging.venlyfinance.com/v1/accounts \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "externalId": "acct-12345",
      "name": "Jane Doe — Main",
      "chain": "BASE",
      "partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22"
    }'
  ```

  ```bash Self-custody theme={null}
  curl -X POST https://api-staging.venlyfinance.com/v1/accounts \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "externalId": "acct-12345",
      "name": "Jane Doe — Main",
      "chain": "BASE",
      "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
      "partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22"
    }'
  ```
</CodeGroup>

```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:00",
    "version": 0
  }
}
```

<Tip>
  Set a meaningful `externalId` — your own user/customer ID. It doubles as a receiver shortcut in transfers (`receiverExternalId`), so you don't have to look up the Venly UUID every time.
</Tip>

### Add a second account holder (optional)

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

***

## Step 4 — Get the account verified

The account is created with `kycStatus: VERIFICATION_PENDING`. A **Venly admin** reviews and verifies it — there's no API call to trigger this. Once approved, `kycStatus` becomes `VERIFIED`.

Poll the account until it flips:

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

```json Response (200) theme={null}
{ "success": true, "result": { "id": "b2a1f0e9-...", "kycStatus": "VERIFIED", "status": "ACTIVE", "version": 1 } }
```

Until then, creating IBANs, transfers, payment requests, and pay-in sessions all fail with a KYC error — the exact `code` and HTTP status vary by operation (e.g. `400 kyc-not-verified` for IBANs, `422 kyc-required` for pay-in sessions). See [Account verification](/guides/finance/kyc-verification) for the full list of what's gated.

***

## Step 5 — View the wallet

The wallet was created with the account. Read it (and its balances) any time:

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

```json Response (200) theme={null}
{
  "success": true,
  "result": [
    {
      "id": "9f8e7d6c-5b4a-4938-8271-6a5b4c3d2e1f",
      "chain": "BASE",
      "type": "SELF_CUSTODY",
      "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
      "balances": [
        { "asset": "USDC", "contractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "amount": { "total": "0", "available": "0", "reserved": "0" } }
      ],
      "amlStatus": "PENDING"
    }
  ]
}
```

Balance amounts are decimal strings. `type` is `VENLY_MANAGED` or `SELF_CUSTODY` depending on your company (this example shows self-custody). A wallet has **no `status` field** — readiness shows as `amlStatus: APPROVED` (Venly-managed) or a `CONFIRMED` permit (self-custody).

<Note>
  **Venly-managed:** nothing to do here — the wallet is ready once its `amlStatus` is `APPROVED` (handled during onboarding).

  **Self-custody:** before this wallet can send funds, the customer signs a one-time [permit](/guides/finance/permits-and-allowances) so Venly can move their tokens (do this once after the account is set up). The wallet is ready once that permit is `CONFIRMED`.
</Note>

***

## Step 6 — Issue a virtual bank account (IBAN)

Once the account is `VERIFIED`, attach an IBAN so it can be funded via SEPA. Incoming euros are auto-converted to the `targetCryptocurrency` (`USDC`, `EURC`, `USDT`, or `USDS`) and credited to the wallet.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe — EUR Funding",
    "inCurrency": "EUR",
    "targetCryptocurrency": "USDC",
    "idempotencyKey": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
  }'
```

```json Response (201) theme={null}
{
  "success": true,
  "result": {
    "id": "4d5e6f70-8192-4a3b-9c4d-5e6f7081920a",
    "accountId": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
    "bankAccountType": "EUR_SEPA",
    "status": "ACTIVE",
    "currency": "EUR",
    "targetCryptocurrency": "USDC",
    "iban": "DE89370400440532013000",
    "bic": "DEUTDEDB",
    "referenceCode": "VFY-7K2Q-931"
  }
}
```

<Warning>
  Surface the **`referenceCode`** to your end user when they initiate a SEPA transfer — it's what links the incoming wire to this account. Without it, settlement falls back to manual reconciliation.
</Warning>

***

## Step 7 — Fund the account

### Option A — SEPA transfer to the IBAN

The customer sends EUR from their bank to the IBAN from Step 6, including the `referenceCode` in the payment reference. On receipt, Venly converts EUR → the target crypto and credits the wallet.

### Option B — Hosted fiat-to-crypto pay-in

Create a pay-in session and redirect the customer to the returned `paymentUrl`.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/fiat-to-crypto/payment-sessions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "inAmount": "100.00",
    "inCurrency": "EUR",
    "outCryptocurrency": "USDC",
    "callbackUrl": "https://your-app.com/webhooks/pay-in",
    "successRedirectUrl": "https://your-app.com/onboarding/funded",
    "failureRedirectUrl": "https://your-app.com/onboarding/retry",
    "idempotencyKey": "c7e8d9f0-1a2b-3c4d-5e6f-7a8b9c0d1e2f"
  }'
```

The response carries a `paymentUrl`. When the customer completes payment, the wallet is credited and your `callbackUrl` is invoked.

***

## Step 8 — Send a transfer

Transfers move funds **between two Venly accounts** — identify the receiver by `receiverAccountId` or by your own `receiverExternalId`. Both the sender **and** the receiver accounts must be `VERIFIED`; sending to an unverified receiver fails with `422 receiver-kyc-not-verified`.

### Crypto transfer

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{senderAccountId}/transfers/crypto \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverExternalId": "acct-67890",
    "chain": "BASE",
    "asset": "USDC",
    "amount": 25,
    "description": "Invoice #1138 settlement",
    "idempotencyKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }'
```

```json Response (201) theme={null}
{
  "success": true,
  "result": {
    "id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
    "senderAccountId": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
    "receiverAccountId": "c3b2a1f0-9d8c-4e3a-bf21-1a2b3c4d5e60",
    "chain": "BASE",
    "asset": "USDC",
    "amount": 25,
    "status": "COMPLETED",
    "transactionHash": "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
    "createdAt": "2026-01-15T09:30:00Z",
    "updatedAt": "2026-01-15T09:30:02Z"
  }
}
```

### Fiat transfer

A fiat-denominated transfer settles in the underlying stablecoin and returns a `fiatOrigin` block with the original currency and exchange rate.

```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{senderAccountId}/transfers/fiat \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverExternalId": "acct-67890",
    "currency": "USD",
    "amount": 25,
    "description": "Q2 supplier payment",
    "idempotencyKey": "e9f0a1b2-3c4d-5e6f-7a8b-9c0d1e2f3a4b"
  }'
```

<Note>
  **Self-custody senders** also need a `CONFIRMED` permit and a granted allowance before a transfer succeeds — see [Approving transfers without gas](/guides/finance/permits-and-allowances). Venly-managed accounts don't.
</Note>

***

## Step 9 — Track the transfer

Crypto and fiat transfers usually complete synchronously, but you can always fetch the latest state:

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

| `status`    | Meaning                                               |
| ----------- | ----------------------------------------------------- |
| `PENDING`   | Accepted, settlement in progress                      |
| `COMPLETED` | Final — funds delivered (`transactionHash` populated) |
| `FAILED`    | Final — see `errorMessage`                            |

List all transfers for an account with `GET /accounts/{accountId}/transfers` plus [pagination](/getting-started/conventions#pagination) parameters. Retrying a create with the same `idempotencyKey` returns the original transfer — no double-spend.

***

## Common pitfalls

<AccordionGroup>
  <Accordion title="KYC errors (kyc-not-verified or kyc-required) on transfers, IBANs, or payments">
    The **account** is still `VERIFICATION_PENDING`. A Venly admin must verify it first — poll `GET /accounts/{id}` until `kycStatus` is `VERIFIED`. (The party being `ACTIVE` is not enough.)
  </Accordion>

  <Accordion title="Self-custody transfer fails even when verified">
    The customer hasn't signed the token permit yet, so Venly has no allowance to move funds. Run the [permit flow](/guides/finance/permits-and-allowances) once, then retry.
  </Accordion>

  <Accordion title="Address is required for SELF_CUSTODY wallet type">
    Self-custody accounts must include the customer's wallet `address` on creation. Venly-managed accounts must not.
  </Accordion>

  <Accordion title="account-not-found on a transfer">
    The `receiverAccountId` / `receiverExternalId` doesn't resolve to an account in your company. Transfers are account-to-account — both sides must be Venly accounts you own.
  </Accordion>

  <Accordion title="Token expired mid-flow">
    Tokens last 5 minutes. Catch `401`, refresh, and retry the original call. Don't pre-authenticate every request — cache the token.
  </Accordion>
</AccordionGroup>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Getting started" icon="rocket" href="/guides/finance/getting-started">
    The condensed party → account → verify → IBAN flow.
  </Card>

  <Card title="Browse the API reference" icon="book" href="/api-reference/Finance-API/parties/list-all-parties">
    Every endpoint, field, and error code.
  </Card>

  <Card title="Permits & allowances" icon="signature" href="/guides/finance/permits-and-allowances">
    Gasless token approvals for self-custody accounts.
  </Card>

  <Card title="Contact support" icon="envelope" href="https://venlyfinance.com/contact">
    Stuck? Get in touch with your `accountId` and the failing request.
  </Card>
</CardGroup>
