# Add a party role
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/add-a-party-to-an-account
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/party-roles
Link an existing party to an account in a given role.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Associates a party with the account in a role. A party can hold only one role per account.
# Create an account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/create-a-new-account
api-reference/Finance-API-Specs.yaml POST /accounts
Open an account and provision its wallet on the chosen chain.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Creates an account and its wallet. Link an existing party with `partyId`, or create one inline with the `party` object. Self-custody accounts also supply the wallet `address` — see [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody). A new account **inherits its holder's verification**: link a `partyId` that is already `VERIFIED` and the account is created `VERIFIED`, ready to move money; a holder with no outcome yet gives `VERIFICATION_PENDING`, which can't move money until it is [verified](/guides/finance/kyc-verification).
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `INVALID_ADDRESS_FORMAT` | malformed wallet address, or `address` missing for a SELF\_CUSTODY company |
| `400` | `kyc-status-not-settable` | `kycStatus` `VERIFIED` or `REJECTED` supplied (not client-settable) |
| `400` | `kyc-not-required-not-allowed` | `kycStatus: NOT_REQUIRED` supplied but no Venly admin has enabled the tenant-managed KYC flag on your company tenant |
| `400` | `verification-kyc-only` | `party.sumsubToken` supplied but `party.partyType` is not `INDIVIDUAL` |
| `400` | `verification-self-custody-only` | `party.sumsubToken` supplied but your company's wallet type is not `SELF_CUSTODY` |
| `400` | `verification-not-provisioned` | `party.sumsubToken` supplied but verification is not provisioned for your company |
| `400` | `SUMSUB_TOKEN_NOT_REDEEMABLE` | the share token is expired or already used. The whole request is rolled back; retry with a fresh token |
| `409` | `account-already-exists` | an account with this `externalId` already exists for your company |
| `409` | `card-provider-already-linked` | the supplied `cardProviderReference` is already linked to another account |
# Get an account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/get-account-details
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}
Retrieve a single account by ID.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the account's `status`, verification status (`kycStatus`), and `version`.
# List accounts
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/list-all-accounts
api-reference/Finance-API-Specs.yaml GET /accounts
Page through the accounts in your organisation.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a paginated list of accounts. Filter by `status` or `externalId`, and page with `page`/`size` (see [Pagination](/getting-started/conventions#pagination)). Each [account](/guides/finance/glossary) belongs to a party and holds wallets and virtual bank accounts.
# List party roles
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/list-party-roles-for-an-account
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/party-roles
List the parties linked to an account and the role each one holds.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the [party-role](/guides/finance/glossary) links for an account (for example, the `ACCOUNT_HOLDER`). This is a plain array — it is not paginated.
# Remove a party role
Source: https://docs.venlyfinance.com/api-reference/Finance-API/accounts/remove-a-party-from-an-account
api-reference/Finance-API-Specs.yaml DELETE /accounts/{accountId}/party-roles/{partyId}
Unlink a party from an account.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Removes a party's role from the account. The last `ACCOUNT_HOLDER` cannot be removed. Returns `204 No Content`.
# Get wallet allowances
Source: https://docs.venlyfinance.com/api-reference/Finance-API/allowances/get-wallet-token-allowances
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/wallets/{walletId}/allowances
Read the ERC-20 token allowances granted from a self-custody wallet.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the token allowances granted from a self-custody wallet to the orchestration wallet. Filter by `asset`. Amounts are decimal strings; a uint256-max value indicates an unlimited allowance. Allowances are established with [permits](/guides/finance/permits-and-allowances).
**Self-custody companies only.** On a Venly-managed company this returns `400 invalid-request` — "Allowances are only applicable for SELF\_CUSTODY companies" — since managed wallets have no allowance to read.
# Verify wallet allowances
Source: https://docs.venlyfinance.com/api-reference/Finance-API/allowances/verify-wallet-allowances
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/wallets/{walletId}/allowance-verification
Activate a self-custody wallet from the allowances it has already granted on-chain.
**Coming soon — only used with card payments.** Wallet activation applies only to accounts set up for card payments, which aren't available yet. Every other account wallet is `ACTIVE` from the moment it is created, so this call answers `400 wallet-not-verifiable`. To let Venly move a self-custody wallet's funds, sign a [permit](/guides/finance/permits-and-allowances).
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Activates a self-custody account wallet by reading the allowances it has already granted on-chain, instead of going through the permit flow.
The wallet grants the permission with a plain `approve(spender, amount)` transaction instead of a permit. (Self-custody wallets are [single-key wallets only](/guides/finance/managed-vs-self-custody#single-key-wallets-only) for now — smart-contract wallets aren't supported.)
The allowance *is* the consent: only the wallet's controller could have granted it. So this endpoint reads public chain state and takes **no request body and no proof**.
## What happens when you call it
Every asset covered by a sufficient allowance is marked confirmed.
The wallet is usable immediately.
You get a `verificationExpiresAt`. Venly keeps re-checking until then, so granting the remaining allowances is enough — you don't have to call again.
`VERIFYING_ALLOWANCE` is a waiting room, not a usable state. Every check that requires an active wallet treats it exactly like `PENDING`, so don't offer the wallet to an end-user until it reads `ACTIVE`.
If the window closes with assets still uncovered, the wallet falls back to `PENDING` and you start again.
Read `assets[]` to see exactly which allowances are still missing — `allowanceSufficient` is per asset.
Both activation routes end in the same place: a wallet is `ACTIVE` once every supported asset is covered. See [Permits and allowances](/guides/finance/permits-and-allowances).
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `wallet-not-verifiable` | the wallet cannot enter verification: it is already `ACTIVE`, or it is `FROZEN`/`CLOSED` |
| `400` | `invalid-request` | the wallet is Venly-managed, so it activates through the permit flow instead |
# Create API credentials
Source: https://docs.venlyfinance.com/api-reference/Finance-API/api-credentials/create-api-credentials
api-reference/Finance-API-Specs.yaml POST /api-credentials
Create an OAuth client-credentials pair. The secret is shown once.
**Requires scope:** `manage:credentials` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Creates a client-credentials pair and returns its `secret` **in this response only**. Record it now; a lost secret is replaced by [rotating](/api-reference/Finance-API/api-credentials/rotate-a-credentials-secret), never by reading it back.
The `clientId` is server-generated from your company and the `name` you supply. Each company holds **one** credential; disabling it does not free the slot. See [API credentials](/guides/finance/api-credentials).
## Errors
| HTTP | `code` | When |
| - | - | - |
| `400` | *(validation)* | `name` missing, blank, or over 100 characters |
| `409` | `credential-limit-exceeded` | your company already holds its credential — rotate it instead |
| `409` | `credential-name-unavailable` | no free `clientId` could be derived for this name; try another |
| `503` | `keycloak-unavailable` | the identity provider was unreachable; retry |
# Enable or disable a credential
Source: https://docs.venlyfinance.com/api-reference/Finance-API/api-credentials/enable-or-disable-a-credential
api-reference/Finance-API-Specs.yaml PUT /api-credentials/{clientId}/status
Pause or resume token issuance for a credential without deleting it.
**Requires scope:** `manage:credentials` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Sets the credential to `ACTIVE` or `DISABLED`. Disabling is a pause, not a delete: the credential keeps its `clientId`, secret and history and simply stops issuing tokens. Re-enabling restores it unchanged.
If Venly provisioned more than one credential for your company before this endpoint existed, disabling one is **final** — at most one may be active, and restoring a disabled one needs Venly. If a secret is compromised, [rotate](/api-reference/Finance-API/api-credentials/rotate-a-credentials-secret) rather than disable.
## Errors
| HTTP | `code` | When |
| - | - | - |
| `400` | *(validation)* | `status` missing or not `ACTIVE` / `DISABLED` |
| `404` | `credential-not-found` | unknown `clientId`, or one belonging to another company |
| `409` | `credential-limit-exceeded` | setting `ACTIVE` while another credential is already active |
| `503` | `keycloak-unavailable` | the identity provider was unreachable; retry |
# List API credentials
Source: https://docs.venlyfinance.com/api-reference/Finance-API/api-credentials/list-api-credentials
api-reference/Finance-API-Specs.yaml GET /api-credentials
Every OAuth credential your company holds — never their secrets.
**Requires scope:** `manage:credentials` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Lists your company's credentials, active and disabled, distinguished by `status`. **No secret is returned, in any form** — a secret is shown only when it is created or rotated. See [API credentials](/guides/finance/api-credentials).
`manage:credentials` is a dashboard-user scope. API credentials never hold it, so no credential can manage another.
# Rotate a credential's secret
Source: https://docs.venlyfinance.com/api-reference/Finance-API/api-credentials/rotate-a-credentials-secret
api-reference/Finance-API-Specs.yaml POST /api-credentials/{clientId}/secret
Replace a credential's secret. The old one stops working immediately.
**Requires scope:** `manage:credentials` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Generates a new `secret` and returns it **in this response only**. The previous secret stops working immediately, so rotate only when the new value can be deployed at once. Tokens already issued against the old secret remain valid until they expire.
Rotation is refused on a `DISABLED` credential — enable it first.
## Errors
| HTTP | `code` | When |
| - | - | - |
| `404` | `credential-not-found` | unknown `clientId`, or one belonging to another company — indistinguishable by design |
| `409` | `credential-disabled` | the credential is disabled; no secret was generated |
| `503` | `keycloak-unavailable` | the identity provider was unreachable; retry |
# Download an invoice PDF
Source: https://docs.venlyfinance.com/api-reference/Finance-API/billing/download-an-invoice-pdf
api-reference/Finance-API-Specs.yaml GET /billing/invoices/{invoiceId}/download
Download the PDF of an issued invoice.
**Requires scope:** `view:billing` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the invoice as **raw PDF bytes** — not wrapped in the JSON envelope. Errors still use the JSON `ErrorResponse`. The `Content-Disposition` header carries a filename derived from the invoice number.
A `404` always means the invoice is not yours to read; an invoice you *can* read whose PDF cannot be produced is `500 billing-provider-error`, so a rendering fault is never mistaken for a wrong id.
# Get an invoice
Source: https://docs.venlyfinance.com/api-reference/Finance-API/billing/get-an-invoice
api-reference/Finance-API-Specs.yaml GET /billing/invoices/{invoiceId}
Read one invoice issued to your company.
**Requires scope:** `view:billing` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns one issued invoice in full. Unsent, voided, foreign and unknown invoice ids all answer the same `404 invoice-not-found`.
# List invoices
Source: https://docs.venlyfinance.com/api-reference/Finance-API/billing/list-invoices
api-reference/Finance-API-Specs.yaml GET /billing/invoices
The invoices Venly has issued to your company, most recent first.
**Requires scope:** `view:billing` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Reads your invoices live from our billing system — Finance stores none. Only issued (`SENT`) invoices are visible. To read them, open the billing portal at `billingUrl` on [your company profile](/api-reference/Finance-API/company/get-your-company-profile) — the per-invoice `sequenceUrl` is an internal reference, not a page you can open. The PDF is available through [Download an invoice PDF](/api-reference/Finance-API/billing/download-an-invoice-pdf).
This endpoint uses **cursor pagination**, not `page`/`size`. Send `nextPageCursor` / `previousPageCursor` back exactly as received. Until Venly has linked your company to a billing customer it answers `422 billing-not-configured`.
See [Billing](/guides/finance/billing).
# Get your company profile
Source: https://docs.venlyfinance.com/api-reference/Finance-API/company/get-your-company-profile
api-reference/Finance-API-Specs.yaml GET /company
Read your own company's KYB and lifecycle status, wallet type, supported chains and billing portal link.
**Requires scope:** any of `manage:accounts`, `manage:transfers` or `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the profile of the company your token belongs to. There's no path parameter: the company is resolved from the token, so no request can name another company. It's a good first call to confirm your credentials work.
| Field | What it tells you |
| - | - |
| `kybStatus` | Your company's own verification: `PENDING`, `VERIFIED` or `DENIED`. |
| `status` | `ONBOARDING` until Venly activates you, then `ACTIVE`. `SUSPENDED` is reversible; `ABORTED` is not. |
| `walletType` | `VENLY_MANAGED` or `SELF_CUSTODY` — see [Managed vs. self-custody](/guides/finance/managed-vs-self-custody). |
| `supportedChains` | The chains your company is configured for. For the assets on them, use [List supported assets](/api-reference/Finance-API/supported-assets/list-supported-assets). |
| `billingUrl` | Your billing portal. **Absent** until Venly links your company to a billing account — treat a missing field as "billing not set up yet", not as an error. |
The billing portal is per company, not per invoice. Opening it asks for your email address and sends a sign-in link; only contacts on your billing account can get in. See [Billing](/guides/finance/billing).
A token without a qualifying scope, without a company, or for a company not yet active on Finance gets `403`. There is no `404` to branch on.
# Create a pay-in session
Source: https://docs.venlyfinance.com/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/fiat-to-crypto/payment-sessions
Start a hosted fiat-to-crypto pay-in and get a payment URL.
**Coming soon — not yet available.** Hosted pay-in sessions are documented ahead of launch. Until they're enabled for your company, creating one is refused with `400 no-suitable-provider`. To receive fiat today, use a [virtual bank account](/guides/finance/virtual-bank-accounts).
**Requires scope:** `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Creates a hosted pay-in session. Redirect the payer to the returned `paymentUrl`; on completion the fiat is converted to the chosen cryptocurrency and credited to the account's wallet. Provide a `callbackUrl` and a unique `idempotencyKey` ([safe retries](/getting-started/conventions#idempotency)).
The account must be **verified** before a pay-in session can be created. See [Account verification](/guides/finance/kyc-verification).
**Retries.** A replay with the same `idempotencyKey` and an identical body returns the original session — never a second payment link. Change anything and it's refused with `422 idempotency-conflict`: the amount and currency decide what the link collects, so a corrected value is a new request under a new key. Bodies are compared by value — `"5"`, `"5.0"` and `"5.00"` are the same `inAmount`, and currency codes ignore case. A failed attempt (`409 vba-not-ready`, `502`, `503`) uses the key up, so retry those with a new one.
A session is an **initiation-only** record: it ends at `PAYMENT_RECEIVED`, and the conversion and crypto credit are a separate [pay-in](/guides/finance/pay-ins) signalled by `PAY_IN_SETTLED`. Read it back with [Get a pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/get-a-pay-in-session).
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `no-suitable-provider` | no enabled provider supports this currency combination |
| `400` | `company-not-active` | your company is not `ACTIVE` |
| `400` | `account-not-active` | the account is not `ACTIVE` |
| `409` | `vba-not-ready` | the account's EUR virtual bank account is still being provisioned — **retryable with a new `idempotencyKey`** |
| `409` | `idempotency-conflict` | a request with the same key is still processing — retry shortly with the **same** key |
| `422` | `idempotency-conflict` | the key was used with a different body, on another account, or on an attempt that failed — correct the request and use a new key |
| `422` | `kyc-required` | the account's KYC status is not `VERIFIED` |
| `422` | `unsupported-asset` | `outCryptocurrency` is not a supported asset for your company |
| `422` | `ambiguous-asset` | `outCryptocurrency` is supported on more than one of your chains; unlink the one you don't settle on |
| `422` | `unsupported-currency` | hosted pay-ins are currently EUR only |
| `422` | `vba-required` | the account has no active EUR virtual bank account — [create one](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account) first |
| `422` | `ambiguous-eur-vba` | more than one active EUR virtual bank account — [close](/api-reference/Finance-API/virtual-bank-accounts/close-a-virtual-bank-account) the surplus |
| `422` | `vba-asset-mismatch` | the virtual bank account settles into a different asset than `outCryptocurrency` |
# Get a pay-in session
Source: https://docs.venlyfinance.com/api-reference/Finance-API/fiat-to-crypto-payment-sessions/get-a-pay-in-session
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/fiat-to-crypto/payment-sessions/{sessionId}
Read one fiat-to-crypto pay-in session.
**Coming soon — not yet available.** Hosted pay-in sessions are documented ahead of launch. Until they're enabled for your company, creating one is refused with `400 no-suitable-provider`. To receive fiat today, use a [virtual bank account](/guides/finance/virtual-bank-accounts).
**Requires scope:** `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a single pay-in session from persisted state. It advances only when the payment provider reports back, so polling this endpoint can only ever show you `PAYMENT_RECEIVED` as its happy-path end — settlement is tracked on the [pay-in](/api-reference/Finance-API/pay-ins/get-a-pay-in), signalled by `PAY_IN_SETTLED`.
# List pay-in sessions
Source: https://docs.venlyfinance.com/api-reference/Finance-API/fiat-to-crypto-payment-sessions/list-pay-in-sessions
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/fiat-to-crypto/payment-sessions
List an account's fiat-to-crypto pay-in sessions, newest first.
**Coming soon — not yet available.** Hosted pay-in sessions are documented ahead of launch. Until they're enabled for your company, creating one is refused with `400 no-suitable-provider`. To receive fiat today, use a [virtual bank account](/guides/finance/virtual-bank-accounts).
**Requires scope:** `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the account's pay-in sessions from persisted state — the provider is never called. Use it for a deposits view, or to recover a session whose `callbackUrl` notification was missed.
A session is an **initiation-only** record. It stops at `PAYMENT_RECEIVED`; the conversion and crypto credit are a separate [pay-in](/api-reference/Finance-API/pay-ins/list-pay-ins). Don't wait for a "completed" session status — there is none.
To poll for changes, sort `updatedAt` descending and read only the first page, sized above your expected change volume. `from`/`to` bound `createdAt`, so there is no "changed since" filter.
# Create a party
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/create-a-new-party
api-reference/Finance-API-Specs.yaml POST /parties
Onboard an individual or organisation — the root entity that holds accounts.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Creates a party. Use `partyType: INDIVIDUAL` with `firstName`/`lastName`, or `ORGANISATION` with `name` (and optional `vatNumber`). The returned `id` is what you pass when [opening an account](/api-reference/Finance-API/accounts/create-a-new-account). Identity verification runs asynchronously after creation — track it on `kycStatus` (individuals) or `kybStatus` (organisations); see [Account verification](/guides/finance/kyc-verification).
# Create a verification link
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/create-a-verification-link
api-reference/Finance-API-Specs.yaml POST /parties/{partyId}/verification
Mint a hosted KYC or KYB link to send an end-user through verification.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Mints a hosted verification link and returns the URL matching the party's type — the KYC flow for `INDIVIDUAL`, the KYB flow for `ORGANISATION`. There is no request body: the stored `partyType` decides the flow.
Hand the URL to the end-user. The verdict arrives asynchronously and flips the party's `kycStatus`/`kybStatus` — poll [Get a party](/api-reference/Finance-API/parties/get-party-details) for the outcome.
Links do not expire. Re-POSTing while a link is live returns the **same** URL with `200`; if it was revoked, a fresh one comes back with `201`. Either way you can safely call this again — you will not strand the user with a dead link.
Treat `verificationUrl` as a credential. It grants access to the end-user's onboarding session, so deliver it over a channel you trust and never log it.
See [Hosted verification links](/guides/finance/onboarding/hosted-verification) for the full flow, or [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing) for the alternative route.
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `409` | `verification-not-provisioned` | the company has no provisioned verification tenant |
| `409` | `party-already-verified` | the party's `kycStatus`/`kybStatus` is already `VERIFIED` |
# Delete a party
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/delete-party
api-reference/Finance-API-Specs.yaml DELETE /parties/{partyId}
Permanently delete a party that has no linked accounts.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Deletes a party. Only allowed when the party has no associated accounts — remove or close its [accounts](/api-reference/Finance-API/accounts/list-all-accounts) first. Returns `204 No Content` on success.
# Get a party's KYC status
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/get-a-partys-kyc-status
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/kyc-status
See where a party's KYC or KYB verification stands, and what the end customer still has to do.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns where the party's verification stands — KYC for an `INDIVIDUAL`, KYB for an `ORGANISATION` — read live from the verification platform. Use it to tell a verification that is **under review** apart from one that is **waiting on your end customer**, and to see which step needs attention.
| Field | What it tells you |
| - | - |
| `verificationState` | The headline: `AWAITING_CUSTOMER`, `IN_REVIEW`, `RESUBMISSION_REQUESTED`, `APPROVED` or `DECLINED`. |
| `requiredAction` | What the end customer still has to do — for example `COMPLETE_SUMSUB_VERIFICATION`. Omitted when nothing is required. |
| `steps` | The review outcome per step, with `rejectLabels` explaining a rejection and `itemIds` listing questionnaire items that still need attention. |
| `caseStatus` | The underlying case status, for support conversations. |
| `lastSyncedAt` | When the state was last synchronised with the verification provider. |
When `verificationState` is `AWAITING_CUSTOMER`, [issue a KYC completion link](/api-reference/Finance-API/parties/issue-a-kyc-completion-link) and hand it to your end customer.
This endpoint reports **progress**, not the verdict you branch on. The party's and its accounts' `kycStatus`/`kybStatus` change only once a verdict (`APPROVED` or `DECLINED`) is reached — read those from [Get a party](/api-reference/Finance-API/parties/get-party-details).
New values may be added to every enum-like field. Treat an unknown `verificationState` as still in progress.
## Errors
| HTTP | `code` | When | Retry? |
| - | - | - | - |
| `404` | `party-not-found` | No such party for your company | **No** |
| `404` | `kyc-status-not-found` | The party has no verification on record yet | **No** — start one with a [verification link](/api-reference/Finance-API/parties/create-a-verification-link) |
| `500` | `identity-verification-rejected` | Verification refused the request | **No** |
| `503` | `identity-verification-unavailable` | Platform unreachable | **Yes**, with backoff |
See [Troubleshooting verification](/guides/finance/onboarding/troubleshooting).
# Get a party's partner-onboarding status
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/get-a-partys-partner-onboarding-status
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/partner-onboarding
Read where a party stands with our banking partner's recipient checks, without attempting provisioning.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns whether the party has been relayed to our banking partner and where the partner's own recipient checks stand — the third readable onboarding gate next to [identity verification](/api-reference/Finance-API/parties/get-verification-linkage) and [partner-terms consent](/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state).
Read it before provisioning: `PENDING` here is exactly what [creating a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account) would refuse with `409 partner-onboarding-pending`. `NOT_STARTED` is a normal `200`. See [Partner onboarding](/guides/finance/onboarding/partner-onboarding).
# Get partner-terms consent state
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/partner-terms
Whether the end customer still has to accept our partners' terms — and the link to hand them.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns one `status` and, while acceptance is outstanding, one `consentUrl` — covering **every** partner the party is onboarding to. You never deal with partners individually.
| `status` | Meaning | What you do |
| - | - | - |
| `NOT_REQUIRED` | Nothing outstanding — including a party that hasn't passed KYC yet | Nothing |
| `REQUIRED` | The end customer must accept before onboarding proceeds | Hand them `consentUrl` |
| `ACCEPTED` | Every enabled partner's currently-active terms are accepted, so onboarding is not blocked on consent | Nothing |
`REQUIRED` always comes with a `consentUrl`. That pairing is guaranteed — if the platform reports terms outstanding without a link, you get a retryable `503` instead of a `REQUIRED` you can't act on.
Each call returns the current state and link, so an expiring or re-issued link is never stale. If you'd rather poll the plain party read, `partnerTermsStatus` on [Get party details](/api-reference/Finance-API/parties/get-party-details) carries the same value.
**`ACCEPTED` is not the end.** It is evaluated against the terms active *now*, so enabling a new partner — or a partner publishing a new terms version — supersedes what was accepted and moves the party back to `REQUIRED`. Re-read it; don't record it as a done milestone.
## Errors
| HTTP | `code` | When | Retry? |
| - | - | - | - |
| `500` | `identity-verification-rejected` | Verification refused the request itself | **No** |
| `503` | `identity-verification-unavailable` | Platform unreachable, or it answered but the state couldn't be resolved safely | **Yes**, with backoff |
A `503` here **never** means "nothing outstanding". The endpoint fails closed rather than guessing, so treating it as `NOT_REQUIRED` would skip the gate.
See [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
# Get a party
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/get-party-details
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}
Retrieve a single party by ID, including verification status and address.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the full party record, including `kycStatus`/`kybStatus`, address, and `version`. Send the `version` back when [updating the party](/api-reference/Finance-API/parties/update-party-details) — see [safe updates](/getting-started/conventions#versioning).
# Get verification linkage
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/get-verification-linkage
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/iv-verification
Read a party's verification linkage and its durable case reference.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the party's verification linkage: the linkage `status` plus the durable `ivCaseReference` you can quote to support when investigating a stuck verification.
This endpoint reports the **plumbing**, not the verdict. For the actual KYC/KYB outcome, read `kycStatus`/`kybStatus` from [Get a party](/api-reference/Finance-API/parties/get-party-details).
`ivCaseReference` is populated once the platform returns a reference and is **never cleared** afterwards — including on `FAILED` after a decline. Read the field directly rather than inferring it from `status`.
This always returns `200` for an existing party, and reflects party-level linkage only — never your company's tenant provisioning state.
# Issue a KYC completion link
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/issue-a-kyc-completion-link
api-reference/Finance-API-Specs.yaml POST /parties/{partyId}/kyc-status/completion-link
Mint a short-lived link your end customer opens to finish a verification that is waiting on them.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Issues a fresh link your end customer opens to finish a verification that is waiting on them — typically a Sumsub verification you forwarded with `sumsubToken` whose applicant never completed their checks. Call it when [the party's KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status) is `AWAITING_CUSTOMER`.
Takes no request body.
Every call mints a new, short-lived link, and nothing is stored — so when one expires, simply call this again. The URL embeds an access token. Treat it as a credential: don't log it, don't put it in a shared inbox, and don't reuse it across customers.
Follow the customer's progress on [Get a party's KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status); the verdict itself lands on the party's `kycStatus`/`kybStatus`.
## Errors
| HTTP | `code` | When | Retry? |
| - | - | - | - |
| `404` | `party-not-found` | No such party for your company | **No** |
| `404` | `kyc-status-not-found` | The party has no verification on record | **No** |
| `409` | `kyc-not-completable` | The verification is already decided, isn't waiting on the customer, or wasn't started from a Sumsub token | **No** — read the [KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status) to see why |
| `500` | `identity-verification-rejected` | Verification refused the request | **No** |
| `503` | `identity-verification-unavailable` | Platform unreachable or couldn't produce a link | **Yes**, with backoff |
See [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing#when-the-customer-still-has-to-finish).
# Issue a partner-terms consent link
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/issue-a-partner-terms-consent-link
api-reference/Finance-API-Specs.yaml POST /parties/{partyId}/partner-terms/link
Mint a consent link so your customer can accept every partner's terms before onboarding starts.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Mints a fresh consent link so your end customer can accept every partner's terms **before** onboarding begins — before any of their data is sent to a partner.
Use this when you'd rather collect consent up front than wait for it to become a blocker. The hosted page shows **every currently-enabled partner's latest terms** in one pass, whether or not the party has been forwarded to any partner yet.
Takes no request body.
**Every call supersedes the previous link.** A link you already handed out stops working the moment you call this again. Don't use it to check state — read [the consent state](/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state) for that.
The URL embeds a scoped, expiring token. Treat it as a credential: don't log it, don't put it in a shared inbox, and don't reuse it across customers. When it expires, issue a new one rather than trying to extend it.
## Errors
| HTTP | `code` | When | Retry? |
| - | - | - | - |
| `409` | `verification-not-provisioned` | Verification isn't enabled for your company | **No** — ask Venly to enable it |
| `500` | `identity-verification-rejected` | Verification refused the request | **No** |
| `503` | `identity-verification-unavailable` | Platform unreachable or couldn't produce a link | **Yes**, with backoff |
See [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
# List parties
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/list-all-parties
api-reference/Finance-API-Specs.yaml GET /parties
Page through the parties (individuals and organisations) in your organisation.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a paginated list of parties. Filter by `partyType`, `status`, or `externalId`, and page with `page`/`size` (see [Pagination](/getting-started/conventions#pagination)). A [party](/guides/finance/glossary) is the root entity in the Finance API — each one can hold one or more accounts.
# Request partner onboarding
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/request-partner-onboarding-for-a-party
api-reference/Finance-API-Specs.yaml POST /parties/{partyId}/partner-onboarding
Start onboarding a verified individual to our banking partner ahead of provisioning.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Starts onboarding the party to our banking partner as soon as it is verified, instead of letting the first virtual-bank-account or payout-route attempt trigger it and fail with `partner-onboarding-pending`. Returns `202` with the current status; poll [Get a party's partner-onboarding status](/api-reference/Finance-API/parties/get-a-partys-partner-onboarding-status) for the outcome.
Idempotent while onboarding is `PENDING` or `APPROVED`. A `REJECTED` party is not a dead end — calling again re-attempts onboarding. See [Partner onboarding](/guides/finance/onboarding/partner-onboarding).
## Errors
| HTTP | `code` | When |
| - | - | - |
| `400` | `party-type-not-supported` | the party is not an `INDIVIDUAL` |
| `409` | `recipient-not-verified` | the party is not KYC-verified yet — retryable once it is |
| `409` | `party-not-account-holder` | the party holds no active `ACCOUNT_HOLDER` role — retryable once assigned |
| `409` | `verification-not-provisioned` | identity verification is not enabled for your company |
| `500` | `identity-verification-rejected` | the platform rejected the request itself; not retryable |
| `503` | `identity-verification-unavailable` | transient; retry with backoff |
# Update a party
Source: https://docs.venlyfinance.com/api-reference/Finance-API/parties/update-party-details
api-reference/Finance-API-Specs.yaml PATCH /parties/{partyId}
Update an existing party's name, VAT number, or address.
**Requires scope:** `manage:parties` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Patches only the fields you send. Include the current `version` (from a prior read); a stale `version` returns `409` so you can refetch and retry (see [Versioning](/getting-started/conventions#versioning)). `partyType` cannot be changed after creation.
# Get a pay-in
Source: https://docs.venlyfinance.com/api-reference/Finance-API/pay-ins/get-a-pay-in
api-reference/Finance-API-Specs.yaml GET /pay-ins/{payInId}
Read one pay-in by the id carried on the PAY_IN_SETTLED or PAY_IN_FAILED webhook.
**Requires scope:** `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Reads one settlement record. The `payInId` comes from a `PAY_IN_SETTLED` or `PAY_IN_FAILED` [webhook](/guides/finance/webhooks#event-catalogue). Unknown and foreign ids return the same `404 pay-in-not-found`.
Reconcile against `netCryptoAmount` — that is what reached the account wallet.
# List pay-ins
Source: https://docs.venlyfinance.com/api-reference/Finance-API/pay-ins/list-pay-ins
api-reference/Finance-API-Specs.yaml GET /pay-ins
List every pay-in settlement record across your company.
**Requires scope:** `manage:pay-in-sessions` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Lists the pay-in ledger — one record per fiat credit that arrived on a [virtual bank account](/guides/finance/virtual-bank-accounts) or through a [pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session), with its settlement status, the gross and net crypto amounts, and the on-chain transaction. See [Pay-ins](/guides/finance/pay-ins).
All filters combine with AND. Unknown or foreign `accountId` / `virtualBankAccountId` values match nothing. Records stay readable after the resources they reference are closed.
# Get a payout bank account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-bank-accounts/get-a-payout-bank-account
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/payout-bank-accounts/{payoutBankAccountId}
Retrieve a single allow-listed pay-out destination.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a single payout bank account. The account number is masked to the last 4 digits — the full number is never retrievable through the API.
# List payout bank accounts
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-bank-accounts/list-payout-bank-accounts
api-reference/Finance-API-Specs.yaml GET /parties/{partyId}/payout-bank-accounts
List a party's allow-listed pay-out destinations, with account numbers masked.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a paginated list of the party's allow-listed bank accounts. Account numbers are masked to the last 4 digits.
Only accounts with `status: ACTIVE` can back a payout route.
# Register a payout bank account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account
api-reference/Finance-API-Specs.yaml POST /parties/{partyId}/payout-bank-accounts
Allow-list a beneficiary bank account as a pay-out destination for a party.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Allow-lists a beneficiary bank account so it can receive pay-outs. The owning party comes from the path, never the body.
This is the **only** endpoint that accepts raw rail credentials. The sensitive one — the account number on `US_ACH`, the IBAN on `SEPA` — is encrypted at rest immediately and is never returned: this response and every read path expose only `accountNumberLast4` or `ibanLast4`. Routing numbers and BICs are public bank identifiers and are returned in full.
`bankAddress` is the **beneficiary bank's** postal address, not the recipient's. All components are required, and `country` must be a valid ISO 3166-1 alpha-2 code.
Two rails: `US_ACH` requires `fiatCurrency: USD` and ACH `railDetails`; `SEPA` requires `fiatCurrency: EUR` and an `iban` (with an optional `bic`). Supply `beneficiaryPhoneNumber` on every registration — some destinations refuse a beneficiary without it, and a bank account cannot be edited afterwards. `beneficiaryEmail` is optional and never sent to the payment provider. A new account starts `PENDING` and becomes `ACTIVE` once activated for pay-outs. See [Pay-outs](/guides/finance/payouts#setup).
Next: pair it with a deposit asset by [creating a payout route](/api-reference/Finance-API/payout-routes/create-a-payout-route).
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `invalid-rail` | unsupported rail |
| `400` | `invalid-currency` | currency is inconsistent with the rail (`US_ACH` requires `USD`, `SEPA` requires `EUR`) |
| `400` | `missing-field` | a required field is absent |
| `400` | `invalid-account-number` | the account number is not 4–17 digits |
| `400` | `invalid-routing-number` | the ABA routing number failed its checksum |
| `400` | `invalid-account-type` | `accountType` is not `CHECKING` or `SAVINGS` |
| `400` | `invalid-iban` | the IBAN is missing, malformed, fails its check digit, or is not from a SEPA country |
| `400` | `invalid-bic` | the BIC is not 8 or 11 characters |
| `400` | `invalid-bank-address` | a bank-address component is missing or `country` is not a valid ISO 3166-1 alpha-2 code |
# Create a payout route
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-routes/create-a-payout-route
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/payout-routes
Connect an allow-listed bank account to a crypto deposit asset for pay-outs.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Creates an account-scoped route connecting an allow-listed [payout bank account](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account) to a crypto deposit asset. The route fixes the beneficiary, lane, asset and fiat currency for every pay-out made over it.
The provider is resolved internally from your tenant's enablement — the request never names one, and provider identity is never exposed.
**The recipient gate must pass.** The bank account's party needs an `ACTIVE` `PAYOUT_RECIPIENT` role on this account with cleared KYC/KYB. Add it via [Add a party to an account](/api-reference/Finance-API/accounts/add-a-party-to-an-account). A party may hold `PAYOUT_RECIPIENT` alongside `ACCOUNT_HOLDER` on the same account — that is the self-payout case.
Routes are created `PENDING` and progress `REGISTERING → ACTIVE` as provider registration completes, or `REJECTED` on terminal failure. **Only `ACTIVE` routes are usable**, so poll [List payout routes](/api-reference/Finance-API/payout-routes/list-payout-routes) before requesting a pay-out.
See [Pay-outs](/guides/finance/payouts) for the full flow.
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `422` | `recipient-not-authorized` | the bank account's party has no `PAYOUT_RECIPIENT` role on this account, or its KYC/KYB is not cleared |
| `422` | `recipient-role-inactive` | the `PAYOUT_RECIPIENT` role exists but is `INACTIVE` |
| `422` | `unsupported-asset` | the deposit asset is not supported for your company |
| `422` | `unsupported-combination` | no single provider resolves for this bank account and deposit asset |
# List payout routes
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-routes/list-payout-routes
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/payout-routes
List an account's pay-out routes and read PUSH deposit addresses.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Lists the account's pay-out routes. Poll this to see a route reach `ACTIVE` before requesting a pay-out over it.
`depositAddress` is included **only** for an `ACTIVE` route on a `SELF_CUSTODY` account. It is the address a customer sends to in order to trigger a [PUSH pay-out](/guides/finance/payouts#push-pay-outs).
Provider identity is never exposed.
# Prepare a route ownership proof
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-routes/prepare-a-route-ownership-proof
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/payout-routes/{routeId}/ownership-proof/prepare
Get the exact message a self-custody wallet owner must sign to finish registering a pay-out route.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the exact text the owner of a self-custody wallet must sign to prove they control it, so the pay-out route can finish registering.
A route parks at `AWAITING_OWNERSHIP_PROOF` when its destination requires a Travel-Rule ownership proof and the source wallet is self-custody. Venly holds no key for that wallet, so only its owner can sign.
This step is **stateless** — nothing is stored, and there's no id or expiry. Calling it twice for the same route returns an equivalent message, so it's safe to repeat.
The owner signs `message` **verbatim**. Don't reformat, re-encode, pretty-print or reconstruct it — the signature is checked against those exact bytes.
Only an externally-owned account can sign: the scheme is EIP-191, and **ERC-1271 contract signatures are not supported**. Self-custody wallets are [single-key wallets only](/guides/finance/managed-vs-self-custody#single-key-wallets-only) for now.
The message is date-bound (`signedOnUtc`), so a proof prepared on one day can't be submitted against a message built for another. Sign and submit in the same session.
Submit the result with [Submit a route ownership proof](/api-reference/Finance-API/payout-routes/submit-a-route-ownership-proof). See [Pay-outs](/guides/finance/payouts#proving-you-control-the-source-wallet).
# Submit a route ownership proof
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payout-routes/submit-a-route-ownership-proof
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/payout-routes/{routeId}/ownership-proof/complete
Submit the signed message and resume a paused pay-out route registration.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Submits the signed message and resumes the route's registration. On success the route leaves `AWAITING_OWNERSHIP_PROOF` and continues to `ACTIVE` asynchronously.
A `200` means the proof was accepted, not that the route is ready. Poll the route or [register a webhook](/guides/finance/webhooks) to hear when it reaches `ACTIVE` — only then does it have a `depositAddress`.
Send `message` **exactly as [prepare](/api-reference/Finance-API/payout-routes/prepare-a-route-ownership-proof) returned it**. It must match the canonical text for this route, which embeds the route's own wallet, and the signature must recover that wallet.
That's what makes the stateless design safe, and it has two consequences worth knowing:
* A proof signed for a different wallet, route or tenant is rejected — it can't be replayed here.
* A message with extra text wrapped around the expected content is rejected too, even if the signature is valid.
You don't have to call `prepare` first. This endpoint only cares that the message is canonical and the signature matches.
The proof is never stored or logged. It's verified in memory, forwarded once, and discarded — only the outcome is persisted.
| Code | Meaning |
| - | - |
| `ownership-proof-not-applicable` | This route needs no owner-signed proof, or its source wallet is Venly-managed. |
| `invalid-proof-message` | `message` isn't the canonical text for this route. |
| `signature-mismatch` | `signature` doesn't recover the route's wallet address. |
# Get a payout
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payouts/get-a-payout
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/payouts/{payoutId}
Read a single pay-out with its masked beneficiary and settlement outcome.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Reads a single pay-out in the full shape: the nested `payoutRoute` includes the masked beneficiary bank account, plus `depositAddress` for an `ACTIVE` route on a `SELF_CUSTODY` account.
Use this to confirm the real outcome of a pay-out:
* `settledFiatAmount` — the fiat the provider actually paid out. Null until `COMPLETED`, and typically lower than the crypto sent, net of fees.
* `sendTxHash` — the on-chain send. The managed broadcast for PULL, the observed customer transaction for PUSH.
* `failureReason` — populated for `REJECTED`, `FAILED` and `RETURNED`.
Internal and sensitive fields — the idempotency key, permit material, wallet ids, managed transaction ids and raw provider data — are never returned.
# List payouts
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payouts/list-payouts
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/payouts
List an account's pay-outs, both API-created and customer-initiated.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Lists the account's pay-outs — both PULL (created via the API) and PUSH (created from an observed customer deposit), distinguished by `fundingMode`.
Returns compact summaries: the nested `payoutRoute` is route-only, with no beneficiary and no deposit address. Use [Get a payout](/api-reference/Finance-API/payouts/get-a-payout) for the full shape.
`sortOn` accepts only `requestedAt`, `createdAt`, `status` and `cryptoAmount`. Any other value returns `400`.
# Request a payout
Source: https://docs.venlyfinance.com/api-reference/Finance-API/payouts/request-a-payout
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/payouts
Send crypto from an account wallet over a route and have the provider pay out fiat.
**Requires scope:** `manage:pay-outs` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Requests a **PULL** pay-out against an `ACTIVE` route: Venly sends crypto from the account wallet to the provider's deposit address using the up-front permit, and the provider pays fiat to the beneficiary.
The route already fixes the beneficiary, lane, asset and fiat currency, so you supply only the route, the crypto amount, and an idempotency key.
**A `201` is not a success signal.** Permit allowance and account-wallet balance are deliberately *not* pre-checked. If either is insufficient, this call still returns `201` and the asynchronous send fails terminally — the pay-out moves to `FAILED` with a `failureReason`.
Track the real outcome by polling [Get a payout](/api-reference/Finance-API/payouts/get-a-payout) or by [registering a webhook](/api-reference/Finance-API/webhooks/register-a-webhook).
Status advances `REQUESTED → SENDING → PROVIDER_PROCESSING → COMPLETED`. Note that a fiat leg can still bounce **after** completion, moving to `RETURNED`.
PUSH pay-outs are not created here — see [Pay-outs](/guides/finance/payouts#push-pay-outs).
**Idempotency:** the key must be unique per company across all idempotent endpoints. Reuse with a different body, or after the original failed, returns `422`; reuse while the original is in flight returns `409`.
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `422` | `route-unresolved` | the route does not resolve on this account |
| `422` | `route-not-active` | the route is not `ACTIVE` |
| `422` | `unsupported-asset` | the route's lane is no longer supported for your tenant |
| `422` | `idempotency-conflict` | the key was reused with a different body, or after a failed original |
# Get permit messages
Source: https://docs.venlyfinance.com/api-reference/Finance-API/permits/get-permit-messages
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/wallets/{walletId}/permits
Fetch the EIP-712 typed-data messages to sign for gasless token approvals.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the EIP-2612 permit messages for a **self-custody** account wallet — one per supported asset, each with a `supportedAssetId` and the `typedData` to sign. Sign `typedData` with the wallet **owner's** key, then [submit the signature](/api-reference/Finance-API/permits/submit-signed-permit) to grant a gasless allowance. Full flow: [Approving transfers without gas](/guides/finance/permits-and-allowances).
Self-custody account wallets only — Venly-managed and escrow wallets are permitted automatically. Calling this on a Venly-managed company returns `400 invalid-request` ("Permits are only applicable for SELF\_CUSTODY companies"). See [Approving transfers without gas](/guides/finance/permits-and-allowances).
# Submit a signed permit
Source: https://docs.venlyfinance.com/api-reference/Finance-API/permits/submit-signed-permit
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/wallets/{walletId}/permits
Submit an EIP-712 signature to grant a gasless token allowance.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Submits the `v`/`r`/`s` signature for a [permit message](/api-reference/Finance-API/permits/get-permit-messages). It moves `PENDING → SUBMITTED → CONFIRMED` (poll `GET .../permits`); once all of a wallet's permits are `CONFIRMED` the wallet becomes `ACTIVE`. Re-submitting an already-confirmed permit returns `409`.
The signature must recover to the wallet `owner`, or the permit settles as `FAILED`. The call returns HTTP `200` with `result.status` — poll `SUBMITTED → CONFIRMED`. See [Approving transfers without gas](/guides/finance/permits-and-allowances).
# List account supported assets
Source: https://docs.venlyfinance.com/api-reference/Finance-API/supported-assets/list-account-supported-assets
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/supported-assets
See which assets an account can actually move, and what's blocking the rest.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
The same assets as [List supported assets](/api-reference/Finance-API/supported-assets/list-supported-assets), each annotated with a `permitStatus` telling you whether **this account** can move that asset yet.
Check this before offering an asset to an end-user. An asset the tenant has enabled is not automatically usable by every account: the account's wallet needs an allowance in place first, and on a self-custody wallet only the customer can grant it.
| `permitStatus` | What it means | Who acts |
| - | - | - |
| `READY` | Usable now | — |
| `ACTIVATING` | Allowance in place, wallets not usable yet | Nobody — re-read shortly |
| `ACTION_REQUIRED` | No allowance yet on a self-custody wallet | **The customer** must sign |
| `PENDING` | No allowance yet on a Venly-managed wallet | Venly |
| `FAILED` | Granting the allowance failed | Retry, then support |
| `NO_WALLET` | No wallet on that asset's chain | Create one |
`permitStatus` is derived on every request and never stored. Don't cache it — a status can change as soon as a customer signs or a wallet finishes activating.
Only `READY` means "spend now". Treat everything else as unavailable, and use `ACTION_REQUIRED` as the trigger to send the customer through [permits and allowances](/guides/finance/permits-and-allowances).
# List supported assets
Source: https://docs.venlyfinance.com/api-reference/Finance-API/supported-assets/list-supported-assets
api-reference/Finance-API-Specs.yaml GET /supported-assets
Discover which chains and assets your tenant can settle in.
**Requires any one of:** `manage:transfers` or `manage:pay-in-sessions` or `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns every chain/asset pair enabled for your tenant. Read this at runtime instead of hard-coding a list — the set is configured per tenant and can change without an API version bump.
Two things this drives:
* **Asset pickers.** Show the end-user only what your tenant can actually settle in.
* **Naming an asset explicitly.** When a fiat currency maps to more than one asset — USDC on two chains, both denominated in USD — fiat transfers need an explicit `chain` + `asset` pair. This tells you which currencies are in that position.
This returns what the **tenant** is enabled for, not what a given account can use today. An account also needs an allowance in place before it can move an asset — use [List account supported assets](/api-reference/Finance-API/supported-assets/list-account-supported-assets) for that.
See [Supported chains and assets](/guides/finance/supported-chains-and-assets).
# Create a crypto transfer
Source: https://docs.venlyfinance.com/api-reference/Finance-API/transfers/create-crypto-transfer
api-reference/Finance-API-Specs.yaml POST /accounts/{senderAccountId}/transfers/crypto
Send a crypto transfer between two accounts.
**Requires scope:** `manage:transfers` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Transfers a crypto `asset` on the given `chain` from the sender to a receiver account. Retrying with the same `idempotencyKey` returns the original transfer — no double-spend (see [Idempotency](/getting-started/conventions#idempotency)).
**Concept guide:** [Transfers](/guides/finance/transfers)
The sender account must be **verified**, and (self-custody) its wallet needs an allowance to the orchestration wallet. See [Account verification](/guides/finance/kyc-verification) and [Approving transfers without gas](/guides/finance/permits-and-allowances).
# Create a fiat transfer
Source: https://docs.venlyfinance.com/api-reference/Finance-API/transfers/create-fiat-transfer
api-reference/Finance-API-Specs.yaml POST /accounts/{senderAccountId}/transfers/fiat
Send a fiat-denominated transfer between two accounts.
**Requires scope:** `manage:transfers` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Transfers a fiat amount from the sender to a receiver (`receiverAccountId` or `receiverExternalId`). The amount settles in the underlying stablecoin; the response includes a `fiatOrigin` block with the original currency and exchange rate. Send a unique `idempotencyKey` ([safe retries](/getting-started/conventions#idempotency)).
**Concept guide:** [Transfers](/guides/finance/transfers)
The sender account must be **verified**, and (self-custody) its wallet needs an allowance to the orchestration wallet. See [Account verification](/guides/finance/kyc-verification) and [Approving transfers without gas](/guides/finance/permits-and-allowances).
# Get a transfer
Source: https://docs.venlyfinance.com/api-reference/Finance-API/transfers/get-transfer-details
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/transfers/{transferId}
Retrieve a single transfer by ID.
**Requires scope:** `manage:transfers` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the transfer details, including `status` (`PENDING`/`COMPLETED`/`FAILED`) and, for fiat transfers, the `fiatOrigin` block. The account must be the sender or receiver, otherwise the API returns `404`.
**Concept guide:** [Transfers](/guides/finance/transfers)
# List transfers
Source: https://docs.venlyfinance.com/api-reference/Finance-API/transfers/list-transfers-for-account
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/transfers
Page through an account's transfers — sent and received, fiat and crypto.
**Requires scope:** `manage:transfers` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the account's transfers. Filter by `accountRole` (`SENDER`/`RECEIVER`) and `status`, and page with `page`/`size` (see [Pagination](/getting-started/conventions#pagination)).
**Concept guide:** [Transfers](/guides/finance/transfers)
# Close a virtual bank account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/virtual-bank-accounts/close-a-virtual-bank-account
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/virtual-bank-accounts/{virtualBankAccountId}/close
Decommission a virtual bank account so its bank details stop accepting money.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Moves an `ACTIVE` virtual bank account to `CLOSED` and asks the payment provider to decommission the bank details. The local transition is applied first, so a provider outage never blocks a closure.
Read `providerClosureStatus` on the response. Only `SUCCEEDED` means the bank details are dead. `FAILED` means they are presumed live — call this endpoint again; it is idempotent and retries only the provider leg.
Closing frees the account's `(currency, targetCryptocurrency)` slot immediately, so a replacement can be created straight away. Money that still arrives on closed details is recorded and can be settled by Venly afterwards; it is never lost. `CLOSED` accounts stay visible in list and detail reads. See [Closing a virtual bank account](/guides/finance/virtual-bank-accounts#closing-a-virtual-bank-account).
# Create a virtual bank account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/virtual-bank-accounts
Assign EUR (SEPA) or USD (ACH) bank details to an account for fiat pay-ins.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Provisions a virtual bank account. Provide a display `name`, the fiat `inCurrency` to receive (EUR provisions a `EUR_SEPA` IBAN, USD a `USD_ACH` account), the `targetCryptocurrency` incoming funds convert to, and a unique `idempotencyKey` ([safe retries](/getting-started/conventions#idempotency)). The response includes the bank coordinates — IBAN/BIC for EUR, account and routing number for USD — and a `referenceCode` the payer must quote so the wire is matched to this account.
The account must be **verified** (and have a linked account-holder party) before a virtual bank account can be created. See [Account verification](/guides/finance/kyc-verification).
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `duplicate-virtual-bank-account` | an active account already exists for this wallet + currency pair |
| `400` | `tenant-not-onboarded-for-lane` | the requested lane is not enabled for your tenant |
| `400` | `tenant-kyb-not-approved` | your tenant's recorded KYB status is not approved |
| `400` | `unsupported-asset` | `targetCryptocurrency` is not a supported asset for your company |
| `400` | `no-suitable-provider` | no enabled provider supports the requested currency pair |
| `400` | `account-holder-unresolved` | the account does not resolve to exactly one active account-holder party |
| `400` | `ownership-proof-required` | this account requires a signed wallet-ownership proof, but no `ownershipProof` block was supplied |
| `400` | `invalid-proof-message` | `ownershipProof.message` is malformed or does not reference the wallet address and embedded token |
| `400` | `signature-mismatch` | `ownershipProof.signature` does not recover the supplied wallet address |
| `400` | `validation-error` | the provider rejected the request as invalid |
| `404` | `account-not-found` | no such account for your company |
| `404` | `wallet-not-found` | the `ownershipProof` wallet address/blockchain is not this account's self-custody wallet on that chain |
| `409` | `idempotency-conflict` | a concurrent request with the same `idempotencyKey` is still processing |
| `409` | `partner-onboarding-pending` | the account holder's verification is still pending. **Retryable** once approved |
| `422` | `idempotency-conflict` | the key was reused with a different body, or replays a failed attempt (use a new key) |
| `422` | `ambiguous-lane` | multiple providers match the currency pair and no lane preference is configured to break the tie |
| `422` | `partner-onboarding-rejected` | the account holder's verification was rejected or revoked; provisioning cannot proceed |
# Get a virtual bank account
Source: https://docs.venlyfinance.com/api-reference/Finance-API/virtual-bank-accounts/get-virtual-bank-account-details
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/virtual-bank-accounts/{virtualBankAccountId}
Retrieve a single virtual bank account by ID.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the virtual bank account, including its bank coordinates (IBAN/BIC for EUR, account and routing number for USD) and the `referenceCode` to quote on incoming transfers so settlement is matched to this account.
# List virtual bank accounts
Source: https://docs.venlyfinance.com/api-reference/Finance-API/virtual-bank-accounts/list-virtual-bank-accounts
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/virtual-bank-accounts
List the virtual bank accounts assigned to an account.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the account's [virtual bank accounts](/guides/finance/glossary) (EUR SEPA IBANs and USD ACH accounts) used to receive fiat pay-ins.
# Prepare a wallet-ownership proof
Source: https://docs.venlyfinance.com/api-reference/Finance-API/virtual-bank-accounts/prepare-a-wallet-ownership-proof
api-reference/Finance-API-Specs.yaml POST /accounts/{accountId}/virtual-bank-accounts/prepare
Get the exact message a self-custody customer must sign to prove they control the destination wallet.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the message a self-custody customer must sign to prove they control the destination wallet of a virtual bank account.
Nothing is persisted — there is no preparation id and no expiry, so two calls for the same account and wallet return an equivalent message.
The customer must sign `message` **verbatim and blind**. It embeds an opaque provider token, so do not reformat, re-encode, pretty-print, or reconstruct it — the signature is checked against the exact bytes, and a mismatch fails with `signature-mismatch`.
Submit the message and signature in the `ownershipProof` block of [Create a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account).
Not every account needs this step. Call it to find out: if a proof is required you get the message to sign, and if it isn't you get `400 ownership-proof-not-applicable` and can create the account directly.
Only an externally-owned account can sign — the scheme is EIP-191, and ERC-1271 contract signatures are not supported. Self-custody wallets are [single-key wallets only](/guides/finance/managed-vs-self-custody#single-key-wallets-only) for now.
## Errors
The codes this endpoint can return, in addition to the [standard errors](/guides/finance/errors#standard-errors). Branch on `code`, never the message.
| HTTP | `code` | When |
| - | - | - |
| `400` | `validation-error` | `walletAddress` or `blockchain` is missing |
| `400` | `invalid-request` | unsupported `blockchain` |
| `400` | `ownership-proof-not-applicable` | this account does not require a customer-signed wallet-ownership proof |
| `400` | `account-holder-unresolved` | the account does not resolve to exactly one active account-holder party |
| `404` | `account-not-found` | no such account for your company |
| `404` | `wallet-not-found` | `walletAddress` is not this account's self-custody wallet on `blockchain` |
# List wallets
Source: https://docs.venlyfinance.com/api-reference/Finance-API/wallets/list-wallets-for-an-account
api-reference/Finance-API-Specs.yaml GET /accounts/{accountId}/wallets
List an account's wallets with their per-asset balances.
**Requires scope:** `manage:accounts` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the account's wallets, each with its `address` and per-asset balances. Balance amounts (`total`, `available`, `reserved`) are decimal strings — `reserved` is the portion held against an in-flight operation. Wallet `type` is `VENLY_MANAGED` or `SELF_CUSTODY` (see [custody models](/guides/finance/managed-vs-self-custody)).
**Concept guide:** [Wallets & balances](/guides/finance/wallets)
# Delete a webhook
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/delete-a-webhook
api-reference/Finance-API-Specs.yaml DELETE /webhooks/{webhookId}
Remove a webhook registration and stop event delivery to it.
**Requires scope:** `manage:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Removes a webhook registration. Venly stops delivering events to this endpoint.
# Get a webhook delivery
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/get-a-webhook-delivery
api-reference/Finance-API-Specs.yaml GET /webhooks/{webhookId}/deliveries/{deliveryId}
One delivery in full — the payload sent and every attempt's outcome.
**Requires scope:** `view:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns the exact `payload` that was POSTed to your endpoint and, per attempt, the HTTP status and body your endpoint returned or the transport error when there was none.
Request and response **headers are never returned** — they carry your endpoint's own API key or basic-auth credentials.
# Get webhook details
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/get-webhook-details
api-reference/Finance-API-Specs.yaml GET /webhooks/{webhookId}
Retrieve a single registered webhook.
**Requires scope:** `view:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns a single webhook registered for your company. Authentication secrets are omitted.
# List webhook deliveries
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/list-webhook-deliveries
api-reference/Finance-API-Specs.yaml GET /webhooks/{webhookId}/deliveries
Delivery history for one webhook, most recent first.
**Requires scope:** `view:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Lists what was delivered to one of your webhooks and whether your endpoint accepted it, so a missed event can be debugged without contacting support. Filter by `eventType`, `status` and time. See [Debugging deliveries](/guides/finance/webhooks#debugging-deliveries).
**Cursor pagination**, not `page`/`size` — send `nextPageCursor` / `previousPageCursor` back exactly as received. How far back the history reaches is the delivery service's retention window; treat older history as unavailable rather than as proof nothing was sent.
Per-attempt response codes and bodies are on [Get a webhook delivery](/api-reference/Finance-API/webhooks/get-a-webhook-delivery), not on this list.
# List webhooks
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/list-webhooks
api-reference/Finance-API-Specs.yaml GET /webhooks
List every webhook registered for your company.
**Requires scope:** `view:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Returns every webhook registered for your company. This list is not paginated.
Authentication secrets are omitted from the response.
# Ping a webhook
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/ping-a-webhook
api-reference/Finance-API-Specs.yaml POST /webhooks/{webhookId}/ping
Send a synthetic PING event to validate your endpoint.
**Requires scope:** `manage:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Queues a synthetic `PING` event for a single registered webhook so you can validate your endpoint end-to-end — including the authentication header your handler expects.
Delivery is **asynchronous**. A `200` means the ping was accepted for delivery, not that your endpoint received it. Check your own logs to confirm arrival.
# Register a webhook
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/register-a-webhook
api-reference/Finance-API-Specs.yaml POST /webhooks
Register an HTTPS endpoint to receive asynchronous Venly Finance events.
**Requires scope:** `manage:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Registers a webhook endpoint for your company so Venly can push asynchronous events — pay-in session transitions, pay-out status changes, verification verdicts and settlement outcomes — instead of you polling for them.
The `url` must be an absolute `https://` endpoint.
Pick an `authenticationMethod` so your endpoint can verify the caller:
| Type | Fields | Behaviour |
| - | - | - |
| `API_KEY` | `headerName`, `apiKey` | The key is injected into the header you name |
| `BASIC_AUTHENTICATION` | `username`, `password` | Standard HTTP basic auth |
Secrets (`apiKey`, `password`) are write-only. They are accepted here and never echoed back on any read response, so store your own copy — to change one, send a full [update](/api-reference/Finance-API/webhooks/update-a-webhook).
After registering, [send a ping](/api-reference/Finance-API/webhooks/ping-a-webhook) to validate your endpoint end-to-end.
# Resend a webhook delivery
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/resend-a-webhook-delivery
api-reference/Finance-API-Specs.yaml POST /webhooks/{webhookId}/deliveries/{deliveryId}/resend
Replay a failed or discarded delivery with its original payload.
**Requires scope:** `manage:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Re-sends the **original payload** as a further attempt on the same delivery. The `notificationId` is unchanged, so deduplication at your endpoint will not discard it — this is a genuine retry, not a new event.
Only a delivery in `FAILURE` or `DISCARDED` can be resent; anything else is `400 delivery-not-resendable`. The response shows the delivery re-queued, normally `SCHEDULED`.
# Update a webhook
Source: https://docs.venlyfinance.com/api-reference/Finance-API/webhooks/update-a-webhook
api-reference/Finance-API-Specs.yaml PUT /webhooks/{webhookId}
Replace a registered webhook's URL, name, or authentication.
**Requires scope:** `manage:webhooks` — see [Required scopes](/getting-started/authentication#required-scopes-finance-api).
Replaces the configuration of an existing webhook.
This is a full replacement, not a partial patch. Send every field you want to keep — including `authenticationMethod` with its secret, since secrets are never returned by a read and so cannot be carried over implicitly.
Use this to rotate an API key or basic-auth password.
# Get bank account configuration
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/bank-account-configuration/get-bank-account-configuration
api-reference/Fundflow-API.yaml GET /v1/bank-accounts/config
Retrieve enabled bank account types, supported countries, and currencies.
Returns configuration for bank accounts: the enabled account types (SEPA, ACH, SWIFT, and more), supported countries, and supported currencies.
**Concept guide:** [Account management](/guides/payments/accounts)
# Get supported blockchain networks
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/blockchain-networks/get-supported-blockchain-networks
api-reference/Fundflow-API.yaml GET /v1/chains
Retrieve the blockchain networks supported for crypto operations.
Returns all blockchain networks supported by the platform for cryptocurrency operations.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# Whitelist a company bank account
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-bank-accounts/create-a-company-bank-account
api-reference/Fundflow-API.yaml POST /v1/company-bank-accounts
Register a company bank account for fiat settlement in ramp requests.
Registers a new company bank account in `PENDING` status. Provide the fields for your account type — `EUR_SEPA`, `USD_WIRE`, `USD_ACH`, `USD_SWIFT`, `GBP_FPS`, `GBP_CHAPS`, or `OTHER_SWIFT`. The account must be in your company's legal name.
**Concept guide:** [Account management](/guides/payments/accounts)
Bank accounts are reviewed manually and may take 1–2 business days to reach `VERIFIED`. Only verified accounts can be used in ramp requests.
# Get company bank account by ID
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-bank-accounts/get-company-bank-account-by-id
api-reference/Fundflow-API.yaml GET /v1/company-bank-accounts/{id}
Fetch details for a specific company bank account.
Returns detailed information about a single company bank account.
**Concept guide:** [Account management](/guides/payments/accounts)
# List company bank accounts
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-bank-accounts/list-company-bank-accounts
api-reference/Fundflow-API.yaml GET /v1/company-bank-accounts
Retrieve your company bank accounts, with optional filtering.
Returns your company's bank accounts with optional filtering by verification status and supported ramp type.
**Concept guide:** [Account management](/guides/payments/accounts)
# Update company bank account
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-bank-accounts/update-company-bank-account
api-reference/Fundflow-API.yaml PATCH /v1/company-bank-accounts/{id}
Update the name of a company bank account.
Updates the name of a company bank account. Send the current `version` for optimistic locking.
**Concept guide:** [Account management](/guides/payments/accounts)
# Whitelist a company wallet
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-wallets/create-a-company-wallet
api-reference/Fundflow-API.yaml POST /v1/company-wallets
Register a company crypto wallet for use in ramp requests.
Registers a new company wallet in `PENDING` status. To verify ownership, send 1 USDC from the wallet to the deposit address returned by [Get deposit wallets](/api-reference/Fundflow-API/deposit-wallets/list-deposit-wallets).
**Concept guide:** [Account management](/guides/payments/accounts)
New wallets start as `PENDING` and must be verified before use. Never share private keys or seed phrases — only the wallet address is required.
# Get company wallet by ID
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-wallets/get-company-wallet-by-id
api-reference/Fundflow-API.yaml GET /v1/company-wallets/{id}
Fetch details for a specific company wallet.
Returns detailed information about a single company wallet.
**Concept guide:** [Account management](/guides/payments/accounts)
# List company wallets
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-wallets/list-company-wallets
api-reference/Fundflow-API.yaml GET /v1/company-wallets
Retrieve your company crypto wallets, filterable by status, chain, and address.
Returns your company's cryptocurrency wallets with optional filtering by verification status, blockchain network, and wallet address.
**Concept guide:** [Account management](/guides/payments/accounts)
# Update company wallet
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company-wallets/update-company-wallet
api-reference/Fundflow-API.yaml PATCH /v1/company-wallets/{id}
Update the description of a company wallet.
Updates the description of a company wallet.
**Concept guide:** [Account management](/guides/payments/accounts)
# Get company details
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/company/get-company-details
api-reference/Fundflow-API.yaml GET /v1/company
Retrieve your company's details and KYB status.
Returns details about the authenticated user's company, including `kybStatus` and the invoices URL.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
Your company must have `kybStatus: VERIFIED` before you can create ramp requests.
# Get crypto currency by ID
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/crypto-currencies/get-crypto-currency-by-id
api-reference/Fundflow-API.yaml GET /v1/crypto-currencies/{id}
Fetch details for a specific cryptocurrency.
Returns detailed information about a single cryptocurrency.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# List all crypto currencies
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/crypto-currencies/list-all-crypto-currencies
api-reference/Fundflow-API.yaml GET /v1/crypto-currencies
Retrieve the cryptocurrencies supported for ramp operations.
Returns all cryptocurrencies supported by the platform for ramp operations, organised by blockchain network.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# Get deposit wallets
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/deposit-wallets/list-deposit-wallets
api-reference/Fundflow-API.yaml GET /v1/deposit-wallets
Retrieve Venly deposit wallet addresses for verification and off-ramp settlement.
Returns the Venly deposit wallet addresses per chain. Send 1 USDC here to verify ownership of a company wallet, and send crypto here when settling an off-ramp request.
**Concept guide:** [Account management](/guides/payments/accounts)
# Calculate fee for a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/fees/calculate-fee-for-a-ramp-request
api-reference/Fundflow-API.yaml POST /v1/fees/calculate
Preview the fee for a given ramp amount before creating a request.
Calculates the fee amount and percentage for a given amount and ramp type, based on your company's configured fee tiers. Call this before creating large requests to understand the cost.
**Concept guide:** [Fee structure](/guides/payments/fees)
# Get company fee configuration
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/fees/get-company-fee-configuration
api-reference/Fundflow-API.yaml GET /v1/fees
Retrieve your company's fee tiers.
Returns the fee configuration for your company, including any volume-based tiers.
**Concept guide:** [Fee structure](/guides/payments/fees)
# Get fiat currency by ID
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/fiat-currencies/get-fiat-currency-by-id
api-reference/Fundflow-API.yaml GET /v1/fiat-currencies/{id}
Fetch details for a specific fiat currency.
Returns detailed information about a single fiat currency.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# List all fiat currencies
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/fiat-currencies/list-all-fiat-currencies
api-reference/Fundflow-API.yaml GET /v1/fiat-currencies
Retrieve the fiat currencies supported for ramp operations.
Returns all fiat currencies supported by the platform for ramp operations (EUR, USD, GBP).
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# Approve a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/approve-a-ramp-request
api-reference/Fundflow-API.yaml POST /v1/ramp-requests/{id}/approve
Approve a pending request to move it to AWAITING_FUNDS.
A Company Admin approves a request created by a Manager. After approval the status changes to `AWAITING_FUNDS` and deposit details are returned — a bank account for on-ramp, a wallet address for off-ramp. Send the current `version` for optimistic locking.
**Concept guide:** [Transactions](/guides/payments/transactions)
Requires the `COMPANY_ADMIN` role. Once approved, a request moves to `AWAITING_FUNDS` and can no longer be cancelled.
# Cancel a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/cancel-a-ramp-request
api-reference/Fundflow-API.yaml POST /v1/ramp-requests/{id}/cancel
Cancel your own pending request before it is approved.
The Company Manager or Admin that created a request can cancel it while it is still in `AWAITING_APPROVAL`; the status changes to `CANCELLED`. Send the current `version` for optimistic locking.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Create a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/create-a-new-ramp-request
api-reference/Fundflow-API.yaml POST /v1/ramp-requests
Start an on-ramp (fiat to crypto) or off-ramp (crypto to fiat) request.
Creates a new ramp request in `AWAITING_APPROVAL` status. For **ON\_RAMP**, specify the fiat amount, the fiat and crypto currencies, and a verified company wallet; for **OFF\_RAMP**, specify the crypto amount, the currencies, and a verified company bank account. A unique payment reference is generated so Venly can match your transfer.
**Concept guide:** [Transactions](/guides/payments/transactions)
Your company must be KYB-**VERIFIED**, and the target company wallet (ON\_RAMP) or bank account (OFF\_RAMP) must be **VERIFIED**. See [Account management](/guides/payments/accounts).
# Edit the amount of a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/edit-the-amount-of-a-ramp-request
api-reference/Fundflow-API.yaml PUT /v1/ramp-requests/{id}/amount
Update the incoming amount of a pending ramp request.
Updates the incoming amount — fiat for on-ramp, crypto for off-ramp — while the request is still in `AWAITING_APPROVAL`. The outgoing amount is recalculated from current exchange rates and fees. Send the current `version` for optimistic locking.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Export ramp requests
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/export-ramp-requests
api-reference/Fundflow-API.yaml GET /v1/ramp-requests/export
Download all your ramp requests as a file for accounting and auditing.
Exports your company's ramp requests as a downloadable file (CSV/Excel) for accounting, auditing, and record-keeping.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Get available off-ramp currency pairs
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/get-available-off-ramp-currency-pairs
api-reference/Fundflow-API.yaml GET /v1/ramp-requests/off-ramp/pairs
List supported crypto-to-fiat currency pairs for off-ramp requests.
Returns the combinations of supported crypto currencies and the fiat currencies they can be converted to. Check the available pairs before creating an off-ramp request.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Get available on-ramp currency pairs
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/get-available-on-ramp-currency-pairs
api-reference/Fundflow-API.yaml GET /v1/ramp-requests/on-ramp/pairs
List supported fiat-to-crypto currency pairs for on-ramp requests.
Returns the combinations of supported fiat currencies and the crypto currencies they can be converted to. Check the available pairs before creating an on-ramp request.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Get ramp request by ID
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/get-ramp-request-by-id
api-reference/Fundflow-API.yaml GET /v1/ramp-requests/{id}
Fetch full details, amounts, status, and event history for one ramp request.
Returns the current status, fiat and crypto amounts, fees, the associated wallet or bank account, payment information, and the complete event history for a ramp request.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Initiate an off-ramp request with transaction hash
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/initiate-a-ramp-request-with-transaction-hash
api-reference/Fundflow-API.yaml PATCH /v1/ramp-requests/{id}/initiate
Mark an off-ramp request as initiated by submitting its on-chain transaction hash.
After sending crypto to the Venly deposit wallet, submit the `blockchainTransactionHash` to initiate processing of an off-ramp request.
**Concept guide:** [Transactions](/guides/payments/transactions)
# List all ramp requests
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/list-all-ramp-requests
api-reference/Fundflow-API.yaml GET /v1/ramp-requests
Retrieve a paginated, filterable list of your on-ramp and off-ramp requests.
Returns ramp requests for your company with optional filtering by `rampType`, `status`, date range, and `paymentReference`, plus sorting and pagination.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Reject a ramp request
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/reject-a-ramp-request
api-reference/Fundflow-API.yaml POST /v1/ramp-requests/{id}/reject
Reject a pending request created by a Manager.
A Company Admin rejects a request that is in `AWAITING_APPROVAL`; the status changes to `REJECTED`. Send the current `version` for optimistic locking.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Update transaction hash
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/ramp-requests/update-transaction-hash
api-reference/Fundflow-API.yaml PATCH /v1/ramp-requests/{id}/tx-hash
Add or update the blockchain transaction hash for a ramp request.
Sets or corrects the `blockchainTransactionHash` for a ramp request so Venly can match your on-chain transfer. Send the current `version` for optimistic locking.
**Concept guide:** [Transactions](/guides/payments/transactions)
# Delete a company user
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/users/delete-a-company-user
api-reference/Fundflow-API.yaml DELETE /v1/company/users/{userId}
Permanently remove a user from your company.
Removes a user from your company.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
This action is permanent and cannot be undone. Requires the `COMPANY_ADMIN` role.
# Invite a user to the company
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/users/invite-a-user-to-the-company
api-reference/Fundflow-API.yaml POST /v1/company/users/invite
Send an email invitation for a new user to join your company.
Invites a new user by email and assigns their role. The user receives an invitation link to complete their registration.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
Requires the `COMPANY_ADMIN` role (the `manage:company-users` scope).
# List company users
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/users/list-company-users
api-reference/Fundflow-API.yaml GET /v1/company/users
Retrieve all users in your company with their roles and status.
Returns every user associated with your company, including each user's role (`COMPANY_ADMIN`, `COMPANY_MANAGER`, or `COMPANY_VIEWER`) and account status.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
# Update user role
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/users/update-user-role
api-reference/Fundflow-API.yaml PUT /v1/company/users/{userId}/role
Change a company user's role.
Updates a user's role — `COMPANY_ADMIN` (full access plus user management), `COMPANY_MANAGER` (create and manage ramp requests), or `COMPANY_VIEWER` (read-only access).
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
Requires the `COMPANY_ADMIN` role (the `manage:company-users` scope).
# Update user status
Source: https://docs.venlyfinance.com/api-reference/Fundflow-API/users/update-user-status
api-reference/Fundflow-API.yaml PATCH /v1/company/users/{userId}/status
Enable or disable a company user account.
Enables or disables a user account. Disabled users cannot access the system.
**Concept guide:** [Getting started with Fundflow](/guides/payments/getting-started)
Requires the `COMPANY_ADMIN` role (the `manage:company-users` scope).
# Finance API
Source: https://docs.venlyfinance.com/api-reference/introduction
Embedded financial infrastructure — parties, accounts, wallets, bank rails, pay-ins, pay-outs, and transfers.
The Finance API lets you onboard and verify parties, open accounts, assign bank deposit rails, generate blockchain wallets, and move money in both directions — fiat in, fiat out, and crypto transfers — through a single RESTful API.
## Base URLs
| Environment | URL |
| - | - |
| Production | `https://api.venlyfinance.com/v1` |
| Staging | `https://api-staging.venlyfinance.com/v1` |
***
## Authentication
All endpoints require a Bearer token in the `Authorization` header. Obtain one using OAuth2 client credentials:
```bash theme={null}
curl -X POST https://login.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"
```
Then pass the token on every request:
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/parties \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"partyType": "INDIVIDUAL",
"firstName": "Jane",
"lastName": "Doe"
}'
```
Tokens expire after **5 minutes**. See the [Authentication Guide](/getting-started/authentication) for token refresh patterns.
***
## Idempotency, pagination, versioning
Both APIs follow shared conventions for idempotent retries, list pagination, and version stability. See the [API Conventions](/getting-started/conventions) page for the full rules.
***
## Data model
Every Finance API resource hangs off a **party**. Parties hold accounts; accounts hold wallets, virtual bank accounts, and pay-out routes. Wallets are the crypto entry point, virtual bank accounts bring fiat in, and pay-out routes send fiat back out.
```mermaid theme={null}
flowchart TD
Company[Your Company] --> PartyA[Party A · individual/organisation]
Company --> PartyB[Party B · individual/organisation]
PartyA --> AccountA[Account A]
PartyB --> AccountB[Account B]
AccountA --> VBA_A[Virtual Bank Account · pay-in rails]
AccountA --> WalletA[Account wallet · per chain]
AccountA --> RouteA[Payout route · pay-out rails]
AccountB --> VBA_B[Virtual Bank Account · pay-in rails]
AccountB --> WalletB[Account wallet · per chain]
VBA_A -. fiat in .-> AccountA
WalletA -. crypto transfers .-> AccountA
RouteA -. fiat out .-> PartyA
VBA_B -. fiat in .-> AccountB
WalletB -. crypto transfers .-> AccountB
```
New here? Start with [Getting started](/guides/finance/getting-started) for the party → account → verification → bank rails flow, read the [data model](/guides/finance/data-model) in depth, or follow the full [Integration Walkthrough](/guides/finance/integration-walkthrough).
***
## Resources
Onboard individuals and organisations. The foundation of every account.
Create and manage financial accounts with role-based party access.
Assign bank deposit rails for EUR SEPA and USD ACH pay-ins.
Mint hosted KYC/KYB links, or forward a verification you already completed.
Generate blockchain wallets for crypto settlement across supported chains.
Execute fiat and crypto transfers between accounts.
Generate pay-in sessions that convert fiat deposits to crypto.
Send crypto from an account wallet and pay a bank beneficiary in fiat.
Allow-list beneficiary bank accounts as pay-out destinations.
Discover which chains and assets your tenant can settle in.
Manage EIP-712 permit signatures for token approvals.
Query token allowances for wallets.
Receive asynchronous events instead of polling for them.
***
## Next Steps
Create a party, open an account, and get verified — with managed and self-custody examples.
The full end-to-end flow, from onboarding to settling a transfer.
KYC, KYB, and the two routes to getting a party verified.
Token exchange, refresh patterns, and environment URLs.
***
## Support
Visit [venlyfinance.com/contact](https://venlyfinance.com/contact) to reach our team.
# Authentication
Source: https://docs.venlyfinance.com/getting-started/authentication
Obtain and use OAuth2 Bearer tokens for the Fundflow API and Finance API.
Both APIs use **OAuth2 client credentials flow**. Every request requires a short-lived Bearer token in the `Authorization` header.
***
## Get an Access Token
Exchange your Client ID and Secret at the token endpoint for your environment:
| Environment | URL |
| - | - |
| Staging | `https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
| Production | `https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
```bash cURL 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"
```
```javascript Node.js theme={null}
const response = await fetch(
'https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
}
);
const { access_token, expires_in } = await response.json();
```
```python Python theme={null}
import os
import requests
response = requests.post(
'https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token',
data={
'grant_type': 'client_credentials',
'client_id': os.environ['CLIENT_ID'],
'client_secret': os.environ['CLIENT_SECRET'],
}
)
access_token = response.json()['access_token']
```
**Response:**
```json theme={null}
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 300,
"token_type": "Bearer"
}
```
Tokens expire after **5 minutes** (300 seconds). Build token refresh into your client — see [Token Refresh](#token-refresh) below.
***
## Use the Token
Pass the `access_token` as a Bearer token on every request:
**Fundflow API:**
```bash theme={null}
curl -X GET https://api-fundflow-staging.venly.io/v1/company \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
**Finance API:**
```bash theme={null}
curl -X GET https://api-staging.venlyfinance.com/v1/parties \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
***
## Required scopes (Finance API)
Your client credentials carry a set of authorities, and each Finance API endpoint requires a specific one. Ask Venly to grant the scopes for the areas you integrate — a token missing the right scope is rejected with `403 forbidden`, not `401`.
| Scope | Grants access to |
| - | - |
| `manage:parties` | Create, read, update and delete [parties](/guides/finance/onboarding/roles-and-entities), and drive [identity verification](/guides/finance/onboarding/lifecycle) |
| `manage:accounts` | [Accounts](/guides/finance/data-model), party roles, [wallets](/guides/finance/wallets), [permits and allowances](/guides/finance/permits-and-allowances), [virtual bank accounts](/guides/finance/virtual-bank-accounts) and [supported-asset discovery](/guides/finance/supported-chains-and-assets) |
| `manage:transfers` | Create and read [fiat and crypto transfers](/guides/finance/transfers) |
| `manage:pay-outs` | [Pay-outs](/guides/finance/payouts), pay-out routes, ownership proofs and payout bank accounts |
| `manage:pay-in-sessions` | Create and read [fiat-to-crypto pay-in sessions](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session), and read the [pay-in ledger](/guides/finance/pay-ins) |
| `manage:webhooks` | Register, replace, ping and delete [webhooks](/guides/finance/webhooks), and resend deliveries |
| `view:webhooks` | Read webhook registrations and [delivery history](/guides/finance/webhooks#debugging-deliveries) |
| `view:billing` | Read and download your [invoices](/guides/finance/billing) |
| `manage:credentials` | Create, rotate and disable [API credentials](/guides/finance/api-credentials). A dashboard-user scope — no API credential holds it |
`manage:accounts` is the broadest — it gates 17 of the endpoints, including everything under an account. Most integrations need it plus whichever of `manage:transfers` and `manage:pay-outs` matches the money movement they do.
`GET /supported-assets` accepts **any** of `manage:accounts`, `manage:transfers` or `manage:pay-in-sessions`, so asset discovery works with whichever scope you already hold.
Each endpoint's reference page states the scope it requires.
***
## Token Refresh
Request a new token before the current one expires. A 30-second buffer is sufficient:
```javascript theme={null}
let accessToken = null;
let tokenExpiresAt = 0;
async function getToken() {
if (Date.now() < tokenExpiresAt - 30_000) return accessToken;
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
});
const data = await res.json();
accessToken = data.access_token;
tokenExpiresAt = Date.now() + data.expires_in * 1000;
return accessToken;
}
```
A `401 Unauthorized` response means the token has expired — re-authenticate and retry the request once.
***
## Environments
| | Staging | Production |
| - | - | - |
| **Token endpoint** | `login-staging.venly.io` | `login.venly.io` |
| **Fundflow API** | `api-fundflow-staging.venly.io/v1` | `api-fundflow.venly.io/v1` |
| **Finance API** | `api-staging.venlyfinance.com/v1` | `api.venlyfinance.com/v1` |
Staging credentials and production credentials are separate. Staging calls do not move real funds.
See [Endpoints & URLs](/getting-started/endpoints) for the complete base URL reference.
***
## Security
* Store `CLIENT_ID` and `CLIENT_SECRET` in environment variables or a secrets manager — never in source code or version control.
* Never log or expose tokens in client-side code.
* Treat a leaked secret as compromised immediately — [rotate it](/guides/finance/api-credentials#rotate) so the old secret stops working at once; tokens already issued expire within minutes.
# API Conventions
Source: https://docs.venlyfinance.com/getting-started/conventions
Idempotency, pagination, and versioning conventions across the Venly APIs, and where they vary by endpoint.
These conventions apply uniformly across the **Fundflow API** and **Finance API**. Endpoint references link here rather than restating the rules — bookmark this page.
***
## Idempotency
State-changing endpoints (`POST` requests that create a resource) accept an `idempotencyKey` field in the request **body**. It is not a header.
```json theme={null}
{
"idempotencyKey": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"amount": 100.00,
"currency": "EUR",
"...": "other fields"
}
```
Use a fresh UUID v4 for every new business operation. Reuse the **same** key only when retrying the **same** request.
### Behaviour
| You send | You get |
| - | - |
| A key for the first time | The operation runs |
| The same key with the same body | The original response, returned again — never a duplicate |
### When to use
| Required | Optional | Not applicable |
| - | - | - |
| Transfers (fiat / crypto / A2A) | Most other `POST` endpoints | `GET` requests (already idempotent) |
| | | `PATCH` on parties/accounts (use the `version` field for optimistic locking) |
| Virtual bank account creation | | `DELETE` requests |
| Fiat-to-crypto payment sessions | | |
**Store the key client-side until you receive a `2xx`.** If your process crashes mid-flight, the same key on retry guarantees the request lands at most once.
A key is bound to one request body. To run a different operation — a new amount or recipient — generate a new key.
***
## Pagination
Pagination is **per endpoint** — not every list is paginated, and one paginated list does not sort.
Check the endpoint's own reference page; the parameters below are the full set where they apply.
| Parameter | Type | Default | Description |
| - | - | - | - |
| `page` | integer | `1` | 1-based page number |
| `size` | integer | `100` | Items per page (minimum 1) |
| `sortOn` | string | `createdAt` | Field to sort by |
| `sortOrder` | string | `DESC` | `ASC` or `DESC`. **Applied only together with `sortOn`** — on its own it is ignored |
**Which endpoints take them** (Finance API):
| Endpoints | Parameters |
| - | - |
| `GET /parties`, `GET /accounts`, `GET /accounts/{id}/transfers`, `GET /accounts/{id}/payouts`, `GET /accounts/{id}/virtual-bank-accounts`, `GET /parties/{id}/payout-bank-accounts` | all four |
| `GET /accounts/{id}/wallets` | `page` and `size` only — no sorting |
| `GET /webhooks`, `GET /supported-assets`, `GET /accounts/{id}/supported-assets`, `GET /accounts/{id}/party-roles`, `GET /accounts/{id}/payout-routes`, and the wallet `permits` / `allowances` reads | **not paginated** — the full list is returned |
An unpaginated list returns everything in one response and carries no `pagination` object. Don't write a
paging loop against those — check for the object rather than assuming it is there.
### Request
```bash theme={null}
curl "https://api.venlyfinance.com/v1/accounts?page=2&size=50&sortOn=createdAt&sortOrder=ASC" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
### Response shape
Every paginated response wraps the result list with a `pagination` object:
```json theme={null}
{
"success": true,
"result": [
{ "id": "...", "...": "..." }
],
"pagination": {
"pageNumber": 2,
"pageSize": 50,
"numberOfElements": 384,
"numberOfPages": 8,
"hasNextPage": true,
"hasPreviousPage": true
}
}
```
**Iterate using `hasNextPage`**, not by computing `pageNumber * pageSize >= numberOfElements`. The total count can shift between requests; the boolean is authoritative.
Without `sortOn`, lists are ordered by `createdAt` **descending** — newest first — and `sortOrder` is
ignored. `?sortOrder=ASC` on its own does not reverse a list; send `sortOn=createdAt&sortOrder=ASC`.
***
## Request headers
Two optional headers apply to every Finance API endpoint.
### Selecting a tenant (`x-tenant-id`)
If your token grants more than one tenant, every request must say which tenant it's for:
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/accounts \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-tenant-id: 3fa85f64-5717-4562-b3fc-2c963f66afa6"
```
With a single-tenant token — the usual case — leave it out. A value you send must exactly match a
tenant the token grants. Missing when required, blank, malformed or naming a tenant the token doesn't
grant: each gets the same generic `403 forbidden`, which reveals neither whether a tenant exists nor
which ones you may use. Send the header **once** — a repeated header is rejected the same way.
### Tracing a call (`X-Correlation-Id`)
Every response, including `401` and `403`, carries an `X-Correlation-Id` header that identifies the
operation in our logs. **Quote it in a support request** and we can find the call, along with anything
we exchanged with a payment provider on its behalf.
You can supply your own, as `X-Correlation-Id` (or `X-Request-Id` as a fallback), to tie our logs to
yours. It must match `^[A-Za-z0-9_.:-]{1,64}$`. A value outside that shape is replaced with a generated
one rather than rejected, so a malformed value never fails a call.
***
## Versioning
Both APIs use **URL path versioning**. The major version is in the base URL:
| API | Base URL |
| - | - |
| Fundflow API | `https://api-fundflow.venly.io/v1` |
| Finance API (production) | `https://api.venlyfinance.com/v1` |
| Finance API (staging) | `https://api-staging.venlyfinance.com/v1` |
### What counts as a breaking change
Breaking changes ship in a **new major version** (e.g. `/v2`). The old version stays live throughout the deprecation window.
| Breaking | Non-breaking (ships within the current version) |
| - | - |
| Removing a field from a response | Adding a new field to a response |
| Renaming a field | Adding a new optional request field |
| Changing a field's type | Adding a new endpoint |
| Making an optional input required | Adding a new enum value |
| Removing an endpoint | Adding a new error code |
| Renaming or removing an enum value | Tightening a previously undocumented validation |
**Treat unknown enum values gracefully.** New enum values are *not* breaking — your client should ignore values it doesn't recognise rather than throwing. This is the most common cause of integrations failing on minor releases.
### Deprecation policy
* New major versions are announced **at least 6 months** before the old version is retired
* Both versions run in parallel during the deprecation window
* Migration guides are published alongside the new version
* Deprecated endpoints return a `Deprecation` HTTP header pointing to the replacement
***
## See also
OAuth2 token exchange, refresh, and environment URLs.
Full base URL reference for every environment.
# Endpoints & URLs
Source: https://docs.venlyfinance.com/getting-started/endpoints
All base URLs and authentication endpoints for Fundflow API and Finance API — staging and production.
Use staging credentials and URLs during development. Staging calls do not move real funds.
**Which host the samples use.** Step-by-step tutorials — [Quick Start](/getting-started/quickstart), the
[Finance API walkthrough](/guides/finance/integration-walkthrough) and
[Getting started](/guides/finance/getting-started) — use the **staging** hosts so you can run them as-is.
Concept guides and endpoint references use the **production** hosts. Swap the host for the environment you
are targeting; nothing else in the request changes.
***
## Authentication
Both the Fundflow API and Finance API use the same OAuth2 token endpoint:
| Environment | URL |
| - | - |
| Staging | `https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
| Production | `https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
**Request** (`application/x-www-form-urlencoded`):
| Parameter | Value |
| - | - |
| `grant_type` | `client_credentials` |
| `client_id` | Your client ID |
| `client_secret` | Your client secret |
Tokens are valid for **5 minutes** (300 seconds). See the [Authentication guide](/getting-started/authentication) for refresh patterns.
***
## Fundflow API
| Environment | Base URL |
| - | - |
| Staging | `https://api-fundflow-staging.venly.io/v1` |
| Production | `https://api-fundflow.venly.io/v1` |
### Key Fundflow endpoints
| Endpoint | Method | Description |
| - | - | - |
| `/company` | GET | Verify KYB status |
| `/ramp-requests` | GET / POST | List or create ramp requests |
| `/ramp-requests/{id}` | GET | Get ramp request by ID |
| `/ramp-requests/{id}/approve` | POST | Approve a ramp request |
| `/ramp-requests/{id}/reject` | POST | Reject a ramp request |
| `/ramp-requests/{id}/cancel` | POST | Cancel a ramp request |
| `/ramp-requests/on-ramp/pairs` | GET | Available on-ramp currency pairs |
| `/ramp-requests/off-ramp/pairs` | GET | Available off-ramp currency pairs |
| `/fees/calculate` | POST | Calculate fee for a ramp request |
| `/company-bank-accounts` | GET / POST | Manage company bank accounts |
| `/company-wallets` | GET / POST | Manage company wallets |
***
## Finance API
| Environment | Base URL |
| - | - |
| Staging | `https://api-staging.venlyfinance.com/v1` |
| Production | `https://api.venlyfinance.com/v1` |
### Key Finance endpoints
| Endpoint | Method | Description |
| - | - | - |
| `/parties` | GET / POST | List or create parties |
| `/parties/{id}` | GET / PATCH / DELETE | Get, update, or delete a party |
| `/accounts` | GET / POST | List or create accounts |
| `/accounts/{id}` | GET | Get account details |
| `/accounts/{id}/party-roles` | GET / POST | List or add party roles on an account |
| `/parties/{id}/verification` | POST | Mint a hosted KYC/KYB verification link |
| `/parties/{id}/iv-verification` | GET | Read verification linkage |
| `/parties/{id}/payout-bank-accounts` | GET / POST | Allow-list pay-out destinations |
| `/accounts/{id}/wallets` | GET | List wallets for an account |
| `/accounts/{id}/virtual-bank-accounts` | GET / POST | List or create bank deposit rails |
| `/accounts/{id}/fiat-to-crypto/payment-sessions` | POST | Create a hosted pay-in session (**coming soon**) |
| `/accounts/{id}/payout-routes` | GET / POST | List or create pay-out routes |
| `/accounts/{id}/payouts` | GET / POST | List or request pay-outs |
| `/accounts/{id}/transfers` | GET | List transfers for an account |
| `/accounts/{senderId}/transfers/crypto` · `/fiat` | POST | Create a crypto or fiat transfer |
| `/webhooks` | GET / POST | List or register webhooks |
Relevant Finance API endpoints accept an `idempotencyKey` field in the request body to prevent duplicate operations on retry. Supply a unique UUID for every new request.
***
## Quick Copy
```bash Staging token 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"
```
```bash Production token theme={null}
curl -X POST https://login.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"
```
# Quick Start
Source: https://docs.venlyfinance.com/getting-started/quickstart
Authenticate, send a request, and read the response — your first Venly API call in under 5 minutes.
Venly Finance offers two separate products — **Fundflow API** for multi-rail payment orchestration, and **Finance API** for embedded financial infrastructure. Both use OAuth2 client credentials for authentication.
***
## Step 1 — Get your credentials
Your **Client ID** and **Client Secret** are provisioned by Venly and sent to you directly. If you haven't received them, [contact us](https://venlyfinance.com/contact).
Use your staging credentials while testing. Staging calls do not move real funds.
***
## Step 2 — Get an access token
Exchange your credentials for a short-lived Bearer token:
```mermaid theme={null}
sequenceDiagram
autonumber
participant App as Your App
participant Auth as login.venly.io
participant API as Venly API
App->>Auth: POST /token (client_credentials)
Auth-->>App: access_token (5 min TTL)
App->>API: GET /company (Bearer token)
API-->>App: company details
App->>API: First domain call (party / ramp request)
API-->>App: 200 OK + resource
```
```bash cURL 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"
```
```javascript Node.js theme={null}
const response = await fetch(
'https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
}),
}
);
const { access_token } = await response.json();
```
Response:
```json theme={null}
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "email profile"
}
```
Tokens expire after **5 minutes** (300 seconds). Implement token refresh logic in your client so requests don't fail mid-session.
Copy the `access_token` — you'll pass it as a Bearer token in every subsequent request.
***
## Step 3 — First Fundflow API call
Call `GET /v1/company` to confirm your Fundflow credentials are working and check your KYB verification status:
```bash cURL theme={null}
curl -X GET https://api-fundflow-staging.venly.io/v1/company \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json"
```
```javascript Node.js theme={null}
const response = await fetch('https://api-fundflow-staging.venly.io/v1/company', {
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
```
A successful response:
```json theme={null}
{
"result": {
"id": "b7e2c1a3-4d5f-6e7a-8b9c-0d1e2f3a4b5c",
"name": "Your Company",
"kybStatus": "VERIFIED"
},
"success": true
}
```
`kybStatus: VERIFIED` means your company is approved to create ramp requests. If it shows `PENDING`, KYB review is still in progress — [contact us](https://venlyfinance.com/contact).
***
## Step 4 — First Finance API call
Call `POST /v1/parties` to create your first party — the foundational record that will hold accounts and wallets:
```bash cURL 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",
"firstName": "Jane",
"lastName": "Doe"
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api-staging.venlyfinance.com/v1/parties', {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
partyType: 'INDIVIDUAL',
firstName: 'Jane',
lastName: 'Doe',
}),
});
const data = await response.json();
```
A successful `201` response:
```json theme={null}
{
"success": true,
"result": {
"id": "f3a2b1c4-8d9e-4f5a-b6c7-d8e9f0a1b2c3",
"partyType": "INDIVIDUAL",
"status": "ACTIVE",
"firstName": "Jane",
"lastName": "Doe",
"createdAt": "2026-01-15T09:30:00Z",
"updatedAt": "2026-01-15T09:30:00Z",
"version": 0
}
}
```
Save the returned `id` — you'll use it to create an account and assign wallets in the next steps.
A party's `status` is **`ACTIVE`** immediately, but it starts unverified — so the **account** you open under it can't move money yet. Send the party through [verification](/guides/finance/onboarding/lifecycle) and the account's `kycStatus` becomes `VERIFIED`. See [Account verification](/guides/finance/kyc-verification).
***
## Next Steps
Create a party, open and verify an account, fund it, and send your first transfer
Set up bank accounts, wallets, and create your first ramp request
Token refresh, environment switching, and credential security
Ramp requests, wallets, fees, currencies, and more
Parties, accounts, wallets, transfers, and bank rails
KYC and KYB — how a party gets verified so its account can move money
Issue wallets to your own users with the Venly Wallet API
# Become a wallet provider
Source: https://docs.venlyfinance.com/getting-started/wallet-provider
No wallet infrastructure? Request Venly Wallet API access and issue wallets to your own users.
The Finance API moves money through **wallets**. If you don't have wallet infrastructure yet, you don't
need to go find a custody provider — you can become one yourself with the **Venly Wallet API**, which
issues and secures wallets for your end-users.
This page gets you from nothing to a working wallet. It's self-contained; the full Wallet API reference
lives at [docs.venly.io](https://docs.venly.io/docs/wallet-api-overview).
## Do you need this?
You have no wallet infrastructure · you want Venly to generate and secure wallets for your users ·
you want to issue wallets under your own brand
Your users already hold their own wallets and you'll register their addresses — that's
[self-custody](/guides/finance/managed-vs-self-custody)
Which path you're on is a **company-level setting** on your Venly tenant, `VENLY_MANAGED` or
`SELF_CUSTODY` — not a per-request choice. See
[Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody), and tell your Venly contact
which model you want.
## Two products, two sets of credentials
Before you start, know that Venly Wallet and Venly Finance are separate products with **separate
authentication**. Mixing up the realm is the single most common early mistake, because the failure looks
like bad credentials rather than a wrong URL.
| | Wallet API | Finance API |
| - | - | - |
| Getting access | [Contact Venly](https://venlyfinance.com/contact) | [Contact Venly](https://venlyfinance.com/contact) |
| Credentials live in | [portal.venly.io/authentication](https://portal.venly.io/authentication) | Provided by your Venly contact |
| Token endpoint realm | `Arkane` | `VenlyFinance` |
| Production token URL | `https://login.venly.io/auth/realms/Arkane/protocol/openid-connect/token` | `https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
| API base URL | `https://api-wallet.venly.io` | `https://api.venlyfinance.com/v1` |
| Token lifetime | \~6 minutes | \~5 minutes |
Both use OAuth2 `client_credentials`. **A Wallet API token will not authenticate a Finance API call**,
and vice versa — you'll hold two tokens.
## Get access and credentials
Venly Wallet API access is **not self-serve**. There is no public signup — you request access and Venly
provisions your account.
[Get in touch with Venly](https://venlyfinance.com/contact) to have a business account provisioned.
Tell them you want Wallet API access, and mention if you also need the
[Finance API](/api-reference/introduction) — the two are provisioned separately.
Once your account exists you'll be able to sign in to
[portal.venly.io](https://portal.venly.io/), where your credentials live.
Go to the authentication section of the portal
([portal.venly.io/authentication](https://portal.venly.io/authentication)) and copy your
**Client ID** and **Client Secret**.
There are two sets, switched with a toggle:
| | Use for |
| - | - |
| **Sandbox** | Testnets. Start here. |
| **Production** | Mainnet. Real value. |
The client secret is a credential — keep it server-side. Never ship it in a browser bundle, mobile app, or public repository.
```bash theme={null}
curl -X POST https://login-sandbox.venly.io/auth/realms/Arkane/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"
```
Then send it on every call:
```
Authorization: Bearer {access_token}
```
Tokens last about 6 minutes. Cache the token and refresh on expiry — don't fetch a new one per
request. A `refresh_token` is returned but is deprecated; just request a new token.
## Issue your first wallet
Three Wallet API concepts, and they nest in this order:
```mermaid theme={null}
flowchart LR
U["User
your end-user"] --> S["Signing method
e.g. a PIN"]
S --> W["Wallet
one per chain"]
```
A **user** is one of your end-users. A **signing method** is how that user authorises actions. A
**wallet** belongs to a user and needs a signing method to create.
Sandbox base URL: `https://api-wallet-sandbox.venly.io`
```bash theme={null}
curl -X POST https://api-wallet-sandbox.venly.io/api/users \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "reference": "user-12345" }'
```
Keep `result.id` — that's your `userId`. Use your own identifier as the `reference` so you can
reconcile against your user table.
```bash theme={null}
curl -X POST https://api-wallet-sandbox.venly.io/api/users/{userId}/signing-methods \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "type": "PIN", "value": "123456" }'
```
Keep `result.id` — that's your `signingMethodId`.
A PIN is the user's authorisation secret. In production, collect it from the user; never hard-code one or reuse a default across users.
```bash theme={null}
curl -X POST https://api-wallet-sandbox.venly.io/api/wallets \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-H "Signing-Method: {signingMethodId}:123456" \
-d '{ "secretType": "BASE", "userId": "{userId}" }'
```
Note the `Signing-Method` header — `{signingMethodId}:{pinValue}`. Wallet creation needs the user's
authorisation, so this isn't optional.
Keep `result.id` and `result.address`.
Your sandbox tenant may already contain a testnet user and wallet preloaded with test assets, so check
before creating your own — list what's there with `GET /api/users?includeSigningMethods=true` and
`GET /api/wallets?userId={userId}`.
### `secretType` and chain names
The Wallet API names chains with `secretType`, and a few differ from the Finance API's `chain` values.
Translate when you cross between the two products:
| Finance API `chain` | Wallet API `secretType` |
| - | - |
| `BASE` | `BASE` |
| `ETHEREUM` | `ETHEREUM` |
| `AVALANCHE` | **`AVAC`** |
| `POLYGON` | **`MATIC`** |
| `ARBITRUM` | `ARBITRUM` |
| `OPTIMISM` | `OPTIMISM` |
| `BSC` | `BSC` |
The Wallet API supports more chains than the Finance API settles on — see
[Supported chains & assets](/guides/finance/supported-chains-and-assets) for what Finance accepts.
## Connecting it to the Finance API
With wallets in hand, you're a wallet provider. Now wire it to Venly Finance:
Tell your Venly contact whether your Finance tenant should be `VENLY_MANAGED` (Venly generates
wallets for accounts) or `SELF_CUSTODY` (you supply addresses). If you're issuing wallets through
the Wallet API yourself and registering those addresses, that's `SELF_CUSTODY`.
Follow the [Finance API quickstart](/getting-started/quickstart). For `SELF_CUSTODY`, pass the
wallet `address` you got above when
[creating the account](/api-reference/Finance-API/accounts/create-a-new-account).
Send the party through [verification](/guides/finance/onboarding/lifecycle) before attempting to
move money.
A self-custody wallet needs a one-time signed [permit](/guides/finance/permits-and-allowances)
before Venly can move its funds. You sign it with the wallet you just created — via the Wallet API's
signing endpoints, using that user's signing method.
## Next steps
The full reference — users, signing methods, wallets, balances, transactions, signing.
Venly's own getting-started guide, in more depth.
Which custody model your Finance tenant should use.
Party → account → verification → bank rails.
# Card Program Setup
Source: https://docs.venlyfinance.com/guides/card-issuance/card-program-setup
Prepare a cardholder account so card authorizations can reserve and settle funds — the Venly-managed and self-custody flows side by side.
When a cardholder pays, the network sends Venly an **authorization** — Venly reserves the amount from the cardholder's wallet, then **settles** it (money leaves to the company's settlement wallet) or **reverses** it (money returns to the cardholder) as the transaction clears. On the Finance API, that authorization **is a payment request**.
This guide takes a cardholder account from creation to a working authorization, and calls out exactly where **Venly-managed** (`VENLY_MANAGED`) and **self-custody** (`SELF_CUSTODY`) companies differ. The difference is one extra step — self-custody wallets need a one-time permit before they can be charged.
**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).
Dedicated card-management endpoints (issue, freeze, list transactions) are on the [roadmap](/guides/card-issuance/introduction). The authorization → settlement plumbing this guide uses is **live today** on the Finance API.
***
## Prerequisites
Your `client_id` must carry the **`finance-api-card`** role plus **`manage:payment-requests`** (and **`manage:all-payment-requests`** if you authorize by card-provider reference). Your Venly contact enables these.
Examples use `curl`. Money-moving calls need an `idempotencyKey` (UUID v4) — `uuidgen`, `[guid]::NewGuid()`, or `crypto.randomUUID()`.
A wallet `address` you'll register on the account, plus access to its **owner key** to sign one permit.
The **`finance-api-card`** role is what provisions the **escrow wallet** every card authorization needs. An account created by a client *without* that role is **non-custodial-only** and rejects authorizations with `escrow-wallet-missing`. The role is applied at **account-create time** — grant it *before* creating the cardholder account; it is not back-filled onto existing accounts.
***
## The flow
```mermaid theme={null}
sequenceDiagram
autonumber
participant App as Your App
participant API as Finance API
participant IV as Verification
App->>API: 1. POST /accounts (cardholder)
API-->>App: accountId (kycStatus VERIFICATION_PENDING)
App->>API: 2. POST /parties/{id}/verification
IV-->>API: Verdict (asynchronous)
App->>API: 3. Fund the account wallet
Note over App,API: 4. SELF_CUSTODY only — sign + submit permits
(wallet PENDING → ACTIVE)
App->>API: 5. POST payment-request (authorization)
API-->>App: status RESERVED
App->>API: 6. Settle or reverse
```
Venly-managed accounts skip step 4 entirely — their wallets are permitted automatically.
***
## Step 1 — Authenticate
```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" }
```
Cache the token. Refresh on `401` or shortly before `expires_in` — don't re-authenticate on every call.
***
## Step 2 — Create the cardholder account
The account represents the cardholder; its wallet funds the card. Optionally attach a **`cardProviderReference`** so authorizations arriving by card-provider reference resolve to this account.
The only structural difference between company types is the wallet `address`:
* **Venly-managed** — Venly creates and holds the wallet. **Omit `address`.**
* **Self-custody** — the cardholder controls the wallet. **Send their `address`** (required).
```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": "cardholder-12345",
"name": "Jane Doe — Card",
"chain": "BASE",
"cardProviderReference": { "type": "PAYMENTOLOGY", "referenceId": "ACC-12345" }
}'
```
```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": "cardholder-12345",
"name": "Jane Doe — Card",
"chain": "BASE",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"cardProviderReference": { "type": "PAYMENTOLOGY", "referenceId": "ACC-12345" }
}'
```
```json Response (201) theme={null}
{
"success": true,
"result": {
"id": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
"externalId": "cardholder-12345",
"name": "Jane Doe — Card",
"kycStatus": "VERIFICATION_PENDING",
"status": "ACTIVE",
"createdAt": "2026-01-15T09:30:00Z",
"version": 0
}
}
```
`cardProviderReference` can only be set when the account is **created**. Decide the card link up front.
***
## Step 3 — Verify the account
A new account is `kycStatus: VERIFICATION_PENDING`. Send the cardholder's party through verification —
mint a hosted link with
[`POST /parties/{partyId}/verification`](/api-reference/Finance-API/parties/create-a-verification-link)
and hand the URL to the cardholder, or forward an existing Sumsub verification at account creation. See
[Onboarding & Verification](/guides/finance/onboarding/lifecycle).
The verdict arrives asynchronously. [Register a webhook](/guides/finance/webhooks) or poll 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 `VERIFIED`, authorizations fail with `account-not-active` ("KYC status must be VERIFIED"). See [Account verification](/guides/finance/kyc-verification).
***
## Step 4 — Fund the wallet
The account wallet must hold the stablecoin the card spends. Read its address and balance:
```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": "VENLY_MANAGED",
"address": "0x01DA4aa698B545D6A32ef44DB37139D3b27eAF61",
"balances": [
{ "asset": "USDC", "contractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "amount": { "total": "0", "available": "0", "reserved": "0" } }
],
"amlStatus": "APPROVED"
}
]
}
```
* **Venly-managed** — fund the returned Venly address with the stablecoin. Gas is covered by Venly's orchestration wallet; the account wallet only needs the token.
* **Self-custody** — the cardholder funds their own `address` with the stablecoin.
***
## Step 5 — Activate the wallet (self-custody only)
Venly-managed wallets are permitted automatically — **skip to Step 6.**
A self-custody wallet is created `PENDING`. Before it can be charged, the owner signs a one-time **permit** for **every** supported asset on the wallet, and the wallet becomes **`ACTIVE` only once all of them are `CONFIRMED`**. Until then authorizations fail with `account-wallet-not-active`.
A raw on-chain `approve()` sets the ERC-20 allowance but does **not** activate the wallet — only a **confirmed permit** does. And it's **all** assets: a wallet that supports USDC *and* EURC needs **both** permits confirmed before it goes `ACTIVE`.
`GET /accounts/{accountId}/wallets/{walletId}/permits` returns, per asset, a `supportedAssetId` and an EIP-712 `typedData` object.
Sign `typedData` with the wallet **owner** key (off-chain, no gas). The signature must recover to the `owner` or the permit becomes `FAILED`. Produces `v`, `r`, `s`.
`POST .../permits` with the `supportedAssetId` and signature → HTTP 200 with `result.status`.
Poll `GET .../permits` until each asset is `CONFIRMED`. When the last one confirms, the wallet activates.
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/wallets/{walletId}/permits \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"supportedAssetId": "5d4c5b99-2ce8-40a1-bc1d-173dd400fa89",
"signature": { "v": "28", "r": "0x...", "s": "0x..." }
}'
```
Full signing example (ethers.js) and the allowance check are in [Approving transfers without gas](/guides/finance/permits-and-allowances).
***
## Step 6 — Create the authorization
With the account `VERIFIED`, funded, and (self-custody) `ACTIVE`, reserve funds. There are two endpoints:
```bash By account theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/{accountId}/payment-requests \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 25.0,
"currency": "USD",
"externalId": "auth-67890",
"description": "Card authorization #67890",
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
```
```bash By card-provider reference theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/payment-requests \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cardProviderReference": { "type": "PAYMENTOLOGY", "referenceId": "ACC-12345" },
"paymentRequest": {
"amount": 25.0,
"currency": "USD",
"externalId": "auth-67890",
"description": "Card authorization #67890",
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}'
```
```json Response theme={null}
{
"success": true,
"result": {
"id": "d4e5f6a7-b8c9-4012-8345-6789abcdef01",
"accountId": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
"amount": { "fiat": 25.0, "crypto": "25.000000" },
"originalAmount": { "fiat": 25.0, "crypto": "25.000000" },
"currency": "USD",
"status": "RESERVED",
"executions": [
{ "type": "AUTHORIZATION", "chain": "BASE", "asset": "USDC", "amount": 25.0, "status": "RESERVED", "transactionHash": "0xa1b2..." }
]
}
}
```
The reserved amount now shows in the wallet's `reserved` balance. Amounts carry both `fiat` and `crypto`.
The reservation runs on-chain. On fast chains the response is `RESERVED` straight away; on others it returns `PENDING` and flips to `RESERVED` once the authorization transaction confirms. There's **no GET endpoint** — re-send the create with the **same `idempotencyKey` and body** to read the latest state, and match on your `externalId`.
***
## Step 7 — Settle or reverse
When the card transaction clears, resolve the hold:
* **Settle** — money moves from escrow to the company's settlement wallet (`SETTLING` → `SETTLED`). The settle amount can be equal to, below, or above the authorized amount.
* **Reverse** — the full reserved amount returns to the cardholder (`REVERSING` → `REVERSED`). Use it to release a hold you won't charge.
Both have **by-reference** variants that take the same `cardProviderReference` instead of the `accountId`.
***
## Managed vs self-custody at a glance
| Step | Venly-managed (`VENLY_MANAGED`) | Self-custody (`SELF_CUSTODY`) |
| - | - | - |
| Create account | No `address` | `address` **required** |
| Wallet custody | Venly holds the key | Cardholder holds the key |
| Funding | Fund the Venly-created address | Cardholder funds their own address |
| Gas | Paid by Venly orchestration | Paid by Venly orchestration |
| **Wallet activation** | Automatic | **Sign a permit for every supported asset** |
| Verify KYC | Required | Required |
| Authorize / settle / reverse | Identical | Identical |
***
## Common pitfalls
The account was created by a client **without `finance-api-card`**, so it's non-custodial-only and has no escrow wallet. Grant the role, then create a **new** cardholder account (escrow isn't back-filled onto existing accounts).
A self-custody wallet that isn't `ACTIVE` yet. Confirm a permit for **every** supported asset on the wallet — the wallet only activates once they're all `CONFIRMED`. A raw `approve()` doesn't count. See [Permits & allowances](/guides/finance/permits-and-allowances).
The account is still `VERIFICATION_PENDING`. Its party must complete [verification](/guides/finance/onboarding/lifecycle) — poll `GET /accounts/{id}` until `kycStatus` is `VERIFIED`.
Self-custody accounts must include the cardholder's wallet `address` on creation. Venly-managed accounts must not.
Tokens last 5 minutes. Catch `401`, refresh, and retry — don't pre-authenticate every request.
***
## Next steps
How `total`, `available` and `reserved` behave.
Gasless wallet activation for self-custody accounts.
Endpoint reference, fields, and error codes.
The white-label card program and its roadmap.
# Card Issuance (Coming Soon)
Source: https://docs.venlyfinance.com/guides/card-issuance/introduction
White-label Mastercard debit cards linked to USDC wallets
**Coming Soon** - Card Issuance is currently in development and will be available soon. This documentation provides an overview of the upcoming features.
## What is Card Issuance?
Venly's Card Issuance product enables you to issue white-label Mastercard debit cards directly linked to USDC wallets. Your users can spend their cryptocurrency holdings anywhere Mastercard is accepted, while you capture interchange revenue on every transaction.
Instant issuance for online and mobile payments
Branded plastic cards for in-person transactions
Direct spending from stablecoin wallets
Earn revenue on every card transaction
***
## Key Features
### White-Label Card Program
Issue cards under your own brand with full customization:
* **Custom Card Design**: Your logo, colors, and branding
* **Branded Cardholder Experience**: Custom mobile app integration
* **Your Business Name**: Cards issued in your company name
* **Direct Program Management**: Full control over card operations
### USDC Wallet Integration
Seamless connection to Venly Finance wallets:
* **Real-Time Balance**: Cards linked directly to USDC wallet balances
* **Instant Settlements**: Transactions settle immediately from wallet
* **Multi-Currency Support**: Automatic conversion at point of sale
* **Unified Treasury**: Single view of wallet and card balances
### Interchange revenue
Capture interchange fees on every card transaction:
* **Monthly Revenue**: Captured on every cardholder transaction
* **Transparent Reporting**: Real-time interchange tracking
* **Direct Settlement**: Revenue paid directly to your account
### Global Acceptance
Mastercard network coverage:
* **200+ Countries**: Accepted worldwide
* **40M+ Merchants**: Online and offline acceptance
* **ATM Withdrawals**: Cash access at Mastercard ATMs
* **Contactless Payments**: NFC-enabled for tap-to-pay
***
## How It Works
Partner with Venly to establish your card program
* Complete Mastercard program application
* Configure card design and branding
* Set up compliance and KYC requirements
* Define spending limits and controls
Your users complete KYC and request cards
* User completes identity verification
* Links USDC wallet to card program
* Selects virtual or physical card
* Card issued instantly (virtual) or shipped (physical)
Cardholders activate and start spending
* Virtual cards: Active immediately
* Physical cards: Activate upon receipt
* Set PIN for ATM and chip transactions
* Configure spending limits and controls
Real-time authorization and settlement
```
1. Cardholder makes purchase
2. Merchant requests authorization
3. Venly checks USDC wallet balance
4. Transaction approved/declined
5. USDC converted to fiat at POS
6. Settlement to merchant
7. Interchange fee captured
```
Earn interchange on every transaction
* Interchange fees calculated automatically
* Revenue tracked in real-time dashboard
* Monthly settlement to your account
* Detailed reporting and analytics
***
## Card Types
### Virtual Cards
**Instant Issuance for Digital Payments**
* **Instant Activation**: Available immediately upon approval
* **Online Shopping**: Use for e-commerce and subscriptions
* **Mobile Wallets**: Add to Apple Pay, Google Pay
* **Secure Transactions**: 3D Secure authentication
* **No Physical Shipping**: Zero delivery time or cost
**Ideal For:**
* Online marketplaces
* Subscription services
* Digital-first businesses
* Quick user onboarding
### Physical Cards
**Branded Plastic Cards for In-Person Use**
* **Custom Design**: Your logo and branding
* **Contactless Enabled**: NFC tap-to-pay
* **Chip & PIN**: EMV chip security
* **Magnetic Stripe**: Legacy terminal support
* **ATM Access**: Cash withdrawals worldwide
**Ideal For:**
* Retail spending
* Travel and hospitality
* Cash access needs
* Premium user experience
***
## Use Cases
### Gaming & iGaming Platforms
Enable players to spend winnings instantly:
* Issue cards to verified players
* Link to player wallet balances
* Instant access to winnings
* Capture interchange on player spending
**Revenue Potential**: Capture interchange revenue on every player transaction
### Crypto Exchanges & Trading Platforms
Let traders spend crypto holdings:
* Convert crypto to spendable cards
* Real-time balance updates
* Multi-currency wallet support
* Seamless fiat off-ramp
### Neobanks & Fintechs
Complete banking experience:
* Accounts + Wallets + Cards in one platform
* Unified customer experience
* Additional revenue stream
* Competitive differentiation
### Corporate Expense Management
Issue cards for business spending:
* Employee expense cards
* Department budget controls
* Real-time spend tracking
* Automated reconciliation
***
## Interchange Revenue Model
### How Interchange Works
Every card transaction generates an interchange fee. The acquirer pays it to the issuer, and the
merchant pays its acquirer a separate merchant discount — so interchange is not a charge levied
directly on the merchant.
A share of that interchange is passed to you on each transaction.
### Revenue Potential
Interchange revenue scales with card spend, and larger programmes improve per-card economics. The
rate itself varies by merchant category, card type and transaction type.
Rates, revenue share and any per-card fees are commercial terms rather than product documentation —
they are set per programme. For the figures that apply to you, or to request a quote, get in touch at
[venlyfinance.com/contact](https://venlyfinance.com/contact).
***
## Card Controls & Security
### Spending Limits
Flexible controls for risk management:
* **Per-Transaction Limits**: Maximum single transaction amount
* **Daily Limits**: Total daily spending cap
* **Monthly Limits**: Monthly spending budget
* **ATM Withdrawal Limits**: Cash withdrawal restrictions
* **Merchant Category Restrictions**: Block specific merchant types
### Security Features
Multi-layer security protection:
* **3D Secure**: Additional authentication for online purchases
* **Real-Time Fraud Detection**: AI-powered transaction monitoring
* **Instant Card Freeze**: Users can freeze cards instantly
* **Biometric Authentication**: Fingerprint/Face ID for mobile
* **Tokenization**: Secure token-based transactions
### Compliance
Built-in regulatory compliance:
* **KYC/AML**: Automated identity verification
* **Transaction Monitoring**: Real-time suspicious activity detection
* **Sanctions Screening**: Automatic sanctions list checking
* **Regulatory Reporting**: Automated compliance reports
* **PCI DSS Compliant**: Secure card data handling
***
## Integration Overview
### API Endpoints (Coming Soon)
**Not stable — do not build against these yet.** The endpoints and payloads below illustrate the intended
shape and **will still change** before release. Treat them as a preview of the design, not a contract: they
are not callable today, and anything implemented against them now is likely to break.
```bash theme={null}
# Issue a new card
POST /v1/cards
{
"cardholderAccountId": "account-id",
"cardType": "VIRTUAL",
"walletId": "usdc-wallet-id",
"spendingLimits": {
"daily": 1000,
"monthly": 10000
}
}
# Get card details
GET /v1/cards/{cardId}
# Freeze/Unfreeze card
POST /v1/cards/{cardId}/freeze
POST /v1/cards/{cardId}/unfreeze
# List transactions
GET /v1/cards/{cardId}/transactions
# Get interchange revenue
GET /v1/cards/interchange-revenue
```
### Webhook Events
Real-time notifications for card events:
* `card.issued` - New card created
* `card.activated` - Card activated by user
* `card.frozen` - Card frozen
* `transaction.authorized` - Transaction approved
* `transaction.declined` - Transaction declined
* `transaction.settled` - Transaction completed
* `interchange.earned` - Interchange revenue captured
***
## Mastercard Program Manager
Venly is a **Mastercard Program Manager**, providing:
✅ **Direct Issuance Capability**: Issue cards without intermediaries\
✅ **Program Management**: Full control over card operations\
✅ **Compliance Support**: Regulatory guidance and support\
✅ **Technical Integration**: Complete API and webhook infrastructure\
✅ **Settlement Services**: Direct settlement to your accounts
***
## Roadmap
### Phase 1 — Initial launch
* Virtual card issuance
* USDC wallet integration
* Basic spending controls
* Interchange revenue tracking
* API and webhooks
### Phase 2 — Enhanced features
* Physical card issuance
* Multi-currency support (EURC, USDT)
* Advanced fraud detection
* Mobile SDK for card management
* Enhanced reporting and analytics
### Phase 3 — Enterprise features
* Corporate card programs
* Multi-level approval workflows
* Advanced spending controls
* Custom card designs
* White-label mobile app
***
## Getting Started
### Prerequisites
To participate in the Card Issuance program, you'll need:
✅ **Active Venly Finance Account**: Verified company account\
✅ **Mastercard Program Approval**: Complete program application\
✅ **Compliance Requirements**: KYC/AML procedures in place\
✅ **Technical Integration**: API integration capability
### Early Access Program
Register your interest for early access to Card Issuance
**Benefits:**
* Priority onboarding when available
* Discounted launch pricing
* Dedicated integration support
* Beta testing opportunity
**Contact**: [venlyfinance.com/contact](https://venlyfinance.com/contact)
***
## Comparison: Card Issuance vs Traditional PSPs
| Feature | Traditional PSPs | Venly Card Issuance |
| - | - | - |
| **Setup Time** | 6-12 months | 2-4 weeks |
| **Integration Complexity** | High (multiple vendors) | Simple (one API) |
| **Crypto Integration** | Limited/None | Native USDC support |
| **Settlement Time** | T+2 to T+5 | Real-time |
| **White-Label** | Limited customization | Full branding control |
***
## Frequently Asked Questions
At launch, cards will be linked to USDC wallets on Base. Support for further assets is planned.
Virtual cards are issued instantly upon approval. Physical cards are produced and shipped within 5-7 business days.
Default limits are €1,000 per transaction, €5,000 daily, and €20,000 monthly. You can configure custom limits per cardholder.
Interchange is calculated as a percentage of each transaction and varies by merchant category. Your revenue share is agreed commercially — ask your Venly contact.
Cardholders must complete KYC verification. Your company must have AML procedures and transaction monitoring in place. Venly provides compliance tools and support.
Yes, physical cards can be fully customized with your logo, colors, and branding. Virtual cards display your branding in mobile wallets.
Transactions are declined in real-time if the USDC wallet balance is insufficient. Users receive instant notifications to add funds.
Yes, cardholders can withdraw cash at any Mastercard-enabled ATM worldwide. Standard ATM fees apply.
***
## Next Steps
Register for early access
Explore wallet integration
Discuss your card program
Estimate your revenue (coming soon)
***
## Support
Questions about Card Issuance?
* **Contact**: [venlyfinance.com/contact](https://venlyfinance.com/contact)
* **Documentation**: [docs.venlyfinance.com](https://docs.venlyfinance.com)
# API credentials
Source: https://docs.venlyfinance.com/guides/finance/api-credentials
Create, rotate and disable your company's OAuth client credentials yourself — and why the secret is shown exactly once.
Your Finance API access is an OAuth `client_credentials` pair. Until 1.8.0 Venly provisioned it for
you and a compromised secret meant a support ticket. You can now manage it through the API.
## What a credential is
| | |
| - | - |
| `clientId` | Server-generated from your company and the `name` you give — never chosen by you. |
| `secret` | Shown **once**, on create and on rotate. Finance does not store it and no endpoint returns it later. |
| `status` | `ACTIVE` or `DISABLED`. There is no deleted state. |
| Roles | Your company's API access — the customer role plus the transfer role matching your wallet type. |
**Each company holds one credential.** Disabling it does not free the slot: the cap counts disabled
credentials too, because a credential is never deleted. If you believe a secret is compromised, rotate
it — don't try to create a second one.
## Who can call these endpoints
The four credential endpoints require the `manage:credentials` scope, and **no API credential ever
holds it** — it is a dashboard-user capability. That is deliberate: it means no credential can create,
rotate or disable another, so a leaked API secret cannot be used to mint a replacement for itself.
Plan for the consequence: if the only dashboard user holding `manage:credentials` is disabled or
leaves, restoring access to this surface requires Venly.
## Create
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/api-credentials \
-H "Authorization: Bearer {dashboard_user_token}" \
-H "Content-Type: application/json" \
-d '{ "name": "checkout-service" }'
```
```json Response (201) theme={null}
{
"success": true,
"result": {
"clientId": "fin-8f2c1a9b-0000-4000-8000-000000000001-checkout-service",
"name": "checkout-service",
"secret": "generated-value-shown-exactly-once",
"status": "ACTIVE",
"createdAt": "2026-09-03T10:12:00Z"
}
}
```
Store `secret` in your secrets manager **from this response**. The
[list endpoint](/api-reference/Finance-API/api-credentials/list-api-credentials) has no `secret` field.
## Rotate
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/api-credentials/{clientId}/secret \
-H "Authorization: Bearer {dashboard_user_token}"
```
The old secret stops working **immediately**; tokens already issued against it stay valid until they
expire (about five minutes). So the safe sequence is:
Have the place the new secret goes — your secrets manager, your config — ready to accept it.
Call the endpoint and capture `result.secret`.
Roll it out. Any service still holding the old secret fails its **next** token request, not its in-flight calls.
Rotation is refused on a `DISABLED` credential (`409 credential-disabled`) — enable it first.
## Disable and re-enable
```bash theme={null}
curl -X PUT https://api-staging.venlyfinance.com/v1/api-credentials/{clientId}/status \
-H "Authorization: Bearer {dashboard_user_token}" \
-H "Content-Type: application/json" \
-d '{ "status": "DISABLED" }'
```
Disabling is a pause: the credential keeps its `clientId`, secret, roles and history, and simply stops
issuing tokens. Setting `ACTIVE` again restores it unchanged.
**If Venly provisioned more than one credential for your company** before this endpoint existed, the
one-credential cap applies to re-enabling: while one is active, enabling another is refused with
`409 credential-limit-exceeded`, and there is no self-service way past it. For such a company,
disabling is how you converge on a single credential — do it deliberately.
## Errors
| HTTP | `code` | Meaning |
| - | - | - |
| `403` | `forbidden` | The token lacks `manage:credentials`. An API credential's own token always gets this. |
| `404` | `credential-not-found` | Unknown `clientId` — or one belonging to another company. The two are indistinguishable by design. |
| `409` | `credential-limit-exceeded` | Creating a second credential, or enabling one while another is active. |
| `409` | `credential-name-unavailable` | No free `clientId` could be derived from this `name`; choose another. |
| `409` | `credential-disabled` | Rotating a disabled credential. |
| `503` | `keycloak-unavailable` | The identity provider was unreachable. Retry with backoff. |
## Next steps
Turning the credential into a bearer token, and the scopes each endpoint needs.
The endpoint reference.
# Billing
Source: https://docs.venlyfinance.com/guides/finance/billing
Read the invoices Venly has issued to your company, and download them as PDF.
[`GET /billing/invoices`](/api-reference/Finance-API/billing/list-invoices) lists the invoices Venly has
issued to your company. It needs the `view:billing` scope, which is separate from everything else
because the figures are commercially sensitive — ask Venly to grant it to the credential or dashboard
user that needs it. Invoices never show the **rates** behind a charge; for those,
[contact Venly](https://venlyfinance.com/contact).
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/billing/invoices \
-H "Authorization: Bearer {access_token}"
```
```json Response (200) theme={null}
{
"success": true,
"result": {
"content": [
{
"id": "7f68c0d4-4d55-4bd8-9b0e-1cfb0255c554",
"invoiceNumber": "INV-042",
"status": "SENT",
"paymentStatus": "PAID",
"currency": "EUR",
"netTotal": "1250.00",
"totalTax": "262.50",
"grossTotal": "1512.50",
"issueDate": "2026-09-01",
"dueDate": "2026-09-15",
"billingPeriodStart": "2026-08-01",
"billingPeriodEnd": "2026-08-31",
"sequenceUrl": "https://billing.example.com/invoices/7f68c0d4-4d55-4bd8-9b0e-1cfb0255c554",
"createdAt": "2026-09-01T06:00:00Z"
}
],
"pagination": { "size": 100, "hasNextPage": false, "hasPreviousPage": false }
}
}
```
Invoices are read live from our billing system — Finance stores none. Only **issued** invoices
(`status: SENT`) are visible; drafts and voided invoices are not. `paymentStatus` is the separate
collection status: `UNPAID`, `PARTIALLY_PAID`, `PAID` or `UNCOLLECTIBLE`. `sequenceUrl` is Venly's internal
reference to the invoice, not a page you can open. To read your invoices, open the billing portal at
`billingUrl` on [your company profile](/api-reference/Finance-API/company/get-your-company-profile), or
[download the PDF](/api-reference/Finance-API/billing/download-an-invoice-pdf).
### Two things that differ from other list endpoints
1. **Cursor pagination.** There is no `page`/`size` envelope. Send `pagination.nextPageCursor` or
`previousPageCursor` back exactly as received; never construct one. A stale cursor is
`400 invalid-cursor` — restart from the first page.
2. **`422 billing-not-configured` until Venly links your company** to a billing customer. Once linked,
a company with no invoices yet gets an empty `content`.
### The PDF
[`GET /billing/invoices/{invoiceId}/download`](/api-reference/Finance-API/billing/download-an-invoice-pdf)
returns **raw PDF bytes** — not the JSON envelope — with a `Content-Disposition` filename derived from
the invoice number. Errors still arrive as JSON.
A `404` always means the invoice is not yours to read. An invoice you *can* read whose PDF cannot be
produced is `500 billing-provider-error`, so a rendering fault is never mistaken for a wrong id.
## Errors
| HTTP | `code` | Meaning |
| - | - | - |
| `400` | `invalid-cursor` | The cursor is stale or malformed; restart from the first page |
| `403` | `forbidden` | The token lacks `view:billing` |
| `404` | `invoice-not-found` | Unknown, foreign, unsent or voided invoice |
| `422` | `billing-not-configured` | Your company is not linked to a billing customer yet |
| `500` | `billing-provider-error` | The billing system rejected the request or could not render the PDF |
| `503` | `billing-provider-unavailable` | The billing system was unreachable. Retry |
## Next steps
The endpoint reference, with every field.
Where `billingUrl` — your billing portal — lives.
# Finance API changelog
Source: https://docs.venlyfinance.com/guides/finance/changelog
Releases and updates to the Venly Finance API — new endpoints, parameters, supported chains, and improvements.
**Follow a stalled verification (new)**
* **Get a party's KYC status** — `GET /parties/{partyId}/kyc-status`. Where the party's KYC/KYB verification stands, read live: a `verificationState` (`AWAITING_CUSTOMER`, `IN_REVIEW`, `RESUBMISSION_REQUESTED`, `APPROVED`, `DECLINED`), the `requiredAction` the end customer still has to take, and the review outcome per step with reject labels. [Reference](/api-reference/Finance-API/parties/get-a-partys-kyc-status)
* **Issue a KYC completion link** — `POST /parties/{partyId}/kyc-status/completion-link`. Mints a fresh, short-lived link your end customer opens to finish a verification that is waiting on them. `409 kyc-not-completable` when it isn't. [Reference](/api-reference/Finance-API/parties/issue-a-kyc-completion-link)
* **`POST /accounts` returns a completion link up front.** When a forwarded `sumsubToken` still needs the customer to act, the `201` carries an optional `verification` block with `requiredAction`, `completionUrl` and `completionUrlExpiresAt`. Returned on the create response only. [Guide](/guides/finance/onboarding/sumsub-token-sharing#when-the-customer-still-has-to-finish)
* **Pay-out route webhooks** — `PAYOUT_ROUTE_ACTIVATED`, `PAYOUT_ROUTE_AWAITING_OWNERSHIP_PROOF` and `PAYOUT_ROUTE_REJECTED` (with a fixed `reason`). Keyed per occurrence: deduplicate on `payoutRouteId` + `occurrenceId`. You no longer need to poll routes. [Guide](/guides/finance/webhooks#event-catalogue)
* **Get your company profile** — `GET /company`. Your KYB and lifecycle status, wallet type, supported chains and, once billing is set up, a `billingUrl` to your billing portal. [Reference](/api-reference/Finance-API/company/get-your-company-profile)
* **`x-tenant-id` header.** Required when your token grants more than one tenant; omit it otherwise. An invalid selection gets a generic `403`. [Conventions](/getting-started/conventions#request-headers)
* **`X-Correlation-Id` on every response.** Quote it to support. Send your own (or `X-Request-Id`) to tie our logs to yours. [Conventions](/getting-started/conventions#request-headers)
* **Contact emails stay with you.** A party's `email` and a payout bank account's `beneficiaryEmail` are no longer sent to payment providers, which get a Venly-controlled address instead. Omitting either never blocks onboarding or registration. `beneficiaryPhoneNumber` is still forwarded and still needed for USD routes.
* **Paginated lists have a stable default order.** Without `sortOn`, lists now come back by `createdAt` descending — newest first — where the order was previously unspecified, so a row could appear on two pages or none. `sortOrder` applies only together with `sortOn`; on its own it is ignored. [Conventions](/getting-started/conventions#pagination)
* **Pay-in session retries must match the original.** Reusing an `idempotencyKey` with a changed body, on another account, or after a failed attempt answers `422 idempotency-conflict`; a retry with an identical body still returns the original session. [Reference](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session)
* Pay-in session `successRedirectUrl` and `failureRedirectUrl` are capped at 2000 characters.
* Invoice `sequenceUrl` is clarified as an internal reference you can't open — use `billingUrl` on the company profile.
* **Virtual bank accounts accept deposits from any payer.** A transfer from a bank account other than the holder's own was sent back; it is now accepted. No API changes. [Guide](/guides/finance/virtual-bank-accounts)
**A new account inherits its holder's verification.** Linking a `partyId` that is already `VERIFIED`
now creates the account `VERIFIED` instead of `VERIFICATION_PENDING`, so a customer's second account
needs no second verification. A rejected holder yields a `REJECTED` account. Code that assumed every
new account starts pending should read `kycStatus` from the create response instead. [Reference](/api-reference/Finance-API/accounts/create-a-new-account)
**Pay-in ledger (new)**
* **List and get pay-ins** — `GET /pay-ins`, `GET /pay-ins/{payInId}`. One record per fiat credit that settled (or failed to settle) into an account wallet, with gross and net crypto amounts and the on-chain transaction. The `payInId` matches the `PAY_IN_*` webhooks. [Guide](/guides/finance/pay-ins)
* **A pay-in session is now an initiation-only record.** It ends at `PAYMENT_RECEIVED` — `PaymentSessionStatus` no longer has `CONVERTING`, `MINTING` or `COMPLETED`. Conversion and the crypto credit are tracked on the new pay-in and announced by `PAY_IN_SETTLED`. [Guide](/guides/finance/pay-ins)
* **Read pay-in sessions back** — `GET /accounts/{accountId}/fiat-to-crypto/payment-sessions` and `…/{sessionId}`. Sessions could not be read after creation before this release. [Reference](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/list-pay-in-sessions)
* **Close a virtual bank account** — `POST /accounts/{accountId}/virtual-bank-accounts/{id}/close`. Decommissions the bank details at the provider; read `providerClosureStatus` to know whether they are actually dead. Also how you resolve the new `duplicate-eur-vba` — one active EUR virtual bank account per account. [Guide](/guides/finance/virtual-bank-accounts#closing-a-virtual-bank-account)
* Creating a hosted pay-in session gained preconditions on accounts that settle through a EUR virtual bank account: retryable `409 vba-not-ready`, and `422 vba-required` / `ambiguous-eur-vba` / `vba-asset-mismatch` / `unsupported-currency`. A provider refusal now answers `502` rather than `500`.
**Account self-service (new)**
* **API credentials** — `GET`/`POST /api-credentials`, `POST …/{clientId}/secret`, `PUT …/{clientId}/status`. Create, rotate and disable your OAuth credential yourself. The secret is shown exactly once. Requires the dashboard-user scope `manage:credentials`. [Guide](/guides/finance/api-credentials)
* **Invoices** — `GET /billing/invoices`, `…/{invoiceId}`, `…/{invoiceId}/download` (PDF). Cursor-paginated. Scope `view:billing`. [Guide](/guides/finance/billing)
**Webhook delivery history (new)**
* `GET /webhooks/{webhookId}/deliveries`, `…/{deliveryId}`, and `POST …/{deliveryId}/resend`. See what was sent, what your endpoint answered, and replay a `FAILURE` or `DISCARDED` delivery with its original payload and `notificationId`. New enum `WebhookDeliveryStatus`. [Guide](/guides/finance/webhooks#debugging-deliveries)
**Pay-outs**
* **SEPA pay-outs** — `PayoutRail` gains `SEPA` (requires `fiatCurrency: EUR`; `railDetails` takes an `iban` and optional `bic`; reads expose `ibanLast4`). New validation codes `invalid-iban`, `invalid-bic`. [Guide](/guides/finance/payouts#setup)
* `failureReason` on a pay-out is now the documented enum `PayoutOutcomeReason` — `REJECTED_BY_PROVIDER`, `ROUTE_UNAVAILABLE`, `SEND_FAILED`, `RETURNED_BY_BANK`, `UNKNOWN`. Treat it as open. [Guide](/guides/finance/payouts#why-it-didnt-arrive)
* `beneficiaryEmail` and `beneficiaryPhoneNumber` on a payout bank account are documented, including that a route fails terminally with `beneficiary-data-incomplete` when the destination requires them and they are missing.
* `GET /accounts/{accountId}/payouts` is additionally sortable on `requestedAt`.
* **Request partner onboarding** — `POST /parties/{partyId}/partner-onboarding`. Start onboarding a verified individual to our banking partner ahead of provisioning, instead of letting the first virtual-bank-account call trigger it and fail. [Guide](/guides/finance/onboarding/partner-onboarding)
* No public API changes.
**Breaking — two error codes were renamed.** On [Create a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account) and [Prepare a wallet-ownership proof](/api-reference/Finance-API/virtual-bank-accounts/prepare-a-wallet-ownership-proof):
| Was | Now | Status | Meaning |
| - | - | - | - |
| `recipient-verification-pending` | **`partner-onboarding-pending`** | `409` | The account holder's onboarding at our banking partner is still in progress. Retryable. |
| `recipient-verification-rejected` | **`partner-onboarding-rejected`** | `422` | The partner rejected or revoked the account holder. Terminal. |
The HTTP statuses and the retryable/terminal split are unchanged. If your integration matches on the old code strings, update them — the API has returned the new names since this release went live.
**Partner-onboarding status (new)** — `GET /parties/{partyId}/partner-onboarding`, and `partnerOnboardingStatus` on the party. The third onboarding gate, next to identity verification and partner-terms consent, is now readable: `NOT_STARTED`, `PENDING`, `APPROVED` or `REJECTED`. `PENDING` is exactly the state behind `partner-onboarding-pending`, so you can wait for `APPROVED` instead of retrying a failing provisioning call. [Guide](/guides/finance/onboarding/partner-onboarding)
* Provisioning a virtual bank account no longer fails with `400 party-email-required` when the account holder has no `email`: a synthetic address is used so onboarding proceeds. Supplying a real address up front is still strongly recommended — see the note on [party email](/guides/finance/changelog).
* The public spec's description of the webhook delivery body was corrected to the two-field `{ eventType, result }` shape the [webhooks guide](/guides/finance/webhooks#what-you-receive) has always shown. The wire format did not change.
Our partners require the real end customer to read and accept their terms on a screen themselves; nobody may accept on their behalf. This release surfaces that requirement through the API, and gives you a way to collect the acceptance before any customer data reaches a partner.
**Partner-terms consent (new)**
Consent is the final gate after KYC. A verified party whose customer has not accepted cannot get a [virtual bank account](/guides/finance/virtual-bank-accounts) or receive a [pay-out](/guides/finance/payouts). See [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
* **Get the consent state** — `GET /parties/{partyId}/partner-terms`. One `status` and, while outstanding, one `consentUrl` covering **every** partner the party is onboarding to. Always current as of the call. [Reference](/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state)
* **Issue a link up front** — `POST /parties/{partyId}/partner-terms/link`. Mints a link so your customer can accept every enabled partner's latest terms **before** onboarding begins, rather than hitting the gate later. [Reference](/api-reference/Finance-API/parties/issue-a-partner-terms-consent-link)
* New enum `PartnerTermsStatus` (`NOT_REQUIRED`, `REQUIRED`, `ACCEPTED`), and a cached **`partnerTermsStatus`** on the party for consumers that would rather poll the plain party read.
`ACCEPTED` is **not terminal**. It is evaluated against the terms active *now*, so enabling a new partner — or a partner publishing a new terms version — moves a party back to `REQUIRED`. Re-read it rather than recording it as a completed milestone.
Each call to the link endpoint also **supersedes the previous link** for that party — never call it just to check state.
* Both endpoints fail **closed**: `503 identity-verification-unavailable` is retryable and never means "nothing outstanding", and a `REQUIRED` is never returned without a `consentUrl`.
* **`email` on a party** — optional, and accepted on create and update. Supply a **real address up front** for an individual on a self-custody account: when an account holder is onboarded to a partner the email is forwarded with the case, and if the party has none a synthetic address is used instead. Because the partner stores it write-once, adding a real address later does **not** replace the synthetic one.
This release makes asset selection explicit. A tenant can now hold the same stablecoin on more than one chain, which means a fiat currency no longer always identifies a single asset — so there are new endpoints to discover what you can use, and new fields to say which one you mean.
**Discover what you can settle in (new)**
Stop hard-coding asset lists. See [Chains and assets](/guides/finance/supported-chains-and-assets).
* **List supported assets** — `GET /supported-assets`. Every chain/asset pair enabled for your tenant, with `decimals` and `contractAddress`. [Reference](/api-reference/Finance-API/supported-assets/list-supported-assets)
* **List account supported assets** — `GET /accounts/{accountId}/supported-assets`. The same list annotated with a per-account `permitStatus`, so you can tell whether *this* account can move an asset yet. New enum `AssetPermitStatus` (`READY`, `ACTIVATING`, `ACTION_REQUIRED`, `PENDING`, `FAILED`, `NO_WALLET`). Derived per request and never stored — don't cache it. [Reference](/api-reference/Finance-API/supported-assets/list-account-supported-assets)
**Name the asset a fiat amount settles in**
* `chain` and `asset` are now accepted on **create fiat transfer**. Send them together.
* Optional while a currency maps to one asset for your tenant; **required** once it maps to more than one. New codes: `ambiguous-asset`, `chain-and-asset-required-together`, `asset-currency-mismatch`.
If you hold the same stablecoin on two chains, calls that previously worked with `currency` alone are now rejected with `ambiguous-asset` rather than settling on an arbitrary chain. Sending `chain` + `asset` is always accepted, so the safe move is to always send them.
**Activate a smart-contract wallet (new)**
* **Verify wallet allowances** — `POST /accounts/{accountId}/wallets/{walletId}/allowance-verification`. A second activation path for self-custody wallets that can't sign a permit — a Safe, for example. Grant a plain `approve()` on-chain, then call this; the allowance itself is the consent, so there's no request body and no proof. [Reference](/api-reference/Finance-API/allowances/verify-wallet-allowances)
* `WalletStatus` gains **`VERIFYING_ALLOWANCE`**. It is *not* a usable state — every check treats it exactly like `PENDING`. If the verification window closes with assets still uncovered, the wallet reverts to `PENDING`. See [Permits and allowances](/guides/finance/permits-and-allowances#contract-wallets-activating-from-an-allowance).
**Pay-out route ownership proofs (new)**
* **Prepare** and **submit** a route ownership proof — `POST /accounts/{accountId}/payout-routes/{routeId}/ownership-proof/prepare` and `.../complete`. Where a destination requires a Travel-Rule proof that you control the source wallet and that wallet is self-custody, registration pauses until its owner signs. [Reference](/api-reference/Finance-API/payout-routes/prepare-a-route-ownership-proof)
* `PayoutRouteStatus` gains **`AWAITING_OWNERSHIP_PROOF`** — the only route status that needs an action from you. EIP-191 externally-owned accounts only; ERC-1271 contract signatures are not supported. See [Pay-outs](/guides/finance/payouts#proving-you-control-the-source-wallet).
**Pay-out beneficiary contact details**
* `beneficiaryEmail` and `beneficiaryPhoneNumber` on **register a payout bank account**. Some destinations are refused without both. A bank account cannot be edited after registration, so a route built on incomplete beneficiary data fails terminally with `beneficiary-data-incomplete` and the beneficiary has to be registered again — supply both up front.
This release opens up the crypto-to-fiat direction, puts identity verification under your control through the API, and adds webhooks so you no longer have to poll for asynchronous outcomes.
**Bank pay-outs (new)**
Send crypto from an account wallet and have a bank beneficiary receive fiat. Three new resources, created once and reused: a *payout bank account* (whose bank account), a *payout route* (which bank account pairs with which crypto asset), and a *payout* (how much, when). See [Pay-outs](/guides/finance/payouts).
* **Register a payout bank account** — `POST /parties/{partyId}/payout-bank-accounts`. The only endpoint that accepts raw rail credentials; the account number is encrypted at rest and only ever returned as `accountNumberLast4`. Initial rail is `US_ACH`. [Reference](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account)
* **List** and **get** payout bank accounts — `GET /parties/{partyId}/payout-bank-accounts` and `.../{payoutBankAccountId}`.
* **Create a payout route** — `POST /accounts/{accountId}/payout-routes`. Pairs an allow-listed bank account with a crypto deposit asset; the provider is resolved internally and never exposed. [Reference](/api-reference/Finance-API/payout-routes/create-a-payout-route)
* **List payout routes** — `GET /accounts/{accountId}/payout-routes`. Exposes `depositAddress` for an `ACTIVE` route on a self-custody account. [Reference](/api-reference/Finance-API/payout-routes/list-payout-routes)
* **Request a payout** — `POST /accounts/{accountId}/payouts`. Creates a PULL pay-out funded from the account wallet via its permit. [Reference](/api-reference/Finance-API/payouts/request-a-payout)
* **List** and **get** payouts — `GET /accounts/{accountId}/payouts` and `.../{payoutId}`.
* New enums: `PayoutStatus` (`REQUESTED`, `SENDING`, `PROVIDER_PROCESSING`, `COMPLETED`, `REJECTED`, `FAILED`, `RETURNED`), `FundingMode` (`PULL`, `PUSH`), `PayoutRouteStatus`, `PayoutRail`, `PayoutBankAccountStatus`.
* `PartyRoleType` gains **`PAYOUT_RECIPIENT`**. A party needs this role, `ACTIVE` and with cleared KYC/KYB, before a route can be created for its bank account. It may be held alongside `ACCOUNT_HOLDER` for self-payouts.
A `201` from *Request a payout* is not a success signal. Permit allowance and wallet balance are deliberately not pre-checked — if either is insufficient, the asynchronous send fails and the pay-out moves to `FAILED` with a `failureReason`. Track the outcome via webhook or by reading the pay-out back.
**Identity verification (new)**
Verification is now driven through the API rather than arranged with your Venly contact. See [Onboarding & Verification](/guides/finance/onboarding/lifecycle).
* **Create a hosted verification link** — `POST /parties/{partyId}/verification`. Mints a hosted KYC (individual) or KYB (organisation) link. Links do not expire: re-POSTing returns the same URL with `200`, or mints a fresh one with `201` if the old was revoked. [Reference](/api-reference/Finance-API/parties/create-a-verification-link)
* **Get verification linkage** — `GET /parties/{partyId}/iv-verification`. Returns the linkage `status` and the durable `ivCaseReference` to quote to support. [Reference](/api-reference/Finance-API/parties/get-verification-linkage)
* **Forward an existing Sumsub verification** — `party.sumsubToken` on `POST /accounts` forwards a Sumsub share token instead of re-verifying the user. Individuals on self-custody tenants only. Write-only, never returned. If the forward fails, the entire account-creation transaction rolls back — retry with a fresh token.
* `KycStatus` gains **`NOT_REQUIRED`** for accounts, letting tenants with tenant-managed KYC declare that they verify end-users themselves. Requires a Venly admin to enable the flag on the company tenant — without it the call fails with `kyc-not-required-not-allowed`. Granted for testing rather than production use; production integrations verify via a hosted link or Sumsub token sharing.
**Webhooks (new)**
Register HTTPS endpoints and have Venly push asynchronous events instead of polling. See [Webhooks](/guides/finance/webhooks).
* `POST /webhooks`, `GET /webhooks`, `GET /webhooks/{webhookId}`, `PUT /webhooks/{webhookId}`, `DELETE /webhooks/{webhookId}`.
* **Ping a webhook** — `POST /webhooks/{webhookId}/ping` queues a synthetic `PING` event so you can validate your endpoint end-to-end. [Reference](/api-reference/Finance-API/webhooks/ping-a-webhook)
* Authentication is `API_KEY` (injected into a header you name) or `BASIC_AUTHENTICATION`. Secrets are write-only and never returned, so an update is a full replacement.
**Virtual bank accounts — USD rails and full deposit instructions**
* **USD → ACH lane** added alongside EUR → SEPA. `bankAccountType` gains `USD_ACH`, and the response now carries `accountNumber` and `routingNumber`. Requires your tenant to be onboarded for the USD lane with an approved KYB recording.
* **`depositRails`** is a new array carrying the complete instruction set per rail — including the bank and beneficiary postal addresses that wire and SWIFT senders require. USD accounts typically expose `ACH`, `WIRE`, `RTP` and `SWIFT`; EUR accounts expose a single `SEPA` rail. Build payment instructions from this rather than the single-rail summary fields.
* **`VirtualBankAccountStatus` gains `PENDING`** — an EUR account can be returned before the provider has populated its deposit rails. Don't show payment instructions until it is `ACTIVE`.
* **Prepare a wallet-ownership proof** — `POST /accounts/{accountId}/virtual-bank-accounts/prepare`. Where a self-custody wallet must prove ownership before a virtual bank account can be created, returns the exact message the customer must sign; submit it back in the new `ownershipProof` block on create. [Reference](/api-reference/Finance-API/virtual-bank-accounts/prepare-a-wallet-ownership-proof)
**New supported chains**
* **Ethereum** (`ETHEREUM`) and **Solana** (`SOLANA`) are now accepted values. Availability for your company still depends on its configured `supportedChains`. See [Supported chains & assets](/guides/finance/supported-chains-and-assets).
**Corrections to previously published docs**
* `AmlStatus` was documented as `PENDING`/`APPROVED`/`REJECTED`. The actual values are **`PENDING`, `APPROVED`, `FLAGGED`, `BLOCKED`** — treat anything other than `APPROVED` as not-ready. See [Account verification](/guides/finance/kyc-verification#wallet-readiness-aml-and-permits-for-self-custody).
* The pay-in session endpoint now documents its `manage:pay-in-sessions` role and its full error set (`no-suitable-provider`, `company-not-active`, `account-not-active`, `idempotency-key-conflict`, `kyc-required`, `unsupported-asset`, plus `502`/`503` provider states).
* `vatNumber` was listed on the party **list** item. It is only returned by a single-party fetch, and has been removed from the list schema.
Version 1.1.0 adds Polygon as a supported chain and lets self-custody tenants register an end-customer's own wallet.
**New supported chain**
* **Polygon** (`POLYGON`) is now available for accounts, wallets, and balances. See [Supported chains & assets](/guides/finance/supported-chains-and-assets).
**Non-custodial accounts**
* Self-custody tenants can register an end-customer's own wallet by passing `address` when creating an account. See [Managed vs. self-custody](/guides/finance/managed-vs-self-custody).
**Improvements**
* Retrying an operation with the same idempotency key and body returns the original result. See [Idempotency](/guides/finance/idempotency).
* Account-to-account crypto transfers are now supported for Venly-managed accounts.
# Countries
Source: https://docs.venlyfinance.com/guides/finance/countries
Supported onboarding countries by risk tier — OFAC restrictions, fiat rail availability, and virtual account eligibility.
Supported onboarding countries organized by risk tier. Covers OFAC restrictions, country risk ratings, fiat rail availability, and virtual account eligibility.
Venly can onboard entities and individuals from regions not sanctioned by OFAC, using a risk-tiering approach. Higher-risk tiers are onboarded on a case-by-case basis depending on the risk profile. **No Virtual Accounts** will be issued to customers who fall under Venly's Prohibited Country Risk Rating. This country risk assessment is provided by our regulated partner.
## **Country Risk Ratings**
| **Jurisdiction** | **ISO3** | **ISO2** | **Risk Rating** | **USD (ACH/Wire/SWIFT)** | **USD (FedNow)** | **EUR (SEPA)** | **GBP (FPS/CHAPS)** |
| :- | :- | :- | :- | :- | :- | :- | :- |
| Afghanistan | AFG | AF | Prohibited | No | No | No | No |
| Albania | ALB | AL | Supported | No | No | Yes | Yes (1PP) |
| Algeria | DZA | DZ | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| American Samoa | ASM | AS | Prohibited | No | No | No | No |
| Andorra | AND | AD | Supported | Yes | Yes | Yes | Yes (1PP) |
| Angola | AGO | AO | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Anguilla | AIA | AI | Supported | Yes | Yes | Yes | Yes (1PP) |
| Antigua and Barbuda | ATG | AG | Supported | Yes | Yes | Yes | Yes (1PP) |
| Argentina | ARG | AR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Armenia | ARM | AM | Supported | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Aruba | ABW | AW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Australia | AUS | AU | Supported | Yes | Yes | Yes | Yes (1PP) |
| Austria | AUT | AT | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Azerbaijan | AZE | AZ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bahamas | BHS | BS | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bahrain | BHR | BH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bangladesh | BGD | BD | Prohibited | No | No | No | No |
| Barbados | BRB | BB | Prohibited | No | No | No | No |
| Belarus | BLR | BY | Prohibited | No | No | No | No |
| Belgium | BEL | BE | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Belize | BLZ | BZ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Benin | BEN | BJ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bermuda | BMU | BM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bhutan | BTN | BT | Supported | No | No | Yes | Yes (1PP) |
| Bolivia | BOL | BO | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Bosnia and Herzegovina | BIH | BA | Supported | No | No | Yes | Yes (1PP) |
| Botswana | BWA | BW | Supported | No | No | Yes | Yes (1PP) |
| Brazil | BRA | BR | Supported | Yes | Yes | Yes | Yes (1PP) |
| British Virgin Islands | VGB | VG | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Brunei Darussalam | BRN | BN | Supported | Yes | Yes | Yes | Yes (1PP) |
| Bulgaria | BGR | BG | Supported (HR) | Yes (Business) | Yes (Business) | Yes (1PP) | Yes (1PP) |
| Burkina Faso | BFA | BF | Prohibited | No | No | No | No |
| Burundi | BDI | BI | Supported | No | No | Yes | Yes (1PP) |
| Cambodia | KHM | KH | Supported (HR) | No | No | Yes | Yes (1PP) |
| Cameroon | CMR | CM | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| Canada | CAN | CA | Prohibited | No | No | No | No |
| Cape Verde | CPV | CV | Supported | Yes | Yes | Yes | Yes (1PP) |
| Cayman Islands | CYM | KY | Supported | Yes | Yes | Yes | Yes (1PP) |
| Central African Republic | CAF | CF | Supported (HR) | No | No | No | No |
| Chad | TCD | TD | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Chile | CHL | CL | Supported | Yes | Yes | Yes | Yes (1PP) |
| China | CHN | CN | Prohibited | No | No | No | No |
| Colombia | COL | CO | Supported | Yes | Yes | Yes | Yes (1PP) |
| Comoros | COM | KM | Supported (HR) | Yes | Yes | Yes | Yes (1PP) |
| Cook Island | COK | CK | Supported | Yes | Yes | Yes | Yes (1PP) |
| Costa Rica | CRI | CR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Cote D’Ivoire | CIV | CI | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Croatia | HRV | HR | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Cuba | CUB | CU | Prohibited | No | No | No | No |
| Curacao | CUW | CW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Cyprus | CYP | CY | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Czech Republic | CZE | CZ | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Denmark | DNK | DK | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Djibouti | DJI | DJ | Supported (HR) | Yes | Yes | Yes | Yes (1PP) |
| Dominica | DMA | DM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Dominican Republic | DOM | DO | Supported | Yes | Yes | Yes | Yes (1PP) |
| Ecuador | ECU | EC | Supported | Yes | Yes | Yes | Yes (1PP) |
| Egypt | EGY | EG | Supported | Yes | Yes | Yes | Yes (1PP) |
| El Salvador | SLV | SV | Supported | Yes | Yes | Yes | Yes (1PP) |
| Equatorial Guinea | GNQ | GQ | Supported (HR) | Yes | Yes | Yes | Yes (1PP) |
| Eritrea | ERI | ER | Supported (HR) | No | No | Yes | Yes (1PP) |
| Estonia | EST | EE | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Eswatini | SWZ | SZ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Ethiopia | ETH | ET | Supported | No | No | Yes | No |
| Faroe Islands | FRO | FO | Supported | Yes | Yes | Yes | Yes (1PP) |
| Fiji | FJI | FJ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Finland | FIN | FI | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| France | FRA | FR | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Gabon | GAB | GA | Supported (HR) | Yes | Yes | Yes | Yes (1PP) |
| Gambia | GMB | GM | Supported | No | No | Yes | Yes (1PP) |
| Georgia | GEO | GE | Supported | Yes | Yes | Yes | Yes (1PP) |
| Germany | DEU | DE | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Ghana | GHA | GH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Gibraltar | GIB | GI | Supported | Yes | Yes | Yes | Yes (1PP) |
| Greece | GRC | GR | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Grenada | GRD | GD | Supported | No | No | Yes | Yes (1PP) |
| Guam | GUM | GU | Supported | Yes | Yes | Yes | Yes (1PP) |
| Guatemala | GTM | GT | Supported | Yes | Yes | Yes | Yes (1PP) |
| Guernsey | GGY | GG | Supported | Yes | Yes | Yes | Yes (1PP) |
| Guinea | GIN | GN | Supported | No | No | Yes | Yes (1PP) |
| Guinea-Bissau | GNB | GW | Prohibited | No | No | No | No |
| Guyana | GUY | GY | Supported | Yes | Yes | Yes | Yes (1PP) |
| Haiti | HTI | HT | Prohibited | No | No | No | No |
| Honduras | HND | HN | Supported | Yes | Yes | Yes | Yes (1PP) |
| Hong Kong SAR, China | HKG | HK | Supported | Yes | Yes | Yes | Yes (1PP) |
| Hungary | HUN | HU | Prohibited | No | No | No | No |
| Iceland | ISL | IS | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| India | IND | IN | Prohibited | No | No | No | No |
| Indonesia | IDN | ID | Supported | Yes | Yes | Yes | Yes (1PP) |
| Iran | IRN | IR | Prohibited | No | No | No | No |
| Iraq | IRQ | IQ | Prohibited | No | No | No | No |
| Ireland | IRL | IE | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Isle of Man | IMN | IM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Israel | ISR | IL | Supported | Yes | Yes | Yes | Yes (1PP) |
| Italy | ITA | IT | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Jamaica | JAM | JM | Prohibited | No | No | No | No |
| Japan | JPN | JP | Prohibited | No | No | No | No |
| Jersey | JEY | JE | Supported | Yes | Yes | Yes | Yes (1PP) |
| Jordan | JOR | JO | Supported | No | No | Yes | Yes (1PP) |
| Kazakhstan | KAZ | KZ | Supported | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Kenya | KEN | KE | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| Kiribati | KIR | KI | Supported | Yes | Yes | Yes | Yes (1PP) |
| Korea, North | PRK | KP | Prohibited | No | No | No | No |
| Korea, South | KOR | KR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Kosovo | XXK | XK | Prohibited | No | No | No | No |
| Kuwait | KWT | KW | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| Kyrgyzstan | KGZ | KG | Supported | Yes | Yes | Yes | Yes (1PP) |
| Laos | LAO | LA | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Latvia | LVA | LV | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Lebanon | LBN | LB | Prohibited | No | No | No | No |
| Lesotho | LSO | LS | Supported | Yes | Yes | Yes | Yes (1PP) |
| Liberia | LBR | LR | Prohibited | No | No | No | No |
| Libya | LBY | LY | Prohibited | No | No | No | No |
| Liechtenstein | LIE | LI | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Lithuania | LTU | LT | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Luxembourg | LUX | LU | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Macao SAR, China | MAC | MO | Prohibited | No | No | No | No |
| Madagascar | MDG | MG | Prohibited | No | No | No | No |
| Malawi | MWI | MW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Malaysia | MYS | MY | Prohibited | No | No | No | No |
| Maldives | MDV | MV | Supported | Yes | Yes | Yes | Yes (1PP) |
| Mali | MLI | ML | Supported | No | No | No | No |
| Malta | MLT | MT | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Marshall Islands | MHL | MH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Martinique | MTQ | MQ | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Mauritania | MRT | MR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Mauritius | MUS | MU | Supported | Yes | Yes | Yes | Yes (1PP) |
| Mexico | MEX | MX | Supported | Yes | Yes | Yes | Yes (1PP) |
| Moldova | MDA | MD | Supported | Yes | Yes | Yes | Yes (1PP) |
| Monaco | MCO | MC | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Mongolia | MNG | MN | Prohibited | No | No | No | No |
| Montenegro | MNE | ME | Supported | Yes | Yes | Yes | Yes (1PP) |
| Montserrat | MSR | MS | Supported | Yes | Yes | Yes | Yes (1PP) |
| Morocco | MAR | MA | Prohibited | No | No | No | No |
| Mozambique | MOZ | MZ | Supported | No | No | Yes (1PP) | Yes (1PP) |
| Myanmar | MMR | MM | Prohibited | No | No | No | No |
| Namibia | NAM | NA | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Nauru | NRU | NR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Nepal | NPL | NP | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| Netherlands | NLD | NL | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| New Caledonia | NCL | NC | Supported | Yes | Yes | Yes | Yes (1PP) |
| New Zealand | NZL | NZ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Nicaragua | NIC | NI | Prohibited | No | No | No | No |
| Niger | NER | NE | Supported (HR) | No | No | Yes | Yes (1PP) |
| Nigeria | NGA | NG | Prohibited | No | No | No | No |
| Niue | NIU | NU | Supported | Yes | Yes | Yes | Yes (1PP) |
| North Macedonia | MKD | MK | Supported | No | No | Yes | Yes (1PP) |
| Norway | NOR | NO | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Oman | OMN | OM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Pakistan | PAK | PK | Prohibited | No | No | No | No |
| Palau | PLW | PW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Palestine | PSE | PS | Prohibited | No | No | No | No |
| Panama | PAN | PA | Supported | Yes | Yes | Yes | Yes (1PP) |
| Papua New Guinea | PNG | PG | Supported (HR) | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Paraguay | PRY | PY | Supported | Yes | Yes | Yes | Yes (1PP) |
| Peru | PER | PE | Supported | Yes | Yes | Yes | Yes (1PP) |
| Philippines | PHL | PH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Poland | POL | PL | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Portugal | PRT | PT | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Puerto Rico | PRI | PR | Supported | Yes | Yes | Yes | Yes (1PP) |
| Qatar | QAT | QA | Supported | No | No | Yes | Yes (1PP) |
| Republic of the Congo | COG | CG | Prohibited | No | No | No | No |
| Romania | ROU | RO | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Russia | RUS | RU | Prohibited | No | No | No | No |
| Rwanda | RWA | RW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saint Helena, Ascension and Tristan | SHN | SH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saint Kitts and Nevis | KNA | KN | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saint Lucia | LCA | LC | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saint Pierre and Miquelon | SPM | PM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saint Vincent and the Grenadines | VCT | VC | Supported | Yes | Yes | Yes | Yes (1PP) |
| Samoa | WSM | WS | Supported | Yes | Yes | Yes | Yes (1PP) |
| San Marino | SMR | SM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Sao Tome and Principe | STP | ST | Supported | Yes | Yes | Yes | Yes (1PP) |
| Saudi Arabia | SAU | SA | Supported | Yes | Yes | Yes | Yes (1PP) |
| Senegal | SEN | SN | Prohibited | No | No | No | No |
| Serbia | SRB | RS | Supported | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Seychelles | SYC | SC | Supported | No | No | Yes | Yes (1PP) |
| Sierra Leone | SLE | SL | Supported | Yes | Yes | Yes | Yes (1PP) |
| Singapore | SGP | SG | Supported | Yes | Yes | Yes | Yes (1PP) |
| Slovakia | SVK | SK | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Slovenia | SVN | SI | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Solomon Islands | SLB | SB | Supported | No | No | Yes | Yes (1PP) |
| Somalia | SOM | SO | Prohibited | No | No | No | No |
| South Africa | ZAF | ZA | Supported | No | No | Yes (1PP) | Yes (1PP) |
| South Sudan | SSD | SS | Prohibited | No | No | No | No |
| Spain | ESP | ES | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Sri Lanka | LKA | LK | Supported | Yes | Yes | Yes | Yes (1PP) |
| St Maarten | SXM | SX | Supported | Yes | Yes | Yes | Yes (1PP) |
| Sudan | SDN | SD | Prohibited | No | No | No | No |
| Suriname | SUR | SR | Supported | No | No | Yes | Yes (1PP) |
| Sweden | SWE | SE | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| Switzerland | CHE | CH | Supported | Yes | Yes | Yes | Yes (1PP) |
| Syrian Arab Republic | SYR | SY | Prohibited | No | No | No | No |
| Taiwan | TWN | TW | Supported | Yes | Yes | Yes | Yes (1PP) |
| Tajikistan | TJK | TJ | Supported | Yes | Yes | Yes | Yes (1PP) |
| Tanzania | TZA | TZ | Supported | No | No | Yes (1PP) | Yes (1PP) |
| Thailand | THA | TH | Supported | Yes | Yes | Yes | Yes (1PP) |
| The Democratic Republic Of The Congo | COD | CD | Prohibited | No | No | No | No |
| Timor-Leste | TLS | TL | Supported | Yes | Yes | Yes | Yes (1PP) |
| Togo | TGO | TG | Supported | No | No | Yes | Yes (1PP) |
| Tonga | TON | TO | Supported | No | No | Yes | Yes (1PP) |
| Trinidad and Tobago | TTO | TT | Supported | No | No | Yes (1PP) | Yes (1PP) |
| Tunisia | TUN | TN | Supported | Yes | Yes | Yes | Yes (1PP) |
| Turkey | TUR | TR | Supported | No | No | Yes | Yes (1PP) |
| Turkmenistan | TKM | TM | Supported (HR) | Yes | Yes | Yes | Yes (1PP) |
| Turks and Caicos Islands | TCA | TC | Supported | No | No | Yes | Yes (1PP) |
| Uganda | UGA | UG | Prohibited | No | No | No | No |
| Ukraine | UKR | UA | Prohibited | No | No | No | No |
| United Arab Emirates | ARE | AE | Supported | Yes | Yes | Yes | Yes (1PP) |
| United Kingdom | GBR | GB | Supported | Yes (Business) | Yes (Business) | Yes | Yes (1PP) |
| United States | USA | US | Supported | Yes | Yes | Yes (1PP) | Yes (1PP) |
| United States Virgin Islands | VIR | VI | Prohibited | No | No | No | No |
| Uruguay | URY | UY | Supported | Yes | Yes | Yes | Yes (1PP) |
| Uzbekistan | UZB | UZ | Supported | Yes | Yes | Yes (1PP) | Yes (1PP) |
| Vanuatu | VUT | VU | Supported | No | No | Yes (1PP) | Yes (1PP) |
| Venezuela | VEN | VE | Prohibited | No | No | No | No |
| Vietnam | VNM | VN | Supported (HR) | No | No | Yes (1PP) | Yes (1PP) |
| Western Sahara | ESH | EH | Prohibited | No | No | No | No |
| Yemen | YEM | YE | Prohibited | No | No | No | No |
| Zambia | ZMB | ZM | Supported | Yes | Yes | Yes | Yes (1PP) |
| Zimbabwe | ZWE | ZW | Prohibited | No | No | No | No |
**1PP** means first-party payment: funds must be sent from a bank account held in the same name as the onboarded customer.
**Yes (Business)** means the rail is available to business customers only.
**Supported (HR)** means Supported High Risk: customers in these jurisdictions are onboarded with enhanced due diligence.
# Data model
Source: https://docs.venlyfinance.com/guides/finance/data-model
How parties, accounts, wallets, and virtual bank accounts relate — the object hierarchy behind every Finance API call.
Every Finance API resource hangs off a **party**. Once the hierarchy clicks, the rest of the API is predictable: you create entities top-down, and most operations are scoped to an account.
```mermaid theme={null}
flowchart TD
Company[Your company] --> Party[Party · individual or organisation]
Party -->|party role| Account[Account · one chain]
Account --> Wallet[Wallets · account + escrow]
Account --> VBA[Virtual bank account · IBAN]
Account --> Transfer[Transfers]
Wallet --> Permit[Permits and allowances]
```
## The entities
| Entity | What it is | Key relationships |
| - | - | - |
| **[Party](/api-reference/Finance-API/parties/list-all-parties)** | The legal identity — an `INDIVIDUAL` or `ORGANISATION`. Carries KYC/KYB status. | Linked to one or more accounts via party roles. |
| **[Account](/api-reference/Finance-API/accounts/list-all-accounts)** | The container for money movement, pinned to one [supported chain](/guides/finance/supported-chains-and-assets) at creation. | Belongs to a party (or parties); holds wallets, VBAs, and transfers. |
| **Party role** | The link between a party and an account (`ACCOUNT_HOLDER`). | Many parties can share one account (joint accounts). |
| **[Wallet](/guides/finance/wallets)** | Crypto wallet, either Venly-held or the customer's own (`VENLY_MANAGED` or `SELF_CUSTODY`). Each account has an account wallet (plus an escrow wallet when card payments, coming soon, are enabled). | Belongs to an account; holds per-asset balances. |
| **[Virtual bank account](/guides/finance/virtual-bank-accounts)** | Bank details for receiving fiat (EUR SEPA or USD ACH), auto-converted to crypto. | Belongs to an account. |
## How they're created
You build top-down, and verification gates the money-movement steps:
The root identity. KYC/KYB starts after creation — see [Account verification](/guides/finance/kyc-verification).
Link the party (`partyId`, or an inline `party`) and choose the chain. Venly provisions the wallets.
Send the party through [verification](/guides/finance/onboarding/lifecycle); the verdict moves the account to `VERIFIED`. Until then, money movement is blocked.
Assign a [virtual bank account](/guides/finance/virtual-bank-accounts), then [transfer](/guides/finance/transfers) funds.
## IDs and external IDs
Every entity has a Venly UUID `id`. You can also set your own `externalId` on parties and accounts and use it as a shortcut — for example `receiverExternalId` on a transfer, instead of looking up the Venly UUID.
## Next steps
Build the hierarchy end to end.
Definitions for every entity and status.
# Errors & error codes
Source: https://docs.venlyfinance.com/guides/finance/errors
The response envelope, the HTTP status and error code each failure returns, and how to handle them.
Every Finance API response uses the same envelope, so you can handle success and failure uniformly.
## Response envelope
A successful response sets `success: true` and carries the payload in `result` (list endpoints also include a `pagination` object):
```json Success theme={null}
{ "success": true, "result": { "id": "..." } }
```
A failure sets `success: false` and returns one or more `errors`, each with a stable `code` and a human-readable `message`:
```json Error theme={null}
{
"success": false,
"errors": [
{ "code": "invalid-request", "message": "The request contains invalid parameters." }
]
}
```
Branch on the `code`, not the `message` — messages may change, codes are stable.
## Standard errors
These apply across endpoints:
| HTTP | `code` | When it happens | What to do |
| - | - | - | - |
| `400` | `invalid-request` | Malformed body or invalid parameters. | Fix the request; don't retry it unchanged. |
| `401` | `unauthenticated` | Missing or expired token. | Refresh the token and retry — see [Authentication](/getting-started/authentication). |
| `403` | `forbidden` | The operation isn't available to your company setup. | Contact Venly to enable it. |
| `404` | `-not-found` | The resource (or one it references) doesn't exist — e.g. `account-not-found`, `transfer-not-found`. | Check the ID; for transfers, confirm the account is the sender or receiver. |
| `409` | `concurrent-modification` | Stale `version` on an update. | Re-fetch, reapply your change, retry — see [Versioning](/guides/finance/versioning). |
| `422` | `idempotency-conflict` / *(varies)* | An idempotency key was reused with a different body, or replays a call that originally failed; or a semantically invalid input. | Branch on the `code`; to retry a genuine failure, use a **new** [idempotency key](/guides/finance/idempotency). |
| `500` | `internal-error` | Unexpected server error. | Retry with backoff; if it persists, contact Venly. |
## Verification & activation gates
A new account can't move money until it's verified and its wallet has cleared AML screening. These gates return **different** codes per operation — treat any of them as "not ready yet" rather than matching a single code:
| Operation | HTTP | `code` |
| - | - | - |
| Create a virtual bank account | `400` | `kyc-not-verified` |
| Create a pay-in session | `422` | `kyc-required` |
| Request a pay-out | `400` | `account-not-active` |
| Create a pay-out route | `422` | `recipient-not-authorized` / `recipient-role-inactive` |
| Transfer while the sender's or receiver's wallet hasn't cleared AML | `422` | `sender-aml-not-approved` / `receiver-aml-not-approved` |
On a **self-custody** sender, a transfer or PULL pay-out also needs an allowance for Venly to move the funds. Without one the request is accepted and then fails (`INSUFFICIENT_ALLOWANCE`) — sign the [permit](/guides/finance/permits-and-allowances) first.
See [Account verification](/guides/finance/kyc-verification) and [Approving transfers without gas](/guides/finance/permits-and-allowances).
### Verification errors
Verification-provisioning calls — [minting a link](/api-reference/Finance-API/parties/create-a-verification-link), forwarding a `sumsubToken`, and provisioning a virtual bank account — can also fail on the platform itself:
| HTTP | `code` | Retryable | Meaning |
| - | - | - | - |
| `409` | `verification-not-provisioned` | No | Your company has no provisioned verification tenant. Contact Venly. |
| `409` | `party-already-verified` | No | Nothing to do; the party is already `VERIFIED`. |
| `404` | `kyc-status-not-found` | No | The party has no verification on record yet. |
| `409` | `kyc-not-completable` | No | The verification can't be completed by the customer right now — already decided, not waiting on them, or not started from a Sumsub token. |
| `409` | `partner-onboarding-pending` | **Yes** | The account holder's verification is still pending; retry once approved. |
| `422` | `partner-onboarding-rejected` | No | Verification was rejected or revoked; provisioning cannot proceed. |
| `400` | `SUMSUB_TOKEN_NOT_REDEEMABLE` | **Yes, with a fresh token** | The share token is expired or already used. The whole account-creation transaction is rolled back. |
| `500` | `identity-verification-rejected` | No | The platform rejected the request itself. Contact support. |
| `503` | `identity-verification-unavailable` | **Yes** | The platform was unreachable. Retry with backoff. |
See [Troubleshooting verification](/guides/finance/onboarding/troubleshooting) for the retryable/terminal split on each of these.
## Pay-out errors
[Pay-out routes](/guides/finance/payouts) and [pay-outs](/api-reference/Finance-API/payouts/request-a-payout) add these:
| HTTP | `code` | When it happens |
| - | - | - |
| `409` | `route-already-exists` | A route already exists for this account, bank account and deposit asset. |
| `422` | `route-not-active` | The route hasn't reached `ACTIVE`, or was `REJECTED`. |
| `422` | `route-unresolved` | The route doesn't belong to this account. |
| `422` | `recipient-not-authorized` | The bank account's party lacks a `PAYOUT_RECIPIENT` role on this account, or its KYC/KYB isn't cleared. |
| `422` | `recipient-role-inactive` | The `PAYOUT_RECIPIENT` role exists but is `INACTIVE`. |
| `422` | `unsupported-asset` | The deposit asset or route lane isn't enabled for your tenant. |
| `422` | `unsupported-combination` | No single provider resolves for that bank account and deposit asset. |
| `400` | `invalid-account-number` / `invalid-routing-number` / `invalid-account-type` / `invalid-iban` / `invalid-bic` / `invalid-bank-address` | Rail credentials failed validation when registering a payout bank account. |
| `400` | `invalid-currency` | `fiatCurrency` does not match the rail — `US_ACH` needs `USD`, `SEPA` needs `EUR`. |
A pay-out request that returns `201` can still fail. Allowance and balance are not pre-checked, so an underfunded pay-out lands in `FAILED` with a `failureReason` instead of erroring at request time — see [Pay-outs](/guides/finance/payouts#a-201-is-not-success).
## Pay-in errors
[Creating a hosted pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session) on an account that settles through a EUR virtual bank account adds these:
| HTTP | `code` | Retryable | When it happens |
| - | - | - | - |
| `409` | `vba-not-ready` | **Yes** | The EUR virtual bank account is still being provisioned or has no IBAN yet. |
| `422` | `vba-required` | After fixing | The account has no active EUR virtual bank account. Create one. |
| `422` | `ambiguous-eur-vba` | After fixing | More than one active EUR virtual bank account. Close the surplus. |
| `422` | `vba-asset-mismatch` | No | The virtual bank account settles into a different asset than `outCryptocurrency`. |
| `422` | `unsupported-currency` | No | Hosted pay-ins are currently EUR only. |
| `400` | `duplicate-eur-vba` | After fixing | On **creating** a virtual bank account: the account already has an active EUR one. |
## Credential and billing errors
| HTTP | `code` | Endpoint | When it happens |
| - | - | - | - |
| `404` | `credential-not-found` | [API credentials](/guides/finance/api-credentials) | Unknown `clientId`, or another company's — indistinguishable by design. |
| `409` | `credential-limit-exceeded` | API credentials | Creating a second credential, or enabling one while another is active. |
| `409` | `credential-disabled` | API credentials | Rotating a disabled credential. |
| `503` | `keycloak-unavailable` | API credentials | The identity provider was unreachable. Retry. |
| `422` | `billing-not-configured` | [Billing](/guides/finance/billing) | Your company is not linked to a billing customer yet. |
| `500` / `503` | `billing-provider-error` / `billing-provider-unavailable` | Billing | The billing system rejected the request / was unreachable. |
| `400` | `delivery-not-resendable` | [Webhook deliveries](/guides/finance/webhooks#debugging-deliveries) | Only `FAILURE` and `DISCARDED` deliveries can be resent. |
## Rate limits
The Finance API doesn't publish fixed numeric rate limits. Build for resilience regardless:
* Cache your access token and reuse it until it nears expiry — don't fetch one per request.
* Back off and retry on `500` (and on `429`, if returned), using exponential backoff with jitter.
* Make retries safe with an [idempotency key](/guides/finance/idempotency) so a replay never double-charges.
If you expect high call volumes, confirm current limits with your Venly contact — they aren't enumerated in the API contract.
## Next steps
Idempotency, pagination, and versioning rules.
The most common reason an otherwise-correct request is rejected.
Every verification error code, and how to tell a stall from a failure.
Catch asynchronous failures that no HTTP response can tell you about.
# Getting started
Source: https://docs.venlyfinance.com/guides/finance/getting-started
Create a party, open an account, get it verified, and issue an IBAN — with managed and self-custody examples.
This is the shortest path to a working Finance API integration. You'll create a **party**, open an **account** linked to it, get the party **verified**, and then issue a **virtual bank account**.
```mermaid theme={null}
flowchart LR
A[Create party] --> B[Create account
linked to party]
B --> C[Send party through
verification]
C --> D[Create virtual
bank account]
```
**Base URL (staging):** `https://api-staging.venlyfinance.com/v1` · **Auth:** OAuth2 Bearer token — see [Authentication](/getting-started/authentication).
Whether your company is **Venly-managed** or **self-custody** changes one step below — see [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody).
## Step 1 — Create a party
A **party** is the person or organisation behind an account. Its `status` is **`ACTIVE` immediately**, but it still starts unverified — verification is a separate step covered below.
```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"
}'
```
```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:00Z",
"updatedAt": "2026-01-15T09:30:00Z",
"version": 0
}
}
```
Save the returned `id` — it's the `partyId` you link in the next step. For an organisation, send `partyType: ORGANISATION` with `name` (and optional `vatNumber`) instead of `firstName`/`lastName`.
## Step 2 — Open an account linked to the party
An **account** holds the wallet, IBANs, and transfer history. Pass the `partyId` from Step 1 to link the party as `ACCOUNT_HOLDER`. The wallet is provisioned automatically.
The only difference between company types is the wallet address:
* **Venly-managed** — Venly creates and holds the wallet. Don't send an `address`.
* **Self-custody** — the customer controls the wallet. You **must** send their `address`.
```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"
}'
```
```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
}
}
```
The account is created with `kycStatus: VERIFICATION_PENDING` when its party has not been verified yet. It exists and can be set up, but it **can't move money yet**. (An account opened for a party that is already `VERIFIED` inherits that and is created `VERIFIED`.)
Self-custody omitting `address` returns `400 "Address is required for SELF_CUSTODY wallet type"`. See [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody) for the full difference (including permits).
## Step 3 — Get the party verified
Mint a hosted verification link and hand it to the end-user:
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/parties/{partyId}/verification \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
The response carries a `verificationUrl` for the KYC flow (individuals) or KYB flow (organisations). The verdict arrives asynchronously and flips the account's `kycStatus` to `VERIFIED`.
If you already verified this user in your own Sumsub account, you can instead forward a share token as `party.sumsubToken` at account creation. See [Onboarding & Verification](/guides/finance/onboarding/lifecycle) for both routes.
Poll the account until it flips (or better, [register a webhook](/guides/finance/webhooks)):
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/accounts/b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
```json Response (200) theme={null}
{
"success": true,
"result": { "id": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f", "kycStatus": "VERIFIED", "status": "ACTIVE", "version": 1 }
}
```
Until `kycStatus` is `VERIFIED`, creating virtual bank accounts, transfers, and pay-in sessions all fail with a KYC error — the exact `code` and HTTP status depend on the operation (e.g. `400 kyc-not-verified` for virtual bank accounts, `422 kyc-required` for pay-in sessions). See [Account verification](/guides/finance/kyc-verification).
## Step 4 — Create a virtual bank account
With the account verified, issue a EUR IBAN so it can be funded via SEPA. Incoming euros are converted to the `targetCryptocurrency` and credited to the wallet.
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/accounts/b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f/virtual-bank-accounts \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe — EUR",
"inCurrency": "EUR",
"targetCryptocurrency": "USDC",
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
```
```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"
}
}
```
Give the `iban` and `referenceCode` to your customer — the `referenceCode` is what links their incoming SEPA payment to this account.
## Next steps
Funding, transfers, and tracking — end to end.
Sign a permit so a self-custody account can move funds.
States, gating, and what works before verification.
Every endpoint, field, and example.
# Glossary
Source: https://docs.venlyfinance.com/guides/finance/glossary
Definitions for parties, accounts, party roles, virtual bank accounts, custodial wallets, transfers, permits, and other Finance API concepts.
Terms used throughout the Finance API reference and guides. Each definition links to the primary endpoint or guide where the concept is used.
## Identity
### Party
The legal identity behind every account. Two types:
* **`INDIVIDUAL`** — a natural person (requires `firstName`, `lastName`)
* **`ORGANISATION`** — a company or business (requires `name`, optionally `vatNumber`)
A party is created independently of accounts and starts unverified. See [Create a new party](/api-reference/Finance-API/parties/create-a-new-party).
### Party Role
The relationship between a party and an account:
* **`ACCOUNT_HOLDER`** — the party owns or co-owns the account
* **`PAYOUT_RECIPIENT`** — the party is an allowed [pay-out](/guides/finance/payouts) destination for the account
A single account can have multiple party roles, enabling joint accounts. A party can hold both roles at once — that's the self-payout case. See [Add a party to an account](/api-reference/Finance-API/accounts/add-a-party-to-an-account).
### KYC / KYB
**Know Your Customer** (individuals) and **Know Your Business** (organisations), tracked on two separate fields:
* **`kycStatus`** (individuals & accounts) — `VERIFICATION_PENDING → VERIFIED` (or `REJECTED`)
* **`kybStatus`** (organisations) — `PENDING → VERIFIED` (or `DENIED`)
You drive verification through the API: mint a [hosted verification link](/api-reference/Finance-API/parties/create-a-verification-link), or forward an unused Sumsub share token from a verification you already ran. The verdict arrives asynchronously. Outbound money movement is blocked until the account is `VERIFIED`. See [Individual KYC](/guides/finance/onboarding/individual-kyc) and [Business KYB](/guides/finance/onboarding/business-kyb).
### Sumsub share token
A single-use, short-lived token from your own Sumsub account that forwards an existing verification to Venly, instead of putting the user through a second flow. Passed as `party.sumsubToken` when creating an account; individuals on self-custody tenants only. See [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing).
### IV case reference
`ivCaseReference` — a durable identifier for a party's verification case, readable via [Get verification linkage](/api-reference/Finance-API/parties/get-verification-linkage). Once set it is never cleared, so quote it to Venly support when investigating a stuck verification.
***
## Account structure
### Account
The container that holds wallets, virtual bank accounts, and transfer history for a party (or set of parties). Pinned to a single blockchain network at creation. Statuses: `ACTIVE`, `SUSPENDED`, `CLOSED`.
### External ID
A merchant-supplied unique identifier for an account or party (`externalId` field). Use it as a shortcut in transfer requests (`receiverExternalId`) instead of looking up the Venly UUID.
### Account wallet
The wallet that holds an account's balance, bound to one chain. Its `type` is either **`VENLY_MANAGED`** (Venly holds the keys; funds move via API calls) or **`SELF_CUSTODY`** (your customer holds the keys and signs a one-time [permit](/guides/finance/permits-and-allowances) so Venly can move funds). See [Wallets & balances](/guides/finance/wallets).
### Escrow wallet
A second, always Venly-managed wallet that holds funds in flight during settlement. Only on accounts set up for card payments (coming soon). You never sign for it.
### Orchestration wallet
The Venly-controlled wallet that pulls funds from a self-custody account wallet via ERC-20 `transferFrom`. It is the **spender** a self-custody permit authorises, and it's returned as `orchestrationWallet` on the [allowances](/api-reference/Finance-API/allowances/get-wallet-token-allowances) response.
### Virtual Bank Account (VBA)
A bank account reference (IBAN + BIC for `EUR_SEPA`, account + routing number for `USD_ACH`) attached to a Venly account. Used to receive fiat funding that's auto-converted to crypto and credited to the wallet. See [Create a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account).
### IBAN / BIC
**International Bank Account Number** and **Bank Identifier Code** — the standard EUR\_SEPA addressing format returned on a virtual bank account.
### Reference Code
A unique string returned with each VBA. The end user must include this in their SEPA payment message — it's how the incoming wire is matched to the right account. Without it, settlement falls back to manual reconciliation.
### Beneficiary Name
The legal name on the bank account record (typically a Venly custody entity). Display this to end users alongside the IBAN when they initiate a wire.
***
## Money movement
### Transfer
A value movement from one account to another, addressed by `receiverAccountId` or `receiverExternalId`. Two flavors, depending on how you denominate the amount:
* **Fiat transfer** — you send a fiat `currency` + `amount`; it's resolved to the underlying stablecoin and settled on-chain. The response carries a `fiatOrigin` block (original currency, amount, exchange rate).
* **Crypto transfer** — you send a crypto `asset` + `chain` + `amount`, moved directly between the accounts on-chain.
See [Transfers](/guides/finance/transfers).
### Transfer Status
Lifecycle states: `PENDING → COMPLETED` (or `FAILED` with an `errorMessage`).
### Payment Session (Fiat-to-Crypto)
**Coming soon — not yet available.** A hosted checkout flow that converts a card or bank payment into crypto credited to an account's wallet. Returns a `paymentUrl` you redirect the user to. An **initiation-only** record: it ends at `PAYMENT_RECEIVED`, and settlement is tracked on the [pay-in](#pay-in). See [Create a fiat-to-crypto payment session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session).
### Pay-in
The settlement record of one fiat credit — what arrived, what it converted to, and the `netCryptoAmount` that reached the account wallet. Signalled by `PAY_IN_SETTLED` / `PAY_IN_FAILED`. See [Pay-ins](/guides/finance/pay-ins).
### Pay-out
Crypto leaves an account wallet and a bank beneficiary receives fiat — the opposite direction to a pay-in. Requires a [payout bank account](#payout-bank-account) and a [payout route](#payout-route). See [Pay-outs](/guides/finance/payouts).
### Payout Bank Account
An allow-listed beneficiary bank account, registered against a party, on the `US_ACH` or `SEPA` rail. The only place raw bank credentials are accepted; reads expose just `accountNumberLast4` or `ibanLast4`. See [Register a payout bank account](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account).
### Payout Route
An account-scoped pairing of a payout bank account with a crypto deposit asset. Fixes the beneficiary, rail, asset and fiat currency for every pay-out over it. Only `ACTIVE` routes are usable. See [Create a payout route](/api-reference/Finance-API/payout-routes/create-a-payout-route).
### Funding Mode (PULL / PUSH)
How a pay-out is funded on-chain:
* **`PULL`** — Venly sends from the account wallet using its permit. Created via the API.
* **`PUSH`** — the customer sent to the route's `depositAddress` themselves; Venly observes the deposit and creates the pay-out. Self-custody only.
### Deposit Rails
The `depositRails` array on a [virtual bank account](/guides/finance/virtual-bank-accounts), listing every payment rail it is reachable over (`ACH`, `WIRE`, `RTP`, `SWIFT`, `SEPA`) with the complete instruction set each one needs. Build payment instructions from this rather than the single-rail summary fields.
### Ownership Proof
A customer-signed message proving control of a self-custody destination wallet, required for a virtual bank account whose converted funds settle straight into that wallet. Obtained from [prepare](/api-reference/Finance-API/virtual-bank-accounts/prepare-a-wallet-ownership-proof) and signed verbatim.
### Webhook
A registered HTTPS endpoint Venly pushes asynchronous events to, so you don't poll. See [Webhooks](/guides/finance/webhooks).
***
## On-chain primitives
### Permit (EIP-2612)
A signed message that grants a spender permission to move tokens from a wallet, without an on-chain approval transaction. Used to delegate spending authority cheaply. See [Get permit messages](/api-reference/Finance-API/permits/get-permit-messages).
### Allowance (ERC-20)
The amount of a token that a third party is approved to spend from a wallet. Queried per wallet + token. See [Get wallet token allowances](/api-reference/Finance-API/allowances/get-wallet-token-allowances).
### Stablecoin
A cryptocurrency pegged to a fiat currency. Which assets your tenant can settle in is configuration rather than a fixed list — read [`GET /supported-assets`](/api-reference/Finance-API/supported-assets/list-supported-assets) instead of hard-coding one. Support for gasless permits is a property of the token contract **on a specific chain**, so the same symbol can behave differently across chains.
### Blockchain Network
The chain an account's wallet is provisioned on, fixed at account creation. The chains the API accepts are listed in [Supported chains & assets](/guides/finance/supported-chains-and-assets); the chains available to **your** company are configured by Venly. Requesting a chain your company isn't set up for returns `400 invalid-request` with a message listing your supported chains. See [Supported chains & assets](/guides/finance/supported-chains-and-assets).
***
## API mechanics
### Idempotency Key
A UUID supplied in the request body of state-changing endpoints to make retries safe. Same key + same body returns the original response; same key + different body returns `422`. See the [Idempotency](/getting-started/conventions#idempotency) rules.
### Optimistic Locking (`version`)
On update endpoints, the request body includes a `version` integer matching the resource's current version. Concurrent updates with a stale `version` return `409 Conflict`. Refetch the resource, submit the `version` it returned, and retry — don't increment the number yourself.
### Pagination
Paginated list endpoints accept `page` (1-based), `size`, `sortOn`, and `sortOrder`, and return a `pagination` object. Not every list is paginated — check the endpoint. See [Pagination conventions](/getting-started/conventions#pagination).
### Error handling
Every response carries a `success` flag and, on failure, an `errors` array of `{ code, message }`. Branch on the stable `code`. See the [Errors & error codes](/guides/finance/errors) reference.
***
## Status enums (quick reference)
| Resource | Statuses |
| - | - |
| Party (`status`) | `ACTIVE`, `SUSPENDED`, `BLOCKED` |
| Party Role | `ACTIVE`, `INACTIVE` |
| KYC (`kycStatus` — individuals & accounts) | `VERIFICATION_PENDING`, `VERIFIED`, `REJECTED`, `NOT_REQUIRED` (accounts only, admin-enabled flag) |
| KYB (`kybStatus` — organisations) | `PENDING`, `VERIFIED`, `DENIED` |
| Verification linkage (`status`) | `NOT_LINKED`, `SUBMITTED`, `FORWARDED`, `ACCEPTED`, `COMPLETED`, `FAILED` |
| Account (`status`) | `ACTIVE`, `SUSPENDED`, `CLOSED` |
| Wallet AML (`amlStatus`) | `PENDING`, `APPROVED`, `FLAGGED`, `BLOCKED` |
| Virtual Bank Account | `PENDING`, `ACTIVE`, `CLOSED` |
| Payout Bank Account | `PENDING`, `ACTIVE`, `DISABLED` |
| Payout Route | `PENDING`, `REGISTERING`, `AWAITING_OWNERSHIP_PROOF`, `ACTIVE`, `REJECTED` |
| Payout | `REQUESTED`, `SENDING`, `PROVIDER_PROCESSING`, `COMPLETED`, `REJECTED`, `FAILED`, `RETURNED` |
| Transfer | `PENDING`, `COMPLETED`, `FAILED` |
| Permit | `PENDING`, `SUBMITTED`, `CONFIRMED`, `FAILED` |
| Payment Session | `CREATED`, `PENDING_PAYMENT`, `PAYMENT_RECEIVED` (terminal on the happy path), `FAILED`, `EXPIRED`, `CANCELLED`, `REFUNDING`, `REFUNDED` |
| Pay-in | `RECEIVED`, `SETTLING`, `SETTLED`, `FAILED`, `RETURNED` |
A wallet has **no `status` field** — `GET .../wallets` returns `amlStatus` only. Readiness isn't a single returned value: a **Venly-managed** wallet is ready once `amlStatus` is `APPROVED`; a **self-custody** wallet is ready once its [permit](/guides/finance/permits-and-allowances) `status` is `CONFIRMED`.
***
## See also
Watch every term above used in context, end to end.
Idempotency, pagination, and versioning rules.
# Safe retries with idempotency keys
Source: https://docs.venlyfinance.com/guides/finance/idempotency
Send a unique key with each request so a retry never creates a duplicate.
## Why
You send a request, but the network drops the response before it reaches you. Did it go through? With an idempotency key you can just retry: the same key returns the original result instead of doing the work a second time.
## How it works
The operations that create money movement take an `idempotencyKey` — a UUID you generate — in the request body:
```bash theme={null}
curl --request POST \
--url https://api.venlyfinance.com/v1/accounts/{senderAccountId}/transfers/crypto \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"receiverAccountId": "c3b2a1f0-9d8c-4e3a-bf21-1a2b3c4d5e60",
"chain": "BASE",
"asset": "USDC",
"amount": 25,
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
```
* The **first** call with a key runs the operation.
* The **same key with the same body** returns that original result — the operation runs once, however many times you retry.
### What a replay actually returns
A retry only replays cleanly when the original **succeeded**. Every other case is an error, and one of
them surprises people:
| You send | You get |
| - | - |
| Same key, identical body, original succeeded | The original result |
| Same key, original still in progress | `409 idempotency-conflict` |
| Same key, different body | `422 idempotency-conflict` |
| Same key, original **failed** | `422 idempotency-conflict` — generate a **new** key |
**A failed original spends the key.** Retrying a failed operation with the same key does not re-run it —
it returns `422`. To retry after a failure, generate a fresh key. This is the opposite of what "idempotent
retry" usually implies, so it is worth handling explicitly.
Treat `409` as "wait and re-read" rather than an error to surface: the original is still running, and
retrying the same key once it settles gives you the result.
Keys are unique **per company across all idempotent endpoints**, not per endpoint — so a key used on a
transfer cannot be reused on a pay-out.
## Two rules
1. Generate **one new key per operation** — each transfer, pay-in, pay-out, and virtual bank account gets its own.
2. Reuse a key **only to retry that exact call**. For a genuinely new operation, generate a new key.
A key is **required** on exactly these five operations — and accepted nowhere else:
| Operation | |
| - | - |
| Create a fiat transfer | `POST /accounts/{senderAccountId}/transfers/fiat` |
| Create a crypto transfer | `POST /accounts/{senderAccountId}/transfers/crypto` |
| Request a pay-out | `POST /accounts/{accountId}/payouts` |
| Create a virtual bank account | `POST /accounts/{accountId}/virtual-bank-accounts` |
| Create a pay-in session | `POST /accounts/{accountId}/fiat-to-crypto/payment-sessions` |
Every other endpoint ignores the field. Reads are naturally safe to repeat, and the remaining writes are
either idempotent by construction or keyed on a resource you already name in the path.
Save the key before you send the request. If your process restarts mid-flight, the retry reuses the same key and lands exactly once.
## Next steps
Pagination, versioning, and the shared idempotency rules.
Fiat and crypto transfers — each with its own key.
# Integration Walkthrough
Source: https://docs.venlyfinance.com/guides/finance/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.
**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).
## 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 party **verified** through the API
4. Have the customer **accept the partner terms** on a hosted page
5. Issue a **EUR IBAN** so they can fund via SEPA
6. 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 IV as Verification
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)
App->>API: POST /parties/{id}/verification
API-->>App: verificationUrl
IV-->>API: Verdict (asynchronous)
App->>API: GET /accounts/{id} → kycStatus VERIFIED
App->>API: GET /parties/{id}/partner-terms → REQUIRED + consentUrl
App->>App: hand consentUrl to the customer
IV-->>API: Customer accepts partner terms
App->>API: GET /parties/{id}/partner-terms → ACCEPTED
App->>API: POST /accounts/{id}/virtual-bank-accounts
API-->>App: deposit rails + referenceCode
App->>API: POST /accounts/{id}/transfers/crypto
API-->>App: transferId, status COMPLETED
```
The big gate to design around: an **account** is created unverified (`kycStatus: VERIFICATION_PENDING`) and its party must complete verification before it can move money. A party's `status` is `ACTIVE` right away, which is not the same as being verified. See [Account verification](/guides/finance/kyc-verification).
***
## Prerequisites
A `client_id` and `client_secret` from your Venly contact. Staging calls don't move real money.
All examples use `curl`. Postman, HTTPie, or any SDK works equally well.
Money-moving endpoints require an `idempotencyKey` (UUID v4) — `uuidgen`, `[guid]::NewGuid()`, or `crypto.randomUUID()`.
***
## 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" }
```
Cache the token. Refresh on `401 Unauthorized` or a few seconds before `expires_in` elapses — don't request a new token on every call.
***
## 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** — creating a party does not verify it, and `status: ACTIVE` is not a verification result. Verification happens later, in Step 4.
```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:00Z",
"updatedAt": "2026-01-15T09:30:00Z",
"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).
```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"
}'
```
```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
}
}
```
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.
### 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` because its party has no verification outcome yet — an account opened later for the same, verified party would be created `VERIFIED` and skip this step. Mint a hosted link with [`POST /parties/{partyId}/verification`](/api-reference/Finance-API/parties/create-a-verification-link) and hand it to the end-user, or forward an existing Sumsub verification at account creation. The verdict arrives asynchronously and `kycStatus` becomes `VERIFIED`. See [Onboarding & Verification](/guides/finance/onboarding/lifecycle).
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, 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 — Have the customer accept the partner terms
Verification proves who your customer is. It does not cover the terms our partners require them to
accept — and **they** must accept those themselves, on a page we hand you a link to. Until they do,
issuing an IBAN in the next step is blocked.
Read the party's consent state:
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/parties/{partyId}/partner-terms \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
```json Response (200) theme={null}
{
"success": true,
"result": {
"partyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "REQUIRED",
"consentUrl": "https://onboard.venly.io/partner-terms?token=cons-tok-9f3a2b"
}
}
```
`REQUIRED` means hand `consentUrl` to your customer. You cannot complete this for them. Once they
accept, the status becomes `ACCEPTED` and onboarding continues.
You can get ahead of this instead of waiting for it to block you:
[`POST /parties/{partyId}/partner-terms/link`](/api-reference/Finance-API/parties/issue-a-partner-terms-consent-link)
mints a link immediately, so the customer accepts every partner's terms **before** any of their data
is sent to a partner. Collecting consent during your own onboarding is usually the better experience.
`ACCEPTED` is not permanent. Adding a partner, or a partner publishing a new terms version, moves the
party back to `REQUIRED`. Re-read it rather than recording it once. See
[Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
***
## Step 6 — 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).
**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`.
***
## Step 7 — Issue a virtual bank account (IBAN)
Once the account is `VERIFIED` **and** its partner-terms consent is `ACCEPTED`, attach an IBAN so it can be funded via SEPA. Incoming euros are auto-converted to the `targetCryptocurrency` and credited to the wallet — read [`GET /supported-assets`](/api-reference/Finance-API/supported-assets/list-supported-assets) for the assets your tenant can settle in.
```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"
}
}
```
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.
***
## Step 8 — Fund the account
### Option A — SEPA transfer to the IBAN
The customer sends EUR from their bank to the IBAN from Step 7, 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 (coming soon)
**Not yet available.** Hosted pay-in sessions aren't enabled yet, and creating one is refused with `400 no-suitable-provider`. Use Option A for now.
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 9 — 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"
}'
```
**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.
***
## Step 10 — 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
Verification is not the last gate. Check the party's
[partner-terms consent](/guides/finance/onboarding/partner-terms-consent) — if `status` is `REQUIRED`,
your customer still has to accept on the hosted page, and no amount of retrying will clear it. Hand them
the `consentUrl`. This one is easy to mistake for a KYC problem because the account looks fully verified.
The **account** is still `VERIFICATION_PENDING`. Its party must complete [verification](/guides/finance/onboarding/lifecycle) first — poll `GET /accounts/{id}` until `kycStatus` is `VERIFIED`. (The party's `status` being `ACTIVE` is not enough.)
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.
Self-custody accounts must include the customer's wallet `address` on creation. Venly-managed accounts must not.
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.
Tokens last 5 minutes. Catch `401`, refresh, and retry the original call. Don't pre-authenticate every request — cache the token.
***
## Next steps
The condensed party → account → verify → IBAN flow.
Every endpoint, field, and error code.
Gasless token approvals for self-custody accounts.
Stuck? Get in touch with your `accountId` and the failing request.
# Account verification
Source: https://docs.venlyfinance.com/guides/finance/kyc-verification
Why a new account can't move money yet, and how it becomes verified.
Every account starts **unverified** and must be verified before it can move money. This is the most common reason an otherwise-correct request is rejected, so it's worth understanding early.
## The states
An account's `kycStatus` (returned by [Get an account](/api-reference/Finance-API/accounts/get-account-details)) is one of:
| `kycStatus` | Meaning |
| - | - |
| `VERIFICATION_PENDING` | New account whose holder has no verification outcome yet. Can be set up, but can't move money yet. |
| `VERIFIED` | Approved. All operations are available. |
| `REJECTED` | Verification was declined. |
| `NOT_REQUIRED` | You verify end-users yourself; Venly does not gate on its own verification. Requires a flag a **Venly admin** enables on your tenant — see [tenant-managed KYC](#tenant-managed-kyc) below. |
**A new account inherits its holder's outcome.** Since 1.8.0, linking a `partyId` that is already
`VERIFIED` creates the account `VERIFIED` — it can move money immediately, with no verification step.
A rejected holder yields a `REJECTED` account. Only a holder with no outcome yet — including one
created inline — gives `VERIFICATION_PENDING`. The server derives this; supplying `VERIFIED` yourself is
still refused with `kyc-status-not-settable`.
## Individuals and organisations (KYC & KYB)
The account's `kycStatus` above is what gates money movement. The underlying **party** is verified too, on a field that depends on its type:
| Party type | Field | States |
| - | - | - |
| `INDIVIDUAL` | `kycStatus` | `VERIFICATION_PENDING → VERIFIED` (or `REJECTED`) |
| `ORGANISATION` | `kybStatus` | `PENDING → VERIFIED` (or `DENIED`) |
**KYB** (Know Your Business) is the organisation equivalent of KYC — it verifies the company itself (and, typically, its representatives). Read `kybStatus` from [Get a party](/api-reference/Finance-API/parties/get-party-details). Like KYC, it runs asynchronously after you create the organisation party.
## How a party gets verified
You drive verification through the API. There are two routes:
[`POST /parties/{partyId}/verification`](/api-reference/Finance-API/parties/create-a-verification-link) mints a link you hand to the end-user. Works for individuals (KYC) and organisations (KYB).
Already verified the user yourself? Pass `party.sumsubToken` at account creation. Individuals on self-custody only.
The verdict arrives asynchronously and flips the party's `kycStatus`/`kybStatus`. See the [onboarding lifecycle](/guides/finance/onboarding/lifecycle) for the full picture, [Individual KYC](/guides/finance/onboarding/individual-kyc) and [Business KYB](/guides/finance/onboarding/business-kyb) for each party type, and [Troubleshooting](/guides/finance/onboarding/troubleshooting) for error codes and tracking linkage.
Check the current status at any time with [Get an account](/api-reference/Finance-API/accounts/get-account-details) and read the `kycStatus` field.
### Tenant-managed KYC
Passing `kycStatus: NOT_REQUIRED` when
[creating an account](/api-reference/Finance-API/accounts/create-a-new-account) declares that you verify
end-users within your own compliance stack, so Venly does not gate money movement on its own
verification. It lets you reach transfers and pay-outs without completing a verification flow per party.
**You cannot enable this yourself.** A **Venly admin** must turn the tenant-managed KYC flag on for your
company tenant. Until they do, supplying `NOT_REQUIRED` fails with `kyc-not-required-not-allowed` — it is
not a request you can retry your way past.
In practice the flag is granted for **testing**, not for production use. Production integrations verify
through a [hosted verification link](/guides/finance/onboarding/hosted-verification) or
[Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing).
Constraints:
* `NOT_REQUIRED` is the **only** client-settable value. Supplying `VERIFIED` or `REJECTED` is rejected with `kyc-status-not-settable`.
* It only works where a Venly admin has enabled the flag on the company tenant. Otherwise: `kyc-not-required-not-allowed`.
* It is settable **only at account creation** — you can't move an existing account to `NOT_REQUIRED`.
Omit `kycStatus` entirely for the normal flow.
**Don't let this path shape your integration.** If your test accounts always use `NOT_REQUIRED`, you
never exercise the
[pending-verification state](/guides/finance/onboarding/lifecycle#verdicts-are-asynchronous) — and the
first account that has to wait for a real verdict will block on a code path you never ran. Build the
pending state regardless.
## Verification is not the last step
`VERIFIED` means we know who the party is. It does not mean our partners will deal with them yet:
the end customer must also accept those partners' terms themselves, on a page we hand them a link to.
That acceptance is tracked in its own field on the party — not in `kycStatus` — and it gates
[virtual bank accounts](/guides/finance/virtual-bank-accounts) and [pay-outs](/guides/finance/payouts).
See [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
## Wallet readiness (AML, and permits for self-custody)
A `VERIFIED` `kycStatus` clears the **account**, but funds move through the account's **wallet**, which has its own gate. Each wallet returned by [list wallets](/api-reference/Finance-API/wallets/list-wallets-for-an-account) carries an `amlStatus`:
| `amlStatus` | Meaning |
| - | - |
| `PENDING` | Screening in progress. Money movement is blocked. |
| `APPROVED` | Cleared. |
| `FLAGGED` | Flagged for manual review. Money movement is blocked. |
| `BLOCKED` | Screening blocked the wallet. Money movement is blocked. |
Treat anything other than `APPROVED` as not-ready — `FLAGGED` and `BLOCKED` need Venly intervention, so surface them rather than retrying in a loop. An account is ready to move money when:
| Custody | Ready when |
| - | - |
| **Venly-managed** | `kycStatus` is `VERIFIED` **and** the wallet's `amlStatus` is `APPROVED` (both happen during onboarding — no further action). |
| **Self-custody** | the above **plus** a `CONFIRMED` [permit](/guides/finance/permits-and-allowances) on the account wallet — the one-time signature that lets Venly move its funds. |
The wallet has **no `status` field**, so use `amlStatus` (Venly-managed) and permit `status` (self-custody) as your readiness signals. See [Wallets & balances](/guides/finance/wallets).
## What's blocked until `VERIFIED`
These calls fail while an account is `VERIFICATION_PENDING`:
* [Create a crypto transfer](/api-reference/Finance-API/transfers/create-crypto-transfer) and [Create a fiat transfer](/api-reference/Finance-API/transfers/create-fiat-transfer)
* [Create a pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session)
* [Create a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account)
* [Request a payout](/api-reference/Finance-API/payouts/request-a-payout) — plus the recipient party needs cleared KYC/KYB before a [route](/guides/finance/payouts) can even be created
Each returns an error that means "this account isn't verified yet" — but the error `code` and HTTP status **depend on the operation**, so handle any of them as not-verified rather than matching a single code:
* **Virtual bank account** → `400` with code `kyc-not-verified`:
```json theme={null}
{
"success": false,
"errors": [
{ "code": "kyc-not-verified", "message": "Account KYC status must be VERIFIED to create a virtual bank account. Current status: VERIFICATION_PENDING" }
]
}
```
* **Pay-in session** → `422` with code `kyc-required`:
```json theme={null}
{
"success": false,
"errors": [
{ "code": "kyc-required", "message": "Account {accountId} KYC status is not verified" }
]
}
```
Transfers are gated the same way; check the [transfer endpoints](/api-reference/Finance-API/transfers/create-crypto-transfer) for the exact status code.
## What you can do before verification
Plenty of setup doesn't require a verified account:
* Create parties and accounts
* Mint a [verification link](/api-reference/Finance-API/parties/create-a-verification-link) and read [verification linkage](/api-reference/Finance-API/parties/get-verification-linkage)
* Add and remove party roles
* Read wallets, balances, permits, and allowances
* Register [payout bank accounts](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account) and [webhooks](/api-reference/Finance-API/webhooks/register-a-webhook)
* For self-custody, sign and submit the token permit — see [Approving transfers without gas](/guides/finance/permits-and-allowances)
Build the pending state into your flow: create the account, send the party through [verification](/guides/finance/onboarding/lifecycle), then wait for the verdict — [register a webhook](/guides/finance/webhooks) rather than polling, or poll [Get an account](/api-reference/Finance-API/accounts/get-account-details) until `kycStatus` is `VERIFIED` — before attempting transfers, payments, pay-ins, pay-outs, or virtual bank accounts.
## Next steps
The three gates, both verification routes, and what's blocked when.
Activate a self-custody wallet so Venly can move its funds.
See verification within the full end-to-end flow.
Get verdicts pushed to you instead of polling.
# Venly-managed vs self-custody
Source: https://docs.venlyfinance.com/guides/finance/managed-vs-self-custody
Two ways an account's wallet is controlled — and what changes for your integration.
When you create an account, Venly also sets up a crypto wallet for it. That wallet is controlled in one of two ways. Which one you use is fixed by your company setup, and it changes a few things in how you build.
## The two models
| | Venly-managed | Self-custody |
| - | - | - |
| Who holds the wallet's keys | Venly | Your customer |
| `address` when creating an account | Not needed — Venly creates the wallet | **Required** — you pass the customer's wallet address |
| Moving funds | Venly moves them directly | Venly moves them using a one-time approval the customer signs |
| Permits & allowances endpoints | Not used | Used to set up and check that approval |
## What this means in practice
**Venly-managed** is the simpler path. Venly creates and holds the wallet, so transfers and payments work with no extra setup from the customer.
**Self-custody** means your customer keeps control of their own wallet and keys. Before Venly can move their funds, the customer grants a one-time approval by signing a message — see [Approving transfers without gas](/guides/finance/permits-and-allowances).
## Single-key wallets only
A self-custody account wallet must be controlled by a **single private key** — an externally-owned
account (EOA). **Smart-contract wallets**, such as a Safe multisig or an ERC-4337 smart account, **are
not supported for now.**
Several steps need a signature that recovers to the wallet's own address: the
[permit](/guides/finance/permits-and-allowances) that lets Venly move funds, and the wallet-ownership
proofs some [virtual bank accounts](/guides/finance/virtual-bank-accounts#proving-wallet-ownership)
and [pay-out routes](/guides/finance/payouts#proving-you-control-the-source-wallet) ask for. Only an
EOA can produce those (EIP-2612 and EIP-191); ERC-1271 contract signatures are not accepted.
## Permit and allowance endpoints
The permit and allowance endpoints only apply to self-custody accounts. Calling them on a Venly-managed account returns:
```json theme={null}
{
"success": false,
"errors": [
{ "code": "invalid-request", "message": "Permits are only applicable for SELF_CUSTODY companies" }
]
}
```
A wallet's `type` field tells you which model it uses — `VENLY_MANAGED` or `SELF_CUSTODY`. You'll see it in the [list wallets](/api-reference/Finance-API/wallets/list-wallets-for-an-account) response.
## No wallet infrastructure yet?
Self-custody assumes your customers already hold wallets. If they don't — and you'd rather issue wallets yourself than send users to a third party — you can become a wallet provider with the **Venly Wallet API**. See [Become a wallet provider](/getting-started/wallet-provider).
## Next steps
The one-time approval a self-custody wallet signs.
Read a wallet's `type` to confirm its custody model.
No wallet infrastructure? Issue wallets to your own users with the Venly Wallet API.
# Business KYB
Source: https://docs.venlyfinance.com/guides/finance/onboarding/business-kyb
Verifying a company — the states, why the status values differ from KYC, and the one route available.
**KYB** (Know Your Business) verifies a company or business rather than a person. It applies to
parties with `partyType: ORGANISATION` and is tracked on `kybStatus` — a **different field with
different values** from KYC.
## States
Read `kybStatus` from [Get a party](/api-reference/Finance-API/parties/get-party-details):
| `kybStatus` | Meaning |
| - | - |
| `PENDING` | Created, awaiting a verdict. The starting state. |
| `VERIFIED` | Approved. |
| `DENIED` | Declined. |
KYB does not reuse the KYC values. The pending state is `PENDING`, not `VERIFICATION_PENDING`, and the
failure state is `DENIED`, not `REJECTED`. Only `VERIFIED` is shared.
If you branch on verification status, read the field that matches the party's type — a check written
against `VERIFICATION_PENDING` silently never matches an organisation.
| Party type | Field | Pending | Approved | Declined |
| - | - | - | - | - |
| `INDIVIDUAL` | `kycStatus` | `VERIFICATION_PENDING` | `VERIFIED` | `REJECTED` |
| `ORGANISATION` | `kybStatus` | `PENDING` | `VERIFIED` | `DENIED` |
## Create the party
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/parties \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"partyType": "ORGANISATION",
"name": "Acme Trading BV",
"vatNumber": "NL123456789B01",
"externalId": "org-4471",
"address": {
"addressLine1": "123 Main Street",
"city": "Amsterdam",
"postalCode": "1012AB",
"country": "NL"
}
}'
```
`name` is required for organisations — and it must be the **legal** entity name, not a trading or
display name, because it's what gets checked against registry records and what appears as the
beneficiary name on bank rails. `vatNumber` is optional but speeds verification along where you have it.
`firstName` and `lastName` don't apply to organisations.
Downstream bank-account provisioning requires the account holder to have a legal name. An
organisation party with no `name` fails virtual-bank-account creation with `party-name-required`.
## One route: hosted verification
Organisations verify through the [hosted verification link](/guides/finance/onboarding/hosted-verification).
[`POST /parties/{partyId}/verification`](/api-reference/Finance-API/parties/create-a-verification-link)
returns the **KYB** flow URL automatically — the party's stored `partyType` decides which flow you get,
so there is no parameter to set.
**[Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing) is not available for
organisations.** It's KYC-only. Supplying `party.sumsubToken` with `partyType: ORGANISATION` is
rejected with `400 verification-kyc-only`.
## What gets collected
KYB verifies the business itself — registry existence, incorporation details, ownership structure —
and typically also identifies its representatives and beneficial owners. That makes it a heavier
process than individual KYC, and usually a slower one.
Practically: expect KYB to sit in `PENDING` longer than KYC sits in `VERIFICATION_PENDING`, and don't
build UI that treats a multi-day pending state as an error.
## After the verdict
```mermaid theme={null}
stateDiagram-v2
[*] --> PENDING: party created
PENDING --> VERIFIED: approved
PENDING --> DENIED: declined
VERIFIED --> [*]
```
The verdict flips `kybStatus` on the party and clears the account's `kycStatus` for money movement.
Note that the *account* field is called `kycStatus` regardless of whether its holder is an individual
or an organisation — there is no `kybStatus` on an account.
[Register a webhook](/guides/finance/webhooks) rather than polling for days.
### If it's denied
`DENIED` is terminal for that attempt. You can mint a fresh link — a `DENIED` party isn't `VERIFIED`,
so it won't hit `party-already-verified`. For a disputed decision, quote the party's `ivCaseReference`
from [Get verification linkage](/api-reference/Finance-API/parties/get-verification-linkage) to Venly
support.
## Organisations as pay-out recipients
A verified organisation is often the beneficiary of a [pay-out](/guides/finance/payouts) rather than
the account holder. That needs a cleared `kybStatus` **and** an `ACTIVE` `PAYOUT_RECIPIENT` role on
the paying account — see [the recipient gate](/guides/finance/onboarding/roles-and-entities#the-recipient-gate).
A cleared KYB alone is not enough; a missing role surfaces as `422 recipient-not-authorized`.
## Next steps
The route organisations use.
The natural-person equivalent.
How an organisation party relates to accounts and pay-outs.
Every verification error code.
# Hosted verification links
Source: https://docs.venlyfinance.com/guides/finance/onboarding/hosted-verification
Mint a Venly-hosted KYC or KYB link, hand it to the end-user, and collect the verdict.
The default verification route. Venly hosts the onboarding page, so you never handle identity
documents — you mint a URL, hand it over, and wait.
Works for both party types: the KYC flow for an `INDIVIDUAL`, the KYB flow for an `ORGANISATION`.
Create an [`INDIVIDUAL` or `ORGANISATION` party](/api-reference/Finance-API/parties/create-a-new-party).
It starts unverified.
[`POST /parties/{partyId}/verification`](/api-reference/Finance-API/parties/create-a-verification-link)
with **no request body** — the party's stored `partyType` decides the flow.
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/parties/{partyId}/verification \
-H "Authorization: Bearer {access_token}"
```
```json theme={null}
{
"success": true,
"result": {
"partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22",
"verificationUrl": "https://onboard.venly.io/kyc?token=inv-tok-9f2c41ab",
"status": "VERIFICATION_PENDING"
}
}
```
Only the URL for the party's own flow is returned — there's no choice to make and no second URL.
Redirect them, or deliver it over a channel you trust.
Treat `verificationUrl` as a credential. It grants access to that user's onboarding session — don't log it, don't put it anywhere shared, and don't email it to a group address.
The verdict arrives asynchronously. [Register a webhook](/guides/finance/webhooks), or poll
[Get a party](/api-reference/Finance-API/parties/get-party-details) and read `kycStatus`
(individuals) or `kybStatus` (organisations).
## Links do not expire
Worth internalising, because it simplifies your error handling more than you'd expect:
| You call it again… | You get |
| - | - |
| While the link is still live | `200` with the **same** `verificationUrl`. No new session is minted. |
| After the link was revoked on Venly's side | `201` with a **freshly minted** URL. |
| When the party is already `VERIFIED` | `409 party-already-verified`. |
So **"re-send the verification link" is a safe, idempotent operation.** You don't need to store the
URL, track link state, or worry about handing a user a dead link by calling twice. If a user says they
lost the email, just call it again.
The distinction between `200` and `201` is informational — both carry a usable URL in the same shape.
Treat any `2xx` as success rather than branching on the code.
## The `status` field
The response's `status` is the party's verification status **at mint time**, which tells you what state
the party was in when you asked:
| Party type | Possible values |
| - | - |
| `INDIVIDUAL` | `VERIFICATION_PENDING`, `REJECTED` |
| `ORGANISATION` | `PENDING`, `DENIED` |
It is never `VERIFIED` — an already-verified party returns `409` instead of a link. Seeing `REJECTED`
or `DENIED` here means you're re-verifying someone who previously failed, which is allowed.
## Errors
| Status | Code | Retryable | What to do |
| - | - | - | - |
| `409` | `verification-not-provisioned` | No | Your company has no provisioned verification tenant. Contact Venly — configuration, not something retrying fixes. |
| `409` | `party-already-verified` | No | Nothing to do; the party is done. |
| `404` | — | No | No such party for your company. |
| `500` | `identity-verification-rejected` | No | The platform rejected the request itself. Contact support. |
| `503` | `identity-verification-unavailable` | **Yes** | Platform unreachable. Retry with backoff. |
Full list in [Troubleshooting](/guides/finance/onboarding/troubleshooting).
## When to use this route
You have no existing KYC provider · you're verifying an organisation · your company runs
Venly-managed wallets · you'd rather not touch identity documents at all
You already verified this **individual** in your own Sumsub account **and** run `SELF_CUSTODY` —
then token sharing avoids a second flow
## Next steps
The alternative route for individuals.
Tracking linkage and resolving failures.
# Individual KYC
Source: https://docs.venlyfinance.com/guides/finance/onboarding/individual-kyc
Verifying a natural person — the states, both routes, and what happens to the account.
**KYC** (Know Your Customer) verifies a natural person. It applies to parties with
`partyType: INDIVIDUAL` and is tracked on `kycStatus`.
## States
Read `kycStatus` from [Get a party](/api-reference/Finance-API/parties/get-party-details):
| `kycStatus` | Meaning |
| - | - |
| `VERIFICATION_PENDING` | Created, awaiting a verdict. The starting state. |
| `VERIFIED` | Approved. |
| `REJECTED` | Declined. |
The account under this party has its **own** `kycStatus`, and that one is what gates money movement.
It also accepts a fourth value, `NOT_REQUIRED`, which parties never carry. See
[Roles & entities](/guides/finance/onboarding/roles-and-entities#account).
## Create the party
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/parties \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"partyType": "INDIVIDUAL",
"firstName": "Jane",
"lastName": "Doe",
"externalId": "user-12345",
"address": {
"addressLine1": "123 Main Street",
"city": "Amsterdam",
"postalCode": "1012AB",
"country": "NL"
}
}'
```
`firstName` and `lastName` are required for individuals. `externalId` is your own identifier — useful
for reconciling against your user table, and filterable on
[List parties](/api-reference/Finance-API/parties/list-all-parties).
The party comes back with `status: ACTIVE` and `kycStatus: VERIFICATION_PENDING`. `ACTIVE` here means
the record is live, **not** that Jane has been identified.
## Two routes
Venly hosts the flow. Mint a link, hand it to Jane, wait for the verdict. Works for anyone.
Already verified Jane in your own Sumsub account? Forward a share token instead of re-verifying.
Token sharing is the narrower one — it needs a `SELF_CUSTODY` company wallet type and must happen at
account creation. If either doesn't hold, use the hosted link.
There is also [tenant-managed KYC](/guides/finance/kyc-verification#tenant-managed-kyc), where you
declare `kycStatus: NOT_REQUIRED` on the account and skip Venly's verification. It needs a flag a Venly
admin enables on your tenant, and is granted for testing rather than production — so treat it as a way
to exercise money movement in tests, not as a third onboarding route.
## What gets collected
The hosted flow collects and checks identity documents, a selfie/liveness check, and the person's
address details, then returns a single approve/decline verdict. You never handle the documents —
they go straight to verification, and Venly's API only ever exposes the resulting
status.
Supply the party's `address` when you create it where you have it. It's used in verification and
in downstream bank-rail provisioning, which requires a legal name and address on the account holder.
## After the verdict
The verdict is asynchronous and flips `kycStatus` on the party, which in turn clears the account.
```mermaid theme={null}
stateDiagram-v2
[*] --> VERIFICATION_PENDING: party created
VERIFICATION_PENDING --> VERIFIED: approved
VERIFICATION_PENDING --> REJECTED: declined
VERIFIED --> [*]
```
[Register a webhook](/guides/finance/webhooks) to be told, rather than polling — KYC verdicts are
human-paced.
Once `VERIFIED`, the account still needs its wallet cleared before it can transact. See
[Wallet readiness](/guides/finance/kyc-verification#wallet-readiness-aml-and-permits-for-self-custody).
### If it's rejected
`REJECTED` is terminal for that verification attempt. The party stays in place, but its account can't
move money. Minting a new link for a rejected party is allowed — a `REJECTED` party is not
`VERIFIED`, so it doesn't hit the `party-already-verified` conflict. If a rejection looks wrong,
quote the party's `ivCaseReference` from
[Get verification linkage](/api-reference/Finance-API/parties/get-verification-linkage) to Venly
support; that reference survives a decline specifically so it can be investigated.
## Next steps
The default route, step by step.
The organisation equivalent.
Every verification error code.
How a verified party turns into an account that can transact.
# Onboarding lifecycle
Source: https://docs.venlyfinance.com/guides/finance/onboarding/lifecycle
From a brand-new party to an account that can move money — the states, the gates, and who clears them.
Nothing moves money until the right things are verified. This page is the map: what gets created, what gets verified, and which gate blocks what.
```mermaid theme={null}
flowchart TD
T["Company tenant provisioned
Venly, once per company"] --> P["Create party
unverified"]
P --> V{"Verification route"}
V -->|"Hosted link"| H["End-user completes
Venly-hosted flow"]
V -->|"Sumsub token"| S["Forward a verification
you already ran"]
V -->|"Tenant-managed
flag required"| N["You verify;
declare NOT_REQUIRED"]
H --> W["Verdict applied
kycStatus / kybStatus"]
S --> W
W --> A["Account VERIFIED"]
N --> A
A --> C{"Partner terms
outstanding?"}
C -->|"Yes"| CP["End customer accepts
on hosted consent page"]
C -->|"No"| M
CP --> M["Wallet AML APPROVED
+ permit if self-custody"]
M --> R["Can move money"]
```
## The four things that get cleared
People confuse these constantly, so it's worth separating them up front. They are **not** the same gate.
| # | What | Field | Who clears it |
| - | - | - | - |
| 1 | **Your company** (the tenant) | — | Venly, once, before you can call verification endpoints at all |
| 2 | **The party** (your end-user) | `kycStatus` / `kybStatus` | Venly, per party |
| 3 | **Partner terms** | consent state on the party | **Your end customer**, on a [hosted page](/guides/finance/onboarding/partner-terms-consent) |
| 4 | **The wallet** | `amlStatus` (+ permit) | Screening, per wallet |
All four must be clear. A `VERIFIED` party with a `FLAGGED` wallet still cannot move money — see [Wallet readiness](/guides/finance/kyc-verification#wallet-readiness-aml-and-permits-for-self-custody).
Your company tenant is provisioned by Venly, not through the API. If it isn't, every verification call fails with `verification-not-provisioned` — and no amount of retrying will fix it. See [Troubleshooting](/guides/finance/onboarding/troubleshooting).
## Party vs. account
A party's `status` is `ACTIVE` the moment you create it. **That is not verification.** It means the record is usable, not that the person behind it has been identified.
| Object | Field | Starts at | Meaning |
| - | - | - | - |
| Party | `status` | `ACTIVE` | The record is live. Says nothing about identity. |
| Party | `kycStatus` / `kybStatus` | `VERIFICATION_PENDING` / `PENDING` | Identity verification state. |
| Account | `kycStatus` | `VERIFICATION_PENDING` — or `VERIFIED` straight away if the holder already is | **This is the gate on money movement.** |
The account's `kycStatus` is what every money-movement endpoint checks. It follows from its account-holder party's verification — including at creation: an account opened for an already-verified party is created `VERIFIED`, so a customer's second account needs no second verification.
## Pick a route
**Two routes carry a production integration**, and which you can use is mostly decided for you:
Venly hosts the flow; you hand over a URL. **Works for individuals and organisations.** The default.
Forward a verification you already ran in your own Sumsub account. **Individuals on self-custody only.**
| If you… | Use |
| - | - |
| Have no existing KYC provider | [Hosted verification link](/guides/finance/onboarding/hosted-verification) |
| Are verifying a business | [Hosted verification link](/guides/finance/onboarding/hosted-verification) — KYB has no token-sharing route |
| Already verified this individual in Sumsub, and run self-custody | [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing) |
| Run your own full compliance stack | One of the two above — see the note below |
There is a third path, [tenant-managed KYC](/guides/finance/kyc-verification#tenant-managed-kyc), where
you declare `kycStatus: NOT_REQUIRED` on the account and Venly doesn't gate on its own verification.
It is **not something you can switch on yourself**: a Venly admin has to enable the flag on your company
tenant, and without it the call fails with `kyc-not-required-not-allowed`. In practice it's granted for
testing rather than production use, so **plan your production integration around a hosted link or Sumsub
token sharing** and treat tenant-managed KYC as a way to exercise money movement without real
verifications.
## Verdicts are asynchronous
No verification route returns a verdict inline. In every case you create or forward something, get an acknowledgement, and the outcome lands later.
That shapes your integration: **build the pending state in from the start.** Don't write code that assumes a party is verified immediately after you mint a link.
Mint a link, or forward a token at account creation.
[Register a webhook](/guides/finance/webhooks) so the verdict is pushed to you. Polling
[Get a party](/api-reference/Finance-API/parties/get-party-details) works, but verdicts are
human-paced — polling for hours is wasteful.
`kycStatus` (individuals) or `kybStatus` (organisations) on the party; `kycStatus` on the account
for the money-movement gate.
## What you can do while unverified
Plenty. Verification blocks money movement, not setup:
| Available before verification | Blocked until verified |
| - | - |
| Create parties and accounts | Crypto and fiat [transfers](/guides/finance/transfers) |
| Add and remove party roles | [Transfers](/guides/finance/transfers) |
| Read wallets, balances, permits, allowances | [Pay-in sessions](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session) |
| Sign and submit the self-custody [permit](/guides/finance/permits-and-allowances) | [Virtual bank accounts](/guides/finance/virtual-bank-accounts) |
| Register [payout bank accounts](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account) and [webhooks](/guides/finance/webhooks) | [Pay-outs](/guides/finance/payouts) |
Front-load that left column while verification is in flight.
## Next steps
Company, party, account, and role — which object holds what.
Verifying a natural person.
Verifying a company or business.
Every verification error code and what to do about it.
# Partner onboarding
Source: https://docs.venlyfinance.com/guides/finance/onboarding/partner-onboarding
The third onboarding gate: your customer must be accepted by our banking partner before a virtual bank account or pay-out route can be provisioned — and you can now start and watch that step yourself.
Getting an individual from "verified" to "can receive bank money" involves a step you could not see
until Finance API 1.7.0. Once identity verification has cleared and the customer has accepted the
partner terms, their verified data is relayed to our **banking partner**, which runs its own recipient
checks before it will issue bank details or accept a pay-out destination for them.
Until 1.7.0 that step was invisible: it started the first time you tried to provision, and you learned
about it only when the call failed with `409 partner-onboarding-pending`. It is now a readable — and
startable — resource of its own.
## The three gates, side by side
| Gate | Who clears it | Read it at | Blocks |
| - | - | - | - |
| Identity verification | Your customer, on the hosted flow | [`/iv-verification`](/api-reference/Finance-API/parties/get-verification-linkage), `kycStatus` | Everything that moves money |
| [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent) | Your customer, on the consent page | [`/partner-terms`](/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state) | Virtual bank accounts, pay-outs |
| **Partner onboarding** | Our banking partner, asynchronously | [`/partner-onboarding`](/api-reference/Finance-API/parties/get-a-partys-partner-onboarding-status) | Virtual bank accounts, pay-out routes |
They are independent. A party can be `VERIFIED`, have `ACCEPTED` the terms, and still be `PENDING`
here.
## Reading the status
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/parties/{partyId}/partner-onboarding \
-H "Authorization: Bearer {access_token}"
```
```json Response (200) theme={null}
{
"success": true,
"result": {
"partyId": "7e3b9c2a-1f4d-4a8b-9c11-2d6e8f0a1b22",
"status": "PENDING"
}
}
```
| `status` | Meaning | Provisioning answers |
| - | - | - |
| `NOT_STARTED` | The party has not been relayed to a partner yet. A normal `200`, not an error. | Triggers onboarding, then `409 partner-onboarding-pending` |
| `PENDING` | Relayed; the partner is still checking. | `409 partner-onboarding-pending` — retryable |
| `APPROVED` | The gate is satisfied. | Proceeds |
| `REJECTED` | Declined or revoked. | `422 partner-onboarding-rejected` — terminal for that attempt |
Reading never triggers anything. The same value is cached on the party as `partnerOnboardingStatus`,
so a plain [Get a party](/api-reference/Finance-API/parties/get-party-details) shows it too.
## Starting it early
Rather than letting the first virtual-bank-account call kick onboarding off and fail, start it as soon
as the party is verified:
```bash theme={null}
curl -X POST https://api-staging.venlyfinance.com/v1/parties/{partyId}/partner-onboarding \
-H "Authorization: Bearer {access_token}"
```
The call is asynchronous — it answers `202` with the current status, normally `PENDING` — and
idempotent while onboarding is `PENDING` or `APPROVED`. Poll the read endpoint for the outcome.
Provisioning a virtual bank account after `APPROVED` then succeeds first time.
`REJECTED` is not a dead end. Calling `POST` again re-attempts onboarding, so if the underlying reason
has been resolved — corrected details, a re-run verification — you can retry without involving Venly.
### Preconditions
The request is refused with a **retryable** `409` until the party qualifies:
| `code` | Fix |
| - | - |
| `recipient-not-verified` | Complete [identity verification](/guides/finance/onboarding/lifecycle) first. |
| `party-not-account-holder` | Give the party an active `ACCOUNT_HOLDER` role on an account. |
| `verification-not-provisioned` | Identity verification is not enabled for your company — contact Venly. |
And with a terminal `400 party-type-not-supported` if the party is an `ORGANISATION`: partner
onboarding is individual-scoped.
## Where it fits in the flow
```mermaid theme={null}
flowchart LR
V["Verified
kycStatus VERIFIED"] --> T["Partner terms
ACCEPTED"]
T --> O["Partner onboarding
POST → PENDING → APPROVED"]
O --> P["Provision
VBA / payout route"]
```
Front-load all three while your customer is still in your onboarding UI. Verification and consent need
the customer present; partner onboarding does not, so start it the moment verification clears and it
will usually be `APPROVED` before the customer asks for bank details.
## Next steps
The gate that comes just before this one.
The first thing partner onboarding unblocks.
The retryable / terminal split across every onboarding error.
All the states, from a new party to an account that can move money.
# Partner-terms consent
Source: https://docs.venlyfinance.com/guides/finance/onboarding/partner-terms-consent
Your end customer accepts our partners' terms themselves on a hosted page — and until they do, virtual bank accounts and pay-outs stay blocked.
Verifying a party proves who they are. It does **not** cover the terms our banking and payment partners require that person to agree to. Those partners insist the real end customer reads and accepts their disclosures on a screen themselves — nobody may accept on their behalf.
So there is one more step, and it belongs to your customer rather than to you: they visit a hosted page and accept each outstanding partner's terms. You can let it surface as a gate after verification, or [collect it up front](#collecting-consent-up-front) before any of their data reaches a partner.
This is a **gate, not an optional extra**. A party cannot get a [virtual bank account](/guides/finance/virtual-bank-accounts) or receive a [pay-out](/guides/finance/payouts) until its partner-terms consent is complete.
## What you do
Three steps, and the middle one is not yours.
[`GET /parties/{partyId}/partner-terms`](/api-reference/Finance-API/parties/get-a-partys-partner-terms-consent-state) returns one `status` and, while acceptance is outstanding, the `consentUrl`.
| `status` | What you do |
| - | - |
| `NOT_REQUIRED` | Nothing — including for a party that hasn't passed KYC yet |
| `REQUIRED` | Hand `consentUrl` to the end customer |
| `ACCEPTED` | Nothing — every enabled partner's currently-active terms are accepted |
The status alone tells you whether a link must be presented. You never need to know which partners are involved or how many — it is one status and one link covering all of them.
Present the `consentUrl` — a redirect, an email, wherever your flow puts it. They open it and accept.
You cannot complete this step for them. That is the entire point of the requirement.
Once acceptance is recorded the status becomes `ACCEPTED` and the URL is no longer returned.
[Register a webhook](/guides/finance/webhooks) rather than polling.
The party read carries the same state as a cached field, `partnerTermsStatus` on
[Get party details](/api-reference/Finance-API/parties/get-party-details), if you would rather poll the party you are already fetching. The dedicated endpoint always returns the current link, so it is the one to use when you are about to show a link — a URL you stored earlier may have expired.
## Collecting consent up front
The flow above is reactive: you discover consent is needed when a party reaches the gate. You can also get ahead of it.
[`POST /parties/{partyId}/partner-terms/link`](/api-reference/Finance-API/parties/issue-a-partner-terms-consent-link) mints a link immediately, letting your customer accept **every currently-enabled partner's latest terms in one pass — before any of their data is sent to a partner**. That is usually the better experience: consent is collected during your own onboarding rather than surfacing later as a blocker on a pay-out.
Each call **supersedes the previous link** for that party. A link you already handed out stops working. Never call it to check state — read the consent state for that.
## What the page does
You do not build or host this page — Venly provides it and hands you the link. Two properties of it affect your flow:
* **One page for every outstanding partner.** However many partners a party owes terms to, it is a single link and a single visit. Your customer is never bounced through one page per partner.
* **A separate acceptance per partner.** There is no blanket accept-all, so a customer with several outstanding partners has several things to agree to before the page will let them finish. Expect the visit to take a moment rather than one click.
Each acceptance is recorded individually — who accepted, when, and which version of that partner's terms they saw.
**The terms themselves are not available over the API yet.** Today the hosted page is the only place the
disclosures are presented — you cannot retrieve their text or render them in your own interface. Exposing the
terms and conditions through the API is planned for a later phase; until then, hand the customer the link.
## It can come back
Consent is not once-and-forever. The requirement re-appears when:
* a **new partner** is added for that party, or
* an existing partner's **terms version changes**.
When that happens the party returns to "acceptance required" with a refreshed link, and the page shows only what is newly outstanding — your customer does not re-accept what they already agreed to.
Treat consent as a state you re-read, not a milestone you record once and forget. A party that was fully onboarded last month can legitimately need another visit.
## Handling the link
| Property | What it means for you |
| - | - |
| **Scoped** | The link is tied to that party and those terms. It is not a generic page and must not be shared between customers. |
| **Expires** | An expired link is rejected when opened. Read the party again to get a fresh one rather than caching it. |
| **Re-issuable** | A fresh link can always be issued for the same party, so an expired or lost link is never a dead end. |
Because the link expires, fetch it when you are about to show it. A consent URL stored at verification time and used days later will likely be stale.
## When the platform is unreachable
The consent state is resolved at request time rather than stored, so the read can fail. It **fails closed**, deliberately:
| HTTP | `code` | Meaning | Retry? |
| - | - | - | - |
| `500` | `identity-verification-rejected` | The platform refused the request itself | **No** |
| `503` | `identity-verification-unavailable` | Unreachable, or answered but the state could not be resolved safely | **Yes**, with backoff |
A `503` **never** means "nothing outstanding". If the platform reports terms outstanding but no link, you get a `503` rather than a `REQUIRED` you cannot act on — so treating a failure as `NOT_REQUIRED` would skip the gate entirely.
## What this changes for existing integrations
The consent state lives in its **own field**, `partnerTermsStatus` — it is not a new `kycStatus` value. Code that switches on `kycStatus` keeps working unchanged.
What does change is the set of preconditions for money movement. If your integration creates virtual bank accounts or pay-outs, it needs to account for a verified party that is still not ready, and surface the consent link to the customer at that point.
## Next steps
Where consent sits among the other gates.
KYC and KYB, and what they do and don't cover.
One of the two flows consent gates.
The other one.
# Roles & entities
Source: https://docs.venlyfinance.com/guides/finance/onboarding/roles-and-entities
Company, party, account, wallet, and role — which object holds identity, and which one gates money.
Four objects, and it matters which one you're asking about. Verification lives on the **party**. The gate on money movement lives on the **account**. Permission to receive a pay-out lives on the **role**.
```mermaid theme={null}
flowchart TD
C["Company (your tenant)
provisioned by Venly"] --> P1["Party · INDIVIDUAL
kycStatus"]
C --> P2["Party · ORGANISATION
kybStatus"]
P1 -->|"role: ACCOUNT_HOLDER"| A["Account
kycStatus gates money"]
P2 -->|"role: PAYOUT_RECIPIENT"| A
A --> W["Wallet · per chain
amlStatus"]
```
## Company (tenant)
You. Created and configured by Venly, not through the API. Your company carries the settings that silently decide what the rest of the API will let you do:
| Setting | Why you care |
| - | - |
| Wallet type — `VENLY_MANAGED` or `SELF_CUSTODY` | Decides whether Venly generates wallets or you supply addresses, and whether [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing) is available at all. See [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody). |
| Supported chains | Creating an account on a chain you're not configured for fails, even if the API accepts the enum value. |
| Supported assets | Constrains `targetCryptocurrency` and pay-in/pay-out asset choices. |
| Verification tenant provisioned | Without it, every verification call returns `verification-not-provisioned`. |
| Tenant-managed KYC enabled | Whether you may declare `kycStatus: NOT_REQUIRED`. Enabled by a **Venly admin** per tenant; granted for testing rather than production use. |
| Deposit settlement | Whether a customer-signed [wallet-ownership proof](/guides/finance/virtual-bank-accounts#proving-wallet-ownership) is required before a virtual bank account can be created. Call `prepare` to find out at runtime. |
Most "why does this work in the docs but not for me" moments trace back to this table. None of it is visible in the API responses, so if a call fails on configuration grounds, check with Venly rather than reworking your request.
## Party
The legal identity behind an account — a natural person or a business. **This is the only object that gets identity-verified.**
| `partyType` | Required fields | Verification field | States |
| - | - | - | - |
| `INDIVIDUAL` | `firstName`, `lastName` | `kycStatus` | `VERIFICATION_PENDING → VERIFIED` (or `REJECTED`) |
| `ORGANISATION` | `name` (optionally `vatNumber`) | `kybStatus` | `PENDING → VERIFIED` (or `DENIED`) |
A party's own `status` is separate and unrelated to verification:
| `status` | Meaning |
| - | - |
| `ACTIVE` | The record is usable. **Set immediately on creation — not a verification signal.** |
| `SUSPENDED` | Temporarily unusable. |
| `BLOCKED` | Permanently unusable. |
Parties are created independently of accounts and can be reused: one party can hold roles on several accounts.
See [Individual KYC](/guides/finance/onboarding/individual-kyc) and [Business KYB](/guides/finance/onboarding/business-kyb).
## Account
The money container. It holds wallets, virtual bank accounts, and pay-out routes.
An account carries its **own** `kycStatus`, and **this is the field every money-movement endpoint checks**:
| `kycStatus` | Money movement |
| - | - |
| `VERIFICATION_PENDING` | Blocked |
| `VERIFIED` | Allowed |
| `REJECTED` | Blocked |
| `NOT_REQUIRED` | Allowed — you verify end-users yourself. Needs an admin-enabled tenant flag ([tenant-managed KYC](/guides/finance/kyc-verification#tenant-managed-kyc)) |
Don't read the party's `kycStatus` to decide whether you can transfer. Read the **account's**. They're related but they are different fields on different objects, and only one of them is the gate.
An account needs exactly one active account-holder party. If it resolves to zero or several, provisioning calls fail with `account-holder-unresolved`.
## Party role
The link between a party and an account, and what that party is allowed to do there.
| `roleType` | Meaning |
| - | - |
| `ACCOUNT_HOLDER` | The party owns or co-owns the account. Multiple holders enable joint accounts. |
| `PAYOUT_RECIPIENT` | The party is an allowed [pay-out](/guides/finance/payouts) destination for this account. |
Each role has a `status` of `ACTIVE` or `INACTIVE`.
### The recipient gate
`PAYOUT_RECIPIENT` is the one role with teeth beyond bookkeeping. Before you can create a
[pay-out route](/api-reference/Finance-API/payout-routes/create-a-payout-route) for a bank account,
its owning party needs an `ACTIVE` `PAYOUT_RECIPIENT` role on the paying account **with cleared
KYC/KYB**.
It's re-checked on every pay-out request, not just at route creation — so setting the role to
`INACTIVE` stops future pay-outs immediately.
A party can hold `PAYOUT_RECIPIENT` **and** `ACCOUNT_HOLDER` on the same account. That's the
self-payout case: a user withdrawing to their own bank account.
Manage roles with [Add a party to an account](/api-reference/Finance-API/accounts/add-a-party-to-an-account)
and [Remove a party](/api-reference/Finance-API/accounts/remove-a-party-from-an-account).
## Wallet
Where crypto actually sits, one per chain per account. Wallets aren't identity-verified, but they are
**screened**, and screening is its own gate:
| `amlStatus` | Money movement |
| - | - |
| `PENDING` | Blocked — screening in progress |
| `APPROVED` | Allowed |
| `FLAGGED` | Blocked — needs manual review |
| `BLOCKED` | Blocked |
A wallet has **no `status` field**. For self-custody, readiness also requires a `CONFIRMED`
[permit](/guides/finance/permits-and-allowances). See [Wallets & balances](/guides/finance/wallets).
## Quick reference
| Question | Read |
| - | - |
| Has this person passed KYC? | Party `kycStatus` |
| Has this business passed KYB? | Party `kybStatus` |
| Can this account move money? | **Account `kycStatus`** |
| Is the wallet cleared? | Wallet `amlStatus` (+ permit for self-custody) |
| Can this party receive a pay-out here? | `PAYOUT_RECIPIENT` role `status` + the party's KYC/KYB |
| Where is the verification case? | [Verification linkage](/api-reference/Finance-API/parties/get-verification-linkage) |
## Next steps
How these objects move from created to able-to-transact.
The full resource graph.
# Sumsub token sharing
Source: https://docs.venlyfinance.com/guides/finance/onboarding/sumsub-token-sharing
Forward a verification you already ran in your own Sumsub account instead of re-verifying the user.
If you already run KYC in your own Sumsub account, you can forward that verification to Venly instead
of putting the user through a second flow. You pass a Sumsub **share token** when creating the account,
and Venly redeems it against your verification tenant.
## Three conditions, all required
This route is deliberately narrow. All of these must hold, or the request is rejected:
`partyType` must be `INDIVIDUAL`. KYC only — there is no KYB equivalent.
Your company's wallet type must be `SELF_CUSTODY`.
Verification must be provisioned for your company.
| If this fails | You get |
| - | - |
| `partyType` is not `INDIVIDUAL` | `400 verification-kyc-only` |
| Company wallet type is not `SELF_CUSTODY` | `400 verification-self-custody-only` |
| No provisioned verification tenant | `400 verification-not-provisioned` |
If any of these rule you out, use a [hosted verification link](/guides/finance/onboarding/hosted-verification)
instead — it has none of these constraints.
## Only at account creation
The token is accepted **only** on the inline party of
[`POST /accounts`](/api-reference/Finance-API/accounts/create-a-new-account):
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/accounts \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user-12345",
"chain": "BASE",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"party": {
"partyType": "INDIVIDUAL",
"firstName": "Jane",
"lastName": "Doe",
"sumsubToken": "_act-sbx-jwt-eyJhbGci..."
}
}'
```
Note the shape: it goes on the **inline `party` object**, not at the top level, and you must create the
party inline. There's no way to attach a token to a party you created earlier, and no way to attach one
to an existing account.
Sending it to [`POST /parties`](/api-reference/Finance-API/parties/create-a-new-party) instead is
rejected with `400 sumsub-token-not-supported`.
The field is **write-only**: never persisted, never returned in any response.
## The whole request rolls back on failure
The token is forwarded inside the **same transaction** that creates the party, account, and wallet. If
the forward fails, nothing is persisted — no party, no account, no wallet.
That's deliberate, and it makes your retry logic simple:
**On failure, get a fresh token and resubmit the entire request.** You never have to check what was
partially created, reconcile a half-built account, or clean up an orphaned party.
## Errors
| Status | Code | Retryable | Meaning |
| - | - | - | - |
| `400` | `SUMSUB_TOKEN_NOT_REDEEMABLE` | **Yes, with a fresh token** | The token is expired or already used. |
| `400` | `verification-kyc-only` | No | `partyType` is not `INDIVIDUAL`. |
| `400` | `verification-self-custody-only` | No | Company wallet type is not `SELF_CUSTODY`. |
| `400` | `verification-not-provisioned` | No | Verification not provisioned for your company. |
| `500` | `identity-verification-rejected` | No | The platform rejected the token. Contact support. |
| `503` | `identity-verification-unavailable` | **Yes** | Platform unreachable. Retry with a fresh token. |
Every one of these rolls the transaction back, so a retry is always a clean, full resubmit.
## Mint the token late
Share tokens are **single-use and short-lived**. Mint the token immediately before the
account-creation call rather than caching it — a token minted earlier in a user session has likely
expired by the time you use it, and that surfaces as `SUMSUB_TOKEN_NOT_REDEEMABLE`.
This is the most common failure with this route. If you're seeing it intermittently, look at how long
your token sits between minting and use, not at the Venly call.
Because retries need a *fresh* token, the resilient shape is: mint → submit → on failure, mint again →
submit again. Don't retry the same body.
## After it succeeds
The account is created and the verification is registered against your tenant. From there it behaves
exactly like the hosted route: the verdict is applied asynchronously to the party's `kycStatus`, and
you observe it via [Get a party](/api-reference/Finance-API/parties/get-party-details) or a
[webhook](/guides/finance/webhooks).
Forwarding a token is not itself a verdict. A successful `201` means the verification was accepted for
processing, not that the party is `VERIFIED`.
## When the customer still has to finish
A forwarded verification isn't always complete. If the applicant never finished their checks in your
Sumsub flow, the verification waits on them — and without action it would stay pending indefinitely.
The `201` tells you straight away. It carries a `verification` block with what's missing and a link to
hand to your end customer:
```json theme={null}
{
"success": true,
"result": {
"id": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
"externalId": "user-12345",
"kycStatus": "VERIFICATION_PENDING",
"status": "ACTIVE",
"verification": {
"requiredAction": "COMPLETE_SUMSUB_VERIFICATION",
"completionUrl": "https://in.sumsub.com/websdk/p/sbx_uni_2kq7c9ZxT4",
"completionUrlExpiresAt": "2026-01-15T10:30:00Z"
}
}
}
```
Send the customer to `completionUrl`. It's short-lived and embeds an access token, so treat it as a
credential. When no action is required, the block is absent.
The `verification` block appears **only on this create response**. It's never stored, never returned by
a later read, and absent when the request replays an earlier submission. Don't rely on getting it a
second time — use the endpoints below to follow up.
To follow up later — the link expired, or the customer came back days afterwards:
[`GET /parties/{partyId}/kyc-status`](/api-reference/Finance-API/parties/get-a-partys-kyc-status)
returns the `verificationState`, the `requiredAction` and the outcome of each step. `AWAITING_CUSTOMER`
means it's waiting on your end customer; `IN_REVIEW` means they're done and it's with the reviewer.
[`POST /parties/{partyId}/kyc-status/completion-link`](/api-reference/Finance-API/parties/issue-a-kyc-completion-link)
issues a new short-lived link. Call it each time you need one. A verification that isn't waiting on
the customer answers `409 kyc-not-completable`.
The verdict still lands on the party's `kycStatus` as usual. The KYC status endpoint shows the progress
towards it.
## Next steps
The route with no eligibility constraints.
See what a stalled verification is waiting on.
Tracking linkage and resolving failures.
# Troubleshooting verification
Source: https://docs.venlyfinance.com/guides/finance/onboarding/troubleshooting
Track linkage separately from the verdict, and resolve every verification error code.
Verification spans your API call, Venly, an external platform, and a human reviewer. When something
stalls, the first job is working out *where*. That's what the linkage endpoint is for.
## Linkage is not the verdict
Two different questions, two different endpoints. Mixing them up is the most common mistake here.
| Question | Where to look |
| - | - |
| **"Is this party verified?"** | `kycStatus` / `kybStatus` on [Get a party](/api-reference/Finance-API/parties/get-party-details) |
| **"What's happening with the verification plumbing?"** | [Get verification linkage](/api-reference/Finance-API/parties/get-verification-linkage) |
| **"Is it waiting on my end customer, and for what?"** | [Get a party's KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status) |
[`GET /parties/{partyId}/iv-verification`](/api-reference/Finance-API/parties/get-verification-linkage)
reports how far the party got through the pipeline:
| `status` | Meaning |
| - | - |
| `NOT_LINKED` | No verification has been started for this party. |
| `SUBMITTED` | A verification request was submitted. |
| `FORWARDED` | Forwarded for verification. |
| `ACCEPTED` | The platform accepted the case. |
| `COMPLETED` | A verdict was received and applied. |
| `FAILED` | The linkage failed, or the verdict was a decline. |
`COMPLETED` does **not** mean approved. It means a verdict landed. Read the party's own
`kycStatus`/`kybStatus` for the outcome — a declined verification also reaches `COMPLETED` or `FAILED`
depending on where it ended.
This endpoint always returns `200` for an existing party. It never errors to signal a verification
outcome, and it reflects **party-level** linkage only — never your company's tenant provisioning state.
### `ivCaseReference` is durable
Once the platform returns a case reference, it is populated and **never cleared** — including on
`FAILED` after a decline. It's `null` only while no reference was ever returned.
Read the field directly rather than gating on `status`:
```js theme={null}
// Good — the reference is there whenever it exists
const { ivCaseReference } = linkage;
if (ivCaseReference) attachToSupportTicket(ivCaseReference);
// Fragile — misses FAILED cases that do have a reference
if (linkage.status === 'COMPLETED') { /* ... */ }
```
That durability is the point: **quote `ivCaseReference` to Venly support** when investigating a stuck
or disputed verification. It survives declines specifically so a rejected case can still be traced.
## Diagnosing a stall
| Symptom | Likely cause | Next step |
| - | - | - |
| `status: NOT_LINKED` long after you minted a link | The user never opened the URL | Re-send it — [minting is idempotent](/guides/finance/onboarding/hosted-verification#links-do-not-expire) |
| Stuck at `SUBMITTED` or `FORWARDED` | In flight on the platform side | Wait; if it persists, quote `ivCaseReference` to support |
| `ACCEPTED` for a long time | Awaiting human review — normal for KYB, **or** waiting on the end customer | Read the [KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status). `IN_REVIEW`: wait, and [use a webhook](/guides/finance/webhooks) instead of polling. `AWAITING_CUSTOMER`: [issue a completion link](/api-reference/Finance-API/parties/issue-a-kyc-completion-link) |
| `COMPLETED` but account still blocked | Verdict was a decline, **or** the wallet gate isn't clear | Read the party's status; then check [wallet readiness](/guides/finance/kyc-verification#wallet-readiness-aml-and-permits-for-self-custody) |
| `FAILED` with a `null` reference | Never reached the platform | Retry; if it repeats, it's configuration — contact Venly |
| Party `VERIFIED` but transfers still rejected | You're reading the party, not the account | Read the **account's** `kycStatus` |
That last row catches people regularly. The party and the account each carry their own status, and only
the **account's** `kycStatus` gates money movement. See
[Roles & entities](/guides/finance/onboarding/roles-and-entities#account).
## Every verification error code
### Provisioning and eligibility
These are configuration, not transient. Retrying will not help.
| Status | Code | Meaning |
| - | - | - |
| `409` / `400` | `verification-not-provisioned` | Your company has no provisioned verification tenant. Contact Venly. |
| `409` | `party-already-verified` | The party is already `VERIFIED`. Nothing to do. |
| `404` | `kyc-status-not-found` | The party has no verification on record, so there's no status to read or complete. Start one first. |
| `409` | `kyc-not-completable` | A completion link was requested for a verification that is already decided, isn't waiting on the customer, or wasn't started from a Sumsub token. Read the [KYC status](/api-reference/Finance-API/parties/get-a-partys-kyc-status). |
| `400` | `verification-kyc-only` | `party.sumsubToken` supplied but `partyType` is not `INDIVIDUAL`. |
| `400` | `verification-self-custody-only` | `party.sumsubToken` supplied but your wallet type is not `SELF_CUSTODY`. |
| `400` | `sumsub-token-not-supported` | `sumsubToken` sent to `POST /parties`. It only works on `POST /accounts`. |
| `400` | `kyc-status-not-settable` | `kycStatus` `VERIFIED` or `REJECTED` supplied at account creation. `NOT_REQUIRED` is the only settable value. |
| `400` | `kyc-not-required-not-allowed` | `kycStatus: NOT_REQUIRED` supplied but no Venly admin has enabled the tenant-managed KYC flag on your company tenant. You can't retry past this — either ask Venly to enable it, or verify via a [hosted link](/guides/finance/onboarding/hosted-verification) or [Sumsub token sharing](/guides/finance/onboarding/sumsub-token-sharing). |
### Tokens
| Status | Code | Retryable | Meaning |
| - | - | - | - |
| `400` | `SUMSUB_TOKEN_NOT_REDEEMABLE` | **Yes, with a fresh token** | Expired or already used. The whole account-creation transaction rolled back. |
See [mint the token late](/guides/finance/onboarding/sumsub-token-sharing#mint-the-token-late) — this is
almost always a token-age problem.
### Platform
| Status | Code | Retryable | Meaning |
| - | - | - | - |
| `500` | `identity-verification-rejected` | No | The platform rejected the request itself. Contact support. |
| `503` | `identity-verification-unavailable` | **Yes** | Platform unreachable. Retry with exponential backoff. |
The distinction matters: `500` means *your request was understood and refused*, `503` means *nobody
answered*. Only retry the second.
### Downstream provisioning
Bank-rail provisioning onboards the account holder for verification, so it surfaces
verification state through its own codes:
| Status | Code | Retryable | Meaning |
| - | - | - | - |
| `409` | `partner-onboarding-pending` | **Yes** | The account holder's verification is still pending. Retry once approved. |
| `422` | `partner-onboarding-rejected` | No | Verification was rejected or revoked. Provisioning cannot proceed. |
| `400` | `account-holder-unresolved` | No | The account doesn't resolve to exactly one active account-holder party. |
| `400` | `party-required` / `party-name-required` | No | No linked account-holder party, or that party has no legal name. |
`partner-onboarding-pending` vs `partner-onboarding-rejected` is the retryable/terminal split
for [virtual bank accounts](/guides/finance/virtual-bank-accounts#verification-gates) — the two look
similar but only the first is worth a retry.
### Pay-out recipient gate
Not verification errors as such, but they're what a cleared KYC/KYB still runs into:
| Status | Code | Meaning |
| - | - | - |
| `422` | `recipient-not-authorized` | No `PAYOUT_RECIPIENT` role on this account, **or** the party's KYC/KYB isn't cleared. |
| `422` | `recipient-role-inactive` | The role exists but is `INACTIVE`. |
A verified party is not automatically a permitted pay-out recipient — see
[the recipient gate](/guides/finance/onboarding/roles-and-entities#the-recipient-gate).
## Retry policy at a glance
| Do retry | Don't retry |
| - | - |
| `503 identity-verification-unavailable` | `500 identity-verification-rejected` |
| `409 partner-onboarding-pending` (once approved) | `422 partner-onboarding-rejected` |
| `400 SUMSUB_TOKEN_NOT_REDEEMABLE` (with a **fresh** token) | Anything `*-not-provisioned` or `*-only` |
## Next steps
The three gates and how they compose.
The API-wide error reference.
# Pay-ins
Source: https://docs.venlyfinance.com/guides/finance/pay-ins
The pay-in ledger — every fiat credit that arrived, what it converted to, and what reached the account wallet — and how it relates to virtual bank accounts, pay-in sessions and webhooks.
Fiat reaches an account in two ways: a bank transfer to the account's
[virtual bank account](/guides/finance/virtual-bank-accounts), or a hosted
[pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session)
the payer completes online (**coming soon** — not yet available, so a virtual bank account is the way in today). Either way, what happens next is the same: the fiat is converted and
crypto is credited to the account wallet. That settlement is a **pay-in**, and since 1.8.0 you can read
it.
## Three objects, one deposit
| Object | Answers | Read it at |
| - | - | - |
| **Virtual bank account** | Where can fiat be sent? | [List / get virtual bank accounts](/api-reference/Finance-API/virtual-bank-accounts/list-virtual-bank-accounts) |
| **Pay-in session** | Was the hosted payment initiated and paid? | [List / get pay-in sessions](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/list-pay-in-sessions) |
| **Pay-in** | Did the money settle, into what, and how much arrived? | [List / get pay-ins](/api-reference/Finance-API/pay-ins/list-pay-ins) |
A bank transfer to a virtual bank account produces a pay-in and no session. A hosted payment produces a
session **and** a pay-in — the session tracks initiation, the pay-in tracks settlement.
**A pay-in session never reaches a "completed" status.** On the happy path it stops at
`PAYMENT_RECEIVED`. The statuses `CONVERTING`, `MINTING` and `COMPLETED` were removed in 1.8.0 — if
you poll a session waiting for one of them, you will wait forever. Settlement lives on the pay-in, and
`PAY_IN_SETTLED` is the event that announces it.
## The pay-in record
```bash theme={null}
curl https://api-staging.venlyfinance.com/v1/pay-ins/{payInId} \
-H "Authorization: Bearer {access_token}"
```
```json Response (200) theme={null}
{
"success": true,
"result": {
"id": "0c1d2e3f-4a5b-4c6d-8e7f-90a1b2c3d4e5",
"accountId": "b2a1f0e9-8c7d-4e3a-9f21-0a1b2c3d4e5f",
"virtualBankAccountId": "4d5e6f70-8192-4a3b-9c4d-5e6f7081920a",
"accountWalletId": "9f8e7d6c-5b4a-4938-8271-6a5b4c3d2e1f",
"status": "SETTLED",
"chain": "BASE",
"cryptoAsset": "USDC",
"fiatCurrency": "EUR",
"grossFiatAmount": "100.00",
"grossCryptoAmount": "108.42",
"netCryptoAmount": "108.42",
"transactionHash": "0x4f7a2c9e18b3d05a6c7e94f120d8b3a5e6f1c07d92b4a8e35f6c1d0b7a92e438",
"settledAt": "2026-08-19T09:14:52Z",
"createdAt": "2026-08-19T09:02:10Z",
"updatedAt": "2026-08-19T09:14:52Z"
}
}
```
| Field | Why it matters |
| - | - |
| `id` | The same `payInId` the `PAY_IN_SETTLED` / `PAY_IN_FAILED` webhooks carry — the join key. |
| `netCryptoAmount` | **What reached the account wallet.** Reconcile against this. |
| `grossCryptoAmount` | The deposit as received. Today always equal to `netCryptoAmount`. |
| `grossFiatAmount` | The fiat received. Informational, and absent for some deposit types. |
| `transactionHash` | The on-chain credit, once broadcast. |
| `accountId` | Absent while the deposit is not yet attributed to an account. Historical records keep it even after the account or virtual bank account is closed. |
Amounts are decimal strings — never floats.
## Lifecycle
```mermaid theme={null}
stateDiagram-v2
[*] --> RECEIVED: fiat credit observed
RECEIVED --> SETTLING
SETTLING --> SETTLED
SETTLING --> FAILED
RECEIVED --> RETURNED
```
| `status` | Meaning | Event |
| - | - | - |
| `RECEIVED` | The fiat credit was observed. | — |
| `SETTLING` | Conversion and the on-chain credit are in progress. | — |
| `SETTLED` | The crypto is in the account wallet. | `PAY_IN_SETTLED` |
| `FAILED` | The deposit ended without crediting the account. No `PAY_IN_SETTLED` will follow. | `PAY_IN_FAILED` |
| `RETURNED` | The fiat was sent back to the payer. | — |
Build on the webhooks and use the reads for reconciliation and recovery — listing the ledger with
`status=RECEIVED` or `SETTLING` older than your expected settlement time is a cheap stuck-deposit
check.
## Listing the ledger
[`GET /pay-ins`](/api-reference/Finance-API/pay-ins/list-pay-ins) spans every account in your company.
All filters combine with AND:
| Filter | Use |
| - | - |
| `accountId` | One account's deposits |
| `virtualBankAccountId` | Everything that arrived on one set of bank details — including after you closed it |
| `status` | Stuck or failed deposits |
| `from` / `to` | A reconciliation window on `createdAt` — inclusive / exclusive |
Sorted by `createdAt` descending; `sortOn` accepts only `createdAt`.
## Reading sessions back
Before 1.8.0 a pay-in session could not be read after creation. Now
[List pay-in sessions](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/list-pay-in-sessions)
and [Get a pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/get-a-pay-in-session)
return the persisted state — never a live provider call — so you can render a deposits view or recover
a session whose `callbackUrl` notification was missed.
To poll for changes, sort `updatedAt` descending and read **only the first page**, sized above your
expected change volume. `from`/`to` filter on `createdAt`, so there is no "changed since" filter, and a
session that changes between your page-1 and page-2 requests shifts position and is skipped.
## Errors worth knowing on session creation
1.8.0 added preconditions to
[creating a hosted pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session)
when the account settles through a EUR virtual bank account:
| HTTP | `code` | Meaning |
| - | - | - |
| `409` | `vba-not-ready` | The account's EUR virtual bank account is still being provisioned. **Retryable.** |
| `422` | `vba-required` | The account needs an active EUR virtual bank account first — [create one](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account). |
| `422` | `ambiguous-eur-vba` | More than one active EUR virtual bank account; [close](/api-reference/Finance-API/virtual-bank-accounts/close-a-virtual-bank-account) the surplus. |
| `422` | `vba-asset-mismatch` | The virtual bank account settles into a different asset than the session's `outCryptocurrency`. |
| `422` | `unsupported-currency` | Hosted pay-ins are currently EUR only. |
## Next steps
Issuing — and now closing — the bank details pay-ins arrive on.
`PAY_IN_SETTLED` and `PAY_IN_FAILED`, and how to debug a delivery you didn't get.
Where `netCryptoAmount` lands.
# Pay-outs
Source: https://docs.venlyfinance.com/guides/finance/payouts
Send crypto from an account wallet and have a bank beneficiary receive fiat.
A pay-out moves value the opposite way from a [pay-in](/guides/finance/virtual-bank-accounts): crypto leaves an account wallet, and a beneficiary receives fiat in their bank account.
Getting there takes three objects, created once and reused for every subsequent pay-out:
```mermaid theme={null}
flowchart LR
A["Payout bank account
whose bank account"] --> C["Payout route
bank account + crypto asset"]
B["Deposit asset
which crypto, which chain"] --> C
C --> D["Payout
how much, when"]
```
| Object | Scope | Answers |
| - | - | - |
| **Payout bank account** | Party | Whose bank account, on which rail |
| **Payout route** | Account | Which bank account pairs with which crypto asset |
| **Payout** | Route | How much, and when |
The route is where the durable configuration lives. It fixes the beneficiary, the rail, the crypto asset and the fiat currency — so an actual pay-out request carries almost nothing: a route, an amount, and an idempotency key.
## Setup
Before any of this, the recipient party's **partner-terms consent** must be complete. A verified party
whose end customer has not yet accepted our partners' terms cannot receive a pay-out — present the
party's consent link to them first. See
[Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
Before anything else, the party who owns the destination bank account needs an `ACTIVE`
[`PAYOUT_RECIPIENT` role](/api-reference/Finance-API/accounts/add-a-party-to-an-account) on the
paying account, with cleared KYC/KYB.
A party can hold `PAYOUT_RECIPIENT` **and** `ACCOUNT_HOLDER` on the same account — that's the
self-payout case, where a user withdraws to their own bank account.
This is the "recipient gate". It's checked when you create a route **and again** when you request each pay-out, so revoking the role stops future pay-outs immediately.
[`POST /parties/{partyId}/payout-bank-accounts`](/api-reference/Finance-API/payout-bank-accounts/register-a-payout-bank-account)
Two rails are available. The `rail` decides the `fiatCurrency` it requires and the shape of
`railDetails`:
```json US_ACH (USD) theme={null}
{
"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"
},
"beneficiaryEmail": "jane@example.com",
"beneficiaryPhoneNumber": "+12125550123"
}
```
```json SEPA (EUR) theme={null}
{
"rail": "SEPA",
"fiatCurrency": "EUR",
"label": "Jane's current account",
"accountHolderName": "Jane Doe",
"railDetails": {
"iban": "DE89370400440532013000",
"bic": "DEUTDEDB"
},
"bankName": "Example Bank",
"bankAddress": {
"street1": "Taunusanlage 12",
"city": "Frankfurt am Main",
"region": "HE",
"postalCode": "60325",
"country": "DE"
},
"beneficiaryEmail": "jane@example.com",
"beneficiaryPhoneNumber": "+4915112345678"
}
```
This is the **only** endpoint that accepts raw bank credentials. The sensitive one — the account
number on ACH, the IBAN on SEPA — is encrypted at rest immediately and never returned: every read
path exposes only `accountNumberLast4` or `ibanLast4`. Routing numbers and BICs are public bank
identifiers and come back in full. On SEPA the `bic` is optional; the IBAN is validated for format,
check digit and SEPA-scheme membership (`400 invalid-iban`).
`bankAddress` is the **beneficiary bank's** address, not the recipient's home address. All five components are required and `country` must be ISO 3166-1 alpha-2, or you get `400 invalid-bank-address`.
**Supply `beneficiaryPhoneNumber` on every bank account.** It's forwarded as part of the Travel-Rule
record, and some destinations refuse a beneficiary without it — a USD route fails terminally with
`beneficiary-data-incomplete`. A bank account cannot be edited after registration, so a missing
phone means registering the beneficiary again.
`beneficiaryEmail` is optional and stays your own record: it's never sent to the payment provider.
Where a provider needs an address, it gets a Venly-controlled one, so the provider's operational and
pricing mail never reaches your beneficiary.
The account starts `PENDING` and becomes `ACTIVE` once activated for pay-outs.
[`POST /accounts/{accountId}/payout-routes`](/api-reference/Finance-API/payout-routes/create-a-payout-route)
```json theme={null}
{
"payoutBankAccountId": "a1b2c3d4-e5f6-4708-9a1b-2c3d4e5f6071",
"depositAsset": { "chain": "BASE", "name": "USDC" }
}
```
You never name a provider — Venly resolves one internally by intersecting provider capability with
your tenant's enablement, and provider identity is never exposed in any response.
A route is created `PENDING` and moves `REGISTERING → ACTIVE` as registration completes, or
`REJECTED` on terminal failure.
It can also stop at **`AWAITING_OWNERSHIP_PROOF`**, which is the one status that needs something
from you — see [Proving you control the source wallet](#proving-you-control-the-source-wallet).
**Only `ACTIVE` routes are usable.** Requesting a pay-out earlier returns `422 route-not-active`.
Each status that matters raises a [webhook](/guides/finance/webhooks#event-catalogue):
`PAYOUT_ROUTE_ACTIVATED`, `PAYOUT_ROUTE_AWAITING_OWNERSHIP_PROOF` and `PAYOUT_ROUTE_REJECTED`, the
last with a `reason`. Listen for those instead of polling. Delivery is best-effort, though, so
still reconcile routes waiting on a proof with
[List payout routes](/api-reference/Finance-API/payout-routes/list-payout-routes) now and then.
## Proving you control the source wallet
A route can pause at `AWAITING_OWNERSHIP_PROOF`. That happens when the destination requires a Travel-Rule proof that you control the wallet the funds leave from, **and** that wallet is self-custody. Venly holds no key for it, so only its owner can sign.
Registration doesn't fail — it waits. Nothing else moves until you complete this step.
[`POST /accounts/{accountId}/payout-routes/{routeId}/ownership-proof/prepare`](/api-reference/Finance-API/payout-routes/prepare-a-route-ownership-proof) — no body needed.
Nothing is stored and there's no expiry token, so this is safe to repeat.
Sign `message` **exactly as returned**. Don't reformat, re-encode, pretty-print or reconstruct it — the signature is checked against those exact bytes, and wrapping extra text around it is rejected even when the signature itself is valid.
Only an externally-owned account can sign: the scheme is EIP-191, and **ERC-1271 contract signatures are not supported**. Self-custody wallets are [single-key wallets only](/guides/finance/managed-vs-self-custody#single-key-wallets-only) for now.
[`POST .../ownership-proof/complete`](/api-reference/Finance-API/payout-routes/submit-a-route-ownership-proof) with `message` and `signature`. The route leaves `AWAITING_OWNERSHIP_PROOF` and registration resumes.
A `200` from *complete* means the proof was accepted, not that the route is ready. It continues to `ACTIVE` asynchronously — poll the route or [register a webhook](/guides/finance/webhooks). Only an `ACTIVE` route has a `depositAddress`.
The message is date-bound, so sign and submit in the same session rather than preparing it one day and submitting the next.
| Code | Meaning |
| - | - |
| `ownership-proof-not-applicable` | This route needs no owner-signed proof, or its source wallet is Venly-managed. |
| `invalid-proof-message` | The message isn't the canonical text for this route. |
| `signature-mismatch` | The signature doesn't recover the route's wallet address. |
## Requesting a pay-out (PULL)
With an `ACTIVE` route, a pay-out is a small request:
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/accounts/{accountId}/payouts \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"payoutRouteId": "b2c3d4e5-f607-4819-a2b3-c4d5e6f70819",
"cryptoAmount": 100.00,
"idempotencyKey": "b7c1e084-3f2a-4d19-9c60-5ae8127b04f3"
}'
```
Venly sends crypto from the account wallet to the provider's deposit address using the account's
up-front [permit](/guides/finance/permits-and-allowances), and the provider pays fiat to the
beneficiary. This is **PULL** funding — `fundingMode: PULL`.
### A `201` is not success
This is the single most important thing to get right in a pay-out integration.
Permit allowance and account-wallet balance are deliberately **not** pre-checked. If either is
insufficient, the request still returns `201` with `status: REQUESTED`, and the asynchronous send then
fails terminally — the pay-out lands in `FAILED` with a `failureReason`.
So don't treat the `201` as "money sent". Track the real outcome by
[registering a webhook](/guides/finance/webhooks) or polling
[Get a payout](/api-reference/Finance-API/payouts/get-a-payout).
## The pay-out lifecycle
```mermaid theme={null}
stateDiagram-v2
[*] --> REQUESTED: PULL via API
REQUESTED --> SENDING
SENDING --> PROVIDER_PROCESSING
[*] --> PROVIDER_PROCESSING: PUSH from observed deposit
PROVIDER_PROCESSING --> COMPLETED
COMPLETED --> RETURNED: fiat leg bounced
REQUESTED --> REJECTED
SENDING --> FAILED
```
| Status | Meaning |
| - | - |
| `REQUESTED` | Created via the API; awaiting dispatch of the send leg. PULL only. |
| `SENDING` | The managed on-chain send is in flight. PULL only. |
| `PROVIDER_PROCESSING` | Funds reached the provider's deposit address; awaiting the fiat pay-out. |
| `COMPLETED` | The provider reported the fiat pay-out as completed; `settledFiatAmount` is set. |
| `REJECTED` | Validation failed before dispatch. PULL only. Terminal. |
| `FAILED` | The managed send leg failed terminally. PULL only. Terminal. |
| `RETURNED` | The provider reported the fiat leg as bounced or returned. Terminal. |
`COMPLETED` is not the end of the story. A fiat leg can still bounce afterwards — a closed account, a
name mismatch at the receiving bank — moving the pay-out to `RETURNED`. If you release goods or credit
a ledger on `COMPLETED`, handle `RETURNED` as a reversal.
`settledFiatAmount` is the fiat the provider actually paid, and is null until `COMPLETED`. Expect it
to be lower than the crypto you sent, net of fees — reconcile against this field rather than assuming
a 1:1 conversion.
### Why it didn't arrive
`failureReason` is populated on `REJECTED`, `FAILED` and `RETURNED`, from a fixed vocabulary. No
provider is named and no compliance decision is explained:
| `failureReason` | Meaning | Accompanies |
| - | - | - |
| `REJECTED_BY_PROVIDER` | The banking partner refused the pay-out. Deliberately non-specific. | `REJECTED` |
| `ROUTE_UNAVAILABLE` | The destination could not be used — e.g. a route deactivated before dispatch. | `REJECTED` |
| `SEND_FAILED` | Execution had begun and the crypto send could not be completed; no money reached the beneficiary. | `FAILED` |
| `RETURNED_BY_BANK` | The receiving bank sent the payment back. A completion may or may not have preceded it. | `RETURNED` |
| `UNKNOWN` | The cause is not one this contract recognises. | any |
Treat it as an open enum — handle an unrecognised value as `UNKNOWN` rather than failing.
## PUSH pay-outs
Not every pay-out starts with an API call. On a **self-custody** account, a customer can send crypto
to the route's `depositAddress` themselves. Venly observes that deposit and creates the pay-out for
you, with `fundingMode: PUSH`.
| | PULL | PUSH |
| - | - | - |
| Created by | Your `POST /payouts` call | Venly, from an observed deposit |
| Funded by | Venly, from the account wallet via the permit | The customer, directly |
| Custody | Any | Self-custody only |
| Starts at | `REQUESTED` | `PROVIDER_PROCESSING` |
`depositAddress` is exposed **only** for an `ACTIVE` route on a `SELF_CUSTODY` account — read it from
[List payout routes](/api-reference/Finance-API/payout-routes/list-payout-routes) and show it to your
customer.
Both kinds appear in the same read endpoints, so
[List payouts](/api-reference/Finance-API/payouts/list-payouts) gives you the complete picture.
Filter on `fundingMode` if you need to treat them differently.
## Idempotency
`idempotencyKey` is required and must be unique **per company across all idempotent endpoints** — not
just across pay-outs. The full replay behaviour is in
[Idempotency](/guides/finance/idempotency#what-a-replay-actually-returns).
The one that catches people here: a pay-out that **failed** has spent its key. Retrying with the same key
returns `422`, not a fresh attempt — generate a new key. Because pay-outs fail asynchronously, this often
surfaces well after the original `201`.
## Common errors
| Status | Code | Cause |
| - | - | - |
| `400` | `account-not-active` | The paying account is not `ACTIVE`. |
| `422` | `route-not-active` | The route hasn't reached `ACTIVE` yet, or was `REJECTED`. |
| `422` | `route-unresolved` | The route doesn't belong to this account. |
| `422` | `recipient-not-authorized` | No `PAYOUT_RECIPIENT` role, or the party's KYC/KYB isn't cleared. |
| `422` | `recipient-role-inactive` | The role exists but is `INACTIVE`. |
| `422` | `unsupported-asset` | The route's lane is no longer enabled for your tenant. |
| `409` | `route-already-exists` | A route already exists for this account, bank account and deposit asset. |
| `422` | `unsupported-combination` | No single provider resolves for that bank account and deposit asset. |
## Next steps
Get pay-out status changes pushed to you instead of polling.
The permit that makes PULL pay-outs possible on self-custody wallets.
The pay-in direction: fiat in, crypto out.
Safe retries across every write endpoint.
# Approving transfers without gas
Source: https://docs.venlyfinance.com/guides/finance/permits-and-allowances
How self-custody accounts let Venly move their tokens — the gasless permit and the allowance it grants.
Applies to **self-custody** accounts. (New to that term? See [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody).) Do this **after** the account is [verified](/guides/finance/kyc-verification) and its wallet's AML check is approved.
## Why an allowance is needed
A self-custody account holder controls their own wallet. For Venly to move that wallet's tokens — for an [account-to-account transfer](/guides/finance/transfers) or a [PULL pay-out](/guides/finance/payouts) — the wallet must grant Venly's **orchestration wallet** an ERC-20 **allowance** — permission to pull funds via `transferFrom`. Which assets your tenant can settle in is configuration, not a fixed list — read [`GET /supported-assets`](/api-reference/Finance-API/supported-assets/list-supported-assets) rather than hard-coding one.
Gasless permits are a property of the **token contract on a specific chain**, not of the symbol. The same symbol can support permits on one chain and not another. Check with your Venly contact before committing to an asset that isn't already enabled for you.
Only the **account wallet** needs a permit, and on a self-custody account only the holder can sign it.
On Venly-managed accounts Venly grants the allowance itself.
## Why a permit instead of a plain `approve`
A normal `approve()` costs gas and needs the wallet to hold the chain's native coin. Instead, the holder **signs** an EIP-2612 message (off-chain, no gas) and Venly's orchestration wallet submits it on-chain and pays the gas.
A permit needs a wallet controlled by a single key. **Smart-contract wallets** — a Safe, for example — can't produce that signature and are [not supported for now](/guides/finance/managed-vs-self-custody#single-key-wallets-only).
## The permit flow (self-custody account wallet)
`GET /accounts/{accountId}/wallets/{walletId}/permits` returns, per asset, a `supportedAssetId` and an EIP-712 `typedData` object. `walletId` sets the chain (e.g. Base vs Avalanche). The nonce and the token's domain `name`/`version` are read from the contract for you.
Sign `typedData` with the key for the wallet's **owner** address. **The signature must recover to the `owner`** — a signature from any other wallet makes the permit `FAILED`. Produces `v`, `r`, `s`.
`POST .../permits` with the `supportedAssetId` and the signature. Returns **HTTP 200 with `result.status`** — check the status, not just the code. Re-submitting an already-confirmed permit returns `409`.
Settlement is asynchronous — poll `GET .../permits` until `CONFIRMED` (or `FAILED`). On `CONFIRMED` the allowance is live and Venly can move the wallet's funds.
### Permit status lifecycle
```text theme={null}
PENDING ──submit──▶ SUBMITTED ──on-chain confirmed──▶ CONFIRMED
│ │
└────────── FAILED ◀─┘
```
| `status` | Meaning |
| - | - |
| `PENDING` | Not yet submitted |
| `SUBMITTED` | Signature accepted; settling on-chain |
| `CONFIRMED` | Confirmed on-chain — the allowance is live |
| `FAILED` | Execution/confirmation failed — most often the signature didn't recover to the wallet `owner`. Re-fetch the permit and sign again with the correct key. |
## Signing the message
Take the `typedData` from the permit response and sign it with the **owner's** key. With [ethers.js](https://docs.ethers.org):
```javascript theme={null}
import { ethers } from "ethers";
// `typedData` is result[].typedData from GET .../permits
const { domain, types, message } = typedData;
// ethers builds the domain separator itself, so remove EIP712Domain before signing
const { EIP712Domain, ...signTypes } = types;
const wallet = new ethers.Wallet(PRIVATE_KEY); // MUST be the wallet `owner` key
const signature = await wallet.signTypedData(domain, signTypes, message);
const { v, r, s } = ethers.Signature.from(signature); // submit these below
```
The signature **must recover to the wallet `owner`**. Signing with a different wallet (or signing the JSON as a personal message instead of EIP-712 typed data) produces a signature that the token contract rejects — the permit silently becomes `FAILED`.
Then submit it. Send `v`, `r`, and `s` as **strings** in the `signature` body — `ethers` returns `v` as a number, so convert it (`String(v)`). Use the `supportedAssetId` from the `GET .../permits` response:
```bash theme={null}
curl --request POST \
--url https://api.venlyfinance.com/v1/accounts/{accountId}/wallets/{walletId}/permits \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"supportedAssetId": "7c32877a-cd0c-422a-a0fd-5af0815fc280",
"signature": { "v": "27", "r": "0x...", "s": "0x..." }
}'
```
## Verifying the allowance
Before initiating a payment or transfer, confirm the allowance is in place:
```bash theme={null}
curl https://api.venlyfinance.com/v1/accounts/{accountId}/wallets/{walletId}/allowances \
-H "Authorization: Bearer "
```
Each entry returns the asset, the `orchestrationWallet` that holds the allowance, and the `allowance` as a **human-readable decimal** (on-chain raw value ÷ 10^`decimals`). The authoritative signal is the **permit `status: CONFIRMED`** (above) — not the allowance figure. An unlimited permit reports a very large `allowance` (uint256-max), but the value can also be finite; use this endpoint to read the current spending headroom.
Once a permit is `CONFIRMED`, the allowance persists until it's used up — the holder doesn't sign again for every transfer.
## Next steps
Confirm the allowance is live before moving funds.
Move funds once the permit is `CONFIRMED`.
# Recipe: issue wallets, receive fiat, pay out fiat
Source: https://docs.venlyfinance.com/guides/finance/recipes/wallet-api-end-to-end
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
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.
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.
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 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.
```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.
```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}`.
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.
```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.
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).
***
## 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.
```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"
}'
```
```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
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.
```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** is provisioned — your registered address as the account wallet. It is
AML-screened, and
`ACCOUNT_WALLETS_PROVISIONED` fires when they clear. See [Wallets & balances](/guides/finance/wallets).
* The account starts at `kycStatus: VERIFICATION_PENDING` because the party is new. **That is the gate
on money movement**, and the next two steps clear it. A second account for the same customer, opened
once they are verified, is created `VERIFIED` and skips step 4.
`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.
```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"
}
}
```
`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:
| 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
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.
```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`.
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.
`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).
***
## 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**.
```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.
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`.
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.
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.
```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"
}
]
}
}
```
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 partner-onboarding-pending` | Step 4 hasn't completed. Retry once verified. |
| `422 partner-onboarding-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
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.
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.
```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.
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).
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.
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"
}'
```
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.
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 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.
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.
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.
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.
`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.
***
## 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
Wallet API credentials, chain-name mapping and the full first-wallet flow.
Every field on the response, `depositRails`, and the US lane.
The full lifecycle including the API-initiated variant.
Registering, authenticating and testing your endpoint.
# Supported chains & assets
Source: https://docs.venlyfinance.com/guides/finance/supported-chains-and-assets
The blockchain networks, stablecoins, and fiat currencies the Finance API supports, and how they relate.
A quick reference for the networks and currencies you can use. These are the values the API accepts today; more are added over releases.
## Blockchain networks
An account is pinned to one network when it's created, and its wallet lives there.
| `chain` | Notes |
| - | - |
| `BASE` | Ethereum L2. |
| `AVALANCHE` | EVM C-Chain. |
| `ETHEREUM` | Ethereum mainnet. |
| `POLYGON` | Polygon PoS. |
| `SOLANA` | Non-EVM. Permits and allowances are EVM concepts and do not apply. |
These are the chains the **API** supports; the chains available to **your company** are configured by Venly. Creating an account on a chain your company isn't set up for returns `400 invalid-request` with a message listing your supported chains — for example, *"Chain 'POLYGON' is not supported by this company. Supported chains: \[AVALANCHE, BASE]"*.
On-ramp [virtual bank accounts](/guides/finance/virtual-bank-accounts) are provided through banking partners, and partner coverage rolls out per chain. Confirm availability for your chain with your Venly contact.
## Stablecoins
Crypto balances, transfers, and permits use these assets (the `asset` field):
| `asset` | Pegged to |
| - | - |
| `USDC` | USD |
| `EURC` | EUR |
| `USDT` | USD |
| `USDS` | USD |
### Ask the API instead of hard-coding
Which of these **your tenant** can settle in, and on which chains, is configuration. Read it at runtime:
`GET /supported-assets` — every chain/asset pair enabled for you, with `decimals` and `contractAddress`.
`GET /accounts/{accountId}/supported-assets` — the same list, plus whether *this account* can move each asset yet.
Use the first to build an asset picker and the second to decide what to actually offer an end-user. An asset the tenant has enabled is not automatically usable by every account — see [permits and allowances](/guides/finance/permits-and-allowances).
A symbol is not an asset. The same symbol on two chains is two different contracts with different capabilities, and support for gasless permits is a property of the contract on a given chain — not of the symbol. Never assume a symbol behaves the same everywhere, and read `contractAddress` from the API rather than hard-coding it.
### Naming an asset explicitly
Because a tenant can hold the same symbol on more than one chain, a fiat currency no longer always identifies a single asset. Where it doesn't, [fiat transfers](/guides/finance/transfers) need an explicit `chain` + `asset` pair.
| Your tenant's setup for a currency | What to send |
| - | - |
| One asset (e.g. USD → USDC on Base only) | `currency` alone is enough |
| More than one (e.g. USD → USDC on Base **and** Avalanche) | `currency` **plus** `chain` and `asset` |
Send `chain` and `asset` **together** — one without the other is rejected with `chain-and-asset-required-together`. Omitting both where the currency is ambiguous is rejected with `ambiguous-asset` rather than the API guessing a chain for you.
Sending `chain` + `asset` is always allowed, even when the currency is unambiguous. If you'd rather not branch on tenant configuration, just always send them.
## Fiat currencies
Fiat-denominated operations (fiat transfers, pay-in sessions) accept:
| `currency` |
| - |
| `EUR` |
| `USD` |
| `GBP` |
[Virtual bank accounts](/guides/finance/virtual-bank-accounts) support **EUR** (provisioning a `EUR_SEPA` IBAN) and **USD** (provisioning a `USD_ACH` account). The USD lane additionally requires your tenant to be onboarded for it with an approved KYB recording. `GBP` is accepted for other fiat-denominated operations but not yet for bank-account provisioning.
[Pay-outs](/guides/finance/payouts) settle over the `US_ACH` rail, which requires `USD`, or the `SEPA` rail, which requires `EUR`.
## How fiat converts to crypto
Fiat-denominated operations resolve to a stablecoin and settle on-chain. You name the target stablecoin — for example `targetCryptocurrency` on a [virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account), or the asset a [pay-in session](/api-reference/Finance-API/fiat-to-crypto-payment-sessions/create-a-fiat-to-crypto-payment-session) converts to. A [fiat transfer](/guides/finance/transfers) reports the conversion it used in its `fiatOrigin` block (currency, amount, exchange rate).
## Next steps
Per-asset balances on an account's wallet.
Move these assets between accounts.
# Transfers
Source: https://docs.venlyfinance.com/guides/finance/transfers
Move funds from one account to another — fiat-denominated or crypto-denominated — and track them to completion.
A transfer moves funds from one account to another. You always send to a **receiver account** — by its Venly `receiverAccountId`, or by your own `receiverExternalId` if you set one on the account. There are two endpoints, differing only in how you denominate the amount.
## Fiat vs crypto
| | [Fiat transfer](/api-reference/Finance-API/transfers/create-fiat-transfer) | [Crypto transfer](/api-reference/Finance-API/transfers/create-crypto-transfer) |
| - | - | - |
| You specify | a fiat `currency` + `amount` | a crypto `asset` + `chain` + `amount` |
| Settles in | the underlying stablecoin, on-chain | the named asset, on-chain |
| Response extra | a `fiatOrigin` block (currency, amount, exchange rate) | — |
Both settle on-chain and return a `transactionHash`.
## Prerequisites
The **sender** account must be [verified](/guides/finance/kyc-verification). For **self-custody** senders, the wallet also needs an allowance to the orchestration wallet — see [Approving transfers without gas](/guides/finance/permits-and-allowances).
**Both** the sender **and** the receiver account must be KYC `VERIFIED`. A transfer to an unverified receiver is rejected with `422 receiver-kyc-not-verified` — verify the receiver before sending. Transfers are also scoped to a single company; you cannot transfer to an account in another company.
## Create a fiat transfer
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/accounts/{senderAccountId}/transfers/fiat \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"receiverAccountId": "c3b2a1f0-9d8c-4e3a-bf21-1a2b3c4d5e60",
"currency": "USD",
"amount": 25.0,
"description": "Invoice settlement",
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
```
```json Response 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.0,
"fiatOrigin": { "currency": "USD", "amount": 25.0, "exchangeRate": 1 },
"status": "COMPLETED",
"transactionHash": "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
}
```
A **crypto transfer** is the same call against `/transfers/crypto`, sending `chain` + `asset` instead of `currency` (and the response omits `fiatOrigin`).
### Choosing which asset it settles in
A fiat amount has to become a specific token on a specific chain. While your tenant holds one asset for that currency, `currency` alone is enough and Venly picks it.
If your tenant holds the same currency on more than one chain — USDC on Base *and* Avalanche, both USD — that's ambiguous, so name the pair:
```json theme={null}
{
"receiverAccountId": "c3b2a1f0-9d8c-4e3a-bf21-1a2b3c4d5e60",
"currency": "USD",
"chain": "BASE",
"asset": "USDC",
"amount": 25.0,
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```
| Code | Cause |
| - | - |
| `ambiguous-asset` | The currency maps to more than one asset and you didn't name a pair. Venly refuses rather than guessing a chain. |
| `chain-and-asset-required-together` | You sent one of `chain`/`asset` without the other. |
| `asset-currency-mismatch` | The named asset isn't denominated in the `currency` you sent. |
| `unsupported-asset` | The named `chain` + `asset` pair isn't enabled for your tenant. |
Sending `chain` + `asset` is always accepted, even when the currency is unambiguous. If you'd rather not branch on tenant configuration, always send them. Read the valid pairs from [`GET /supported-assets`](/api-reference/Finance-API/supported-assets/list-supported-assets).
## Status lifecycle
A transfer's `status` is `PENDING → COMPLETED`, or `FAILED` (with an `errorMessage`). Settlement is on-chain, so poll [Get a transfer](/api-reference/Finance-API/transfers/get-transfer-details) — or [List transfers](/api-reference/Finance-API/transfers/list-transfers-for-account), filtering by `accountRole` (`SENDER`/`RECEIVER`) and `status`.
## Idempotency
Every transfer takes a unique `idempotencyKey`; retrying with the same key returns the original transfer instead of sending again. See [Safe retries with idempotency keys](/guides/finance/idempotency).
## Next steps
The endpoint reference and full schema.
Which chains and assets your tenant can settle in.
# Versioning & safe updates
Source: https://docs.venlyfinance.com/guides/finance/versioning
Use the version field so two edits never silently overwrite each other.
Every party and account carries a `version` number that goes up by one each time the resource changes. You use it to update safely.
## Why it matters
If two people — or two of your own processes — edit the same party at the same time, the second save could quietly wipe out the first. The `version` field prevents this: an update only succeeds if you're working from the latest copy.
## How to update
Fetch it and note the `version` (for example, `0`).
Include the `version` you just read in your `PATCH` request body.
If someone changed the resource after you read it, the API returns `409`. Re-fetch to get the new `version`, reapply your change, and send it again.
```bash theme={null}
curl --request PATCH \
--url https://api.venlyfinance.com/v1/parties/{partyId} \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{ "version": 0, "firstName": "Janet" }'
```
A stale `version` returns:
```json theme={null}
{
"success": false,
"errors": [
{ "code": "concurrent-modification", "message": "This request has been modified by another user. Please refresh and try again." }
]
}
```
Always send the `version` from your most recent read — never a cached or hard-coded value.
## API version
The Finance API is at **v1.10.0**, which adds a party's [KYC status and completion links](/api-reference/Finance-API/parties/get-a-partys-kyc-status) so you can follow up a verification that is waiting on your end customer. v1.9.0 added [pay-out route webhooks](/guides/finance/webhooks#event-catalogue), the `x-tenant-id` and `X-Correlation-Id` [request headers](/getting-started/conventions#request-headers), and [your company profile](/api-reference/Finance-API/company/get-your-company-profile). See the [changelog](/guides/finance/changelog) for every release.
## Next steps
Send the `version` with your PATCH request.
The other half of safe writes — duplicate-proof creates.
# Virtual bank accounts
Source: https://docs.venlyfinance.com/guides/finance/virtual-bank-accounts
Give an account its own bank details to receive fiat by SEPA or US rails, auto-converted to crypto.
A virtual bank account (VBA) gives an account its own bank coordinates to receive fiat. Incoming credits are automatically converted to a stablecoin and credited to the account's wallet.
## Prerequisites
The account must be [verified](/guides/finance/kyc-verification) and have a linked account-holder party.
## Lanes
The rail is resolved from the `inCurrency` you request and your tenant's enabled pay-in configuration — you don't pick a provider.
| Lane | `bankAccountType` | Returns | Notes |
| - | - | - | - |
| **EUR → SEPA** | `EUR_SEPA` | `iban` + `bic` | Deposit rails may populate **asynchronously** shortly after creation, so the account can come back `PENDING` first. |
| **USD → ACH** | `USD_ACH` | `accountNumber` + `routingNumber` | Returned synchronously. Requires your tenant to be onboarded for the USD lane with an approved KYB recording. Typically also reachable over WIRE, RTP and SWIFT. |
## Create a virtual bank account
Provide a display `name`, the fiat `inCurrency`, the `targetCryptocurrency` to convert to, and a unique `idempotencyKey`:
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "EUR Deposits",
"inCurrency": "EUR",
"targetCryptocurrency": "USDC",
"idempotencyKey": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'
```
```json Response theme={null}
{
"success": true,
"result": {
"id": "4d5e6f70-8192-4a3b-9c4d-5e6f7081920a",
"bankAccountType": "EUR_SEPA",
"status": "ACTIVE",
"currency": "EUR",
"targetCryptocurrency": "USDC",
"iban": "DE89370400440532013000",
"bic": "DEUTDEDB",
"bankName": "Example Bank",
"beneficiaryName": "Jane Doe",
"referenceCode": "VFY-7K2Q-931",
"depositRails": [
{
"railType": "SEPA",
"iban": "DE89370400440532013000",
"bic": "DEUTDEDB",
"bankName": "Example Bank",
"beneficiaryName": "Jane Doe",
"paymentReference": "VFY-7K2Q-931"
}
]
}
}
```
## The fields that matter
| Field | Why it matters |
| - | - |
| `depositRails` | **The complete instruction set per rail.** Read this to render payment instructions — it carries the bank and beneficiary postal addresses that wire and SWIFT senders require, which the summary fields don't. |
| `iban` / `bic` | Account coordinates for EUR rails. |
| `accountNumber` / `routingNumber` | Account coordinates for US rails. |
| `beneficiaryName` | The legal name on the account — display it next to the coordinates. |
| `referenceCode` | **Critical.** The payer must quote this in the payment reference so the incoming credit is matched to this account. Without it, settlement falls back to manual reconciliation. |
| `targetCryptocurrency` | The stablecoin incoming fiat is converted to. |
| `status` | `PENDING`, `ACTIVE` or `CLOSED`. |
**Anyone can pay in.** Deposits are accepted from any bank account, not only the account holder's own —
a company funding an end user, one person paying for another, or a partner settling on someone's behalf.
Always surface the `referenceCode` to the end user and tell them to include it in their transfer. It's how an incoming credit is routed to the right account.
### Summary fields vs. `depositRails`
The top-level `iban`/`bic`/`accountNumber`/`routingNumber` fields always describe the **primary** rail. `depositRails` lists every rail the account is reachable over.
A USD account typically exposes four rails sharing the same account and routing number — except `SWIFT`, which carries a BIC instead of a routing number:
| `railType` | Coordinates |
| - | - |
| `ACH` | `accountNumber` + `routingNumber` |
| `WIRE` | `accountNumber` + `routingNumber`, plus bank and beneficiary addresses |
| `RTP` | `accountNumber` + `routingNumber` |
| `SWIFT` | `accountNumber` + `bic` |
A EUR account exposes a single `SEPA` rail.
Build your payment-instruction UI from `depositRails` rather than the summary fields. If you only render `iban`/`accountNumber`, a customer sending by wire or SWIFT will be missing the bank address their bank asks for.
### `PENDING` accounts
On the EUR lane, an account can come back `status: PENDING` with `bankAccountType` omitted — the provider hasn't returned the deposit rails yet. Don't show payment instructions until it reaches `ACTIVE` and `depositRails` is populated. [Register a webhook](/guides/finance/webhooks) to hear about the transition instead of polling.
## Proving wallet ownership
On some setups, converted funds settle straight to the customer's own self-custody wallet. Where that applies, the customer must prove they control that wallet first — Venly can't sign for a wallet it doesn't hold the keys for.
You don't need to know up front whether this applies to you. Call `prepare`: if a proof is required you get the exact message to sign, and if it isn't you get `400 ownership-proof-not-applicable` and can create the account directly.
[`POST /accounts/{accountId}/virtual-bank-accounts/prepare`](/api-reference/Finance-API/virtual-bank-accounts/prepare-a-wallet-ownership-proof) with the destination `walletAddress` and `blockchain`.
Nothing is persisted — no preparation id, no expiry. Two calls for the same account and wallet return an equivalent message, so this is safe to repeat.
The customer signs `message` **exactly as returned**, blind. It embeds an opaque provider token, so don't reformat, re-encode, pretty-print, or reconstruct it — the signature is checked against those exact bytes.
Include the `ownershipProof` block — `walletAddress`, `blockchain`, `message`, `signature` — when [creating the virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/create-a-virtual-bank-account).
Where a proof isn't required the `ownershipProof` field is ignored, so you can send it unconditionally if that is simpler than branching.
Only an externally-owned account can sign: the scheme is EIP-191, and ERC-1271 contract signatures are not supported. Self-custody wallets are [single-key wallets only](/guides/finance/managed-vs-self-custody#single-key-wallets-only) for now.
| Code | Meaning |
| - | - |
| `ownership-proof-required` | This account requires a signed proof, but no `ownershipProof` block was supplied. |
| `ownership-proof-not-applicable` | This account doesn't need a signed proof — create it without one. |
| `invalid-proof-message` | The message is malformed, or doesn't reference the wallet address and embedded token. |
| `signature-mismatch` | The signature doesn't recover the supplied wallet address. The provider is never called. |
## Verification gates
Provisioning onboards the account holder for verification, so their verification state can block you.
A verified party is not automatically ready. The account holder must also have accepted our partners'
terms themselves — see [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent).
Until that acceptance is recorded, provisioning is blocked, and the fix is to present the party's
consent link to your end customer rather than to retry.
These states can block you:
| Status | Code | What to do |
| - | - | - |
| `409` | `partner-onboarding-pending` | The account holder's verification is still pending. **Retryable** — retry once approved. |
| `422` | `partner-onboarding-rejected` | Verification was rejected or revoked. Provisioning cannot proceed. |
| `503` | `identity-verification-unavailable` | Transient. Retry with backoff. |
## Reading them back
* [List virtual bank accounts](/api-reference/Finance-API/virtual-bank-accounts/list-virtual-bank-accounts) for an account.
* [Get a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/get-virtual-bank-account-details) by ID.
* [List pay-ins](/api-reference/Finance-API/pay-ins/list-pay-ins) filtered by `virtualBankAccountId` for everything that arrived on it — see [Pay-ins](/guides/finance/pay-ins).
## Closing a virtual bank account
[`POST /accounts/{accountId}/virtual-bank-accounts/{virtualBankAccountId}/close`](/api-reference/Finance-API/virtual-bank-accounts/close-a-virtual-bank-account)
moves an `ACTIVE` account to `CLOSED` and asks the payment provider to decommission the bank details.
There is no request body.
The local transition is applied **before** the provider is contacted, so a provider outage never
blocks a closure — but it also means `CLOSED` alone does not tell you the bank details are dead. Read
`providerClosureStatus` on the response:
| `providerClosureStatus` | The bank details are… | Do |
| - | - | - |
| `SUCCEEDED` | Dead. The provider confirmed closure. | Nothing. |
| `FAILED` | Presumed **live**. The provider call failed. | Call close again — it is idempotent and retries only the provider leg. A failure that persists needs Venly. |
| `NOT_SUPPORTED` | Live indefinitely. The provider offers no closure. | Stop showing the details to payers. Not returned by any current provider. |
Money that still arrives on closed details — after a `FAILED` closure, or in the window before an
in-flight transfer lands — is **never lost**. It is recorded and can be settled by Venly afterwards,
and it stays visible under [List pay-ins](/api-reference/Finance-API/pay-ins/list-pay-ins) filtered by
the closed `virtualBankAccountId`.
Closing frees the account's `(currency, targetCryptocurrency)` slot immediately, so you can create a
replacement straight away. It is also how you resolve `duplicate-eur-vba` — an account may hold only
one active EUR virtual bank account so that incoming credits can be attributed. `CLOSED` accounts stay
visible in list and detail reads.
## Next steps
Where converted funds land.
The other direction: crypto out, fiat to a bank beneficiary.
Which fiat currencies and stablecoins are supported.
Hear about deposits and rail population without polling.
# Wallets & balances
Source: https://docs.venlyfinance.com/guides/finance/wallets
The custodial wallets behind every account — the two-wallet model, balance fields, and how funds get reserved.
When you [create an account](/api-reference/Finance-API/accounts/create-a-new-account), Venly provisions the crypto wallets that hold its funds. You don't manage keys or contracts directly — you read balances and move funds through the API.
## The two-wallet model
Each account has an **account wallet** on the account's chain, holding the customer's balance. On
self-custody accounts the holder signs a [permit](/guides/finance/permits-and-allowances) so Venly can
move its funds; on Venly-managed accounts that's handled for you.
Accounts set up for card payments (coming soon) get a second, always Venly-managed **escrow wallet**
for funds in flight during settlement. You never sign for it.
## Custody type
A wallet's `type` is either **`VENLY_MANAGED`** (Venly holds the keys) or **`SELF_CUSTODY`** (your customer holds them). This is fixed by your company setup and changes how funds move — see [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody).
## Reading wallets
[List wallets for an account](/api-reference/Finance-API/wallets/list-wallets-for-an-account) returns each wallet with its address, per-asset balances, and AML status:
```json Response 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": "125.00", "available": "100.00", "reserved": "25.00" }
}
],
"amlStatus": "APPROVED"
}
]
}
```
## Balance fields
Each asset balance has three amounts, returned as **decimal strings**:
| Field | Meaning |
| - | - |
| `total` | Everything in the wallet. |
| `available` | Free to spend on a new transfer or payment. |
| `reserved` | Held against an in-flight operation — not spendable until that operation completes. |
`total = available + reserved`. When an operation reserves funds, the amount moves from `available` to `reserved`; when it completes, the funds either leave the wallet or return to `available`.
## AML status
`amlStatus` reflects the wallet's screening: `PENDING`, `APPROVED`, or `REJECTED`. A wallet must clear AML (and, for self-custody, have a confirmed permit) before it can move funds.
[List wallets](/api-reference/Finance-API/wallets/list-wallets-for-an-account) exposes `amlStatus` only. For **Venly-managed** wallets, `amlStatus: APPROVED` means ready. For **self-custody**, also check the **permit status**: once the account wallet's permit is `CONFIRMED`, Venly can move its funds. See [Approving transfers without gas](/guides/finance/permits-and-allowances).
## Next steps
Let Venly move a self-custody wallet's funds with a gasless permit.
The endpoint reference, with the full response schema.
# Webhooks
Source: https://docs.venlyfinance.com/guides/finance/webhooks
Have Venly push asynchronous events to your endpoint instead of polling for them.
Much of what the Finance API does finishes **after** your request returns. A pay-in session converts
minutes later. A verification verdict arrives when a reviewer gets to it. A pay-out's fiat leg settles
on the bank's schedule — and can bounce afterwards.
You can poll for all of it, but webhooks are the better answer: register an HTTPS endpoint once, and
Venly pushes each state change to you.
## What's worth a webhook
| Area | Why polling hurts |
| - | - |
| [Pay-ins](/guides/finance/pay-ins) | Settlement happens on-chain after the fiat arrives; `PAY_IN_SETTLED` tells you the moment the crypto is available. |
| [Pay-outs](/guides/finance/payouts) | A `201` doesn't mean sent, and `COMPLETED` can still become `RETURNED` later. |
| [Verification](/guides/finance/onboarding/lifecycle) | Verdicts are human-paced; polling a party for hours is wasteful. |
| [Partner-terms consent](/guides/finance/onboarding/partner-terms-consent) | Your end customer accepts on their own schedule — there is nothing to poll usefully. |
| [Virtual bank accounts](/guides/finance/virtual-bank-accounts) | EUR deposit rails may populate after creation returns. |
## Registering an endpoint
[`POST /webhooks`](/api-reference/Finance-API/webhooks/register-a-webhook)
```json theme={null}
{
"url": "https://client.example/hooks/venly",
"name": "Production events",
"authenticationMethod": {
"type": "API_KEY",
"headerName": "X-Webhook-Key",
"apiKey": "whsec_9f2c41ab7d"
}
}
```
The `url` must be an absolute `https://` endpoint — plain HTTP is rejected with `400`.
### Authenticating the caller
Your endpoint is public, so it needs a way to tell a real Venly delivery from anyone else who found
the URL. Pick one method at registration:
Venly injects `apiKey` into the header you name in `headerName`. Compare it against your stored
value using a constant-time comparison.
Standard HTTP basic auth with `username` and `password`.
Secrets (`apiKey`, `password`) are **write-only**. They are accepted on registration and update, and
never returned by any read. Store your own copy — if you lose it, you must
[update the webhook](/api-reference/Finance-API/webhooks/update-a-webhook) with a new secret rather
than recovering the old one.
## Testing your endpoint
Once registered, [send a ping](/api-reference/Finance-API/webhooks/ping-a-webhook):
```bash theme={null}
curl -X POST https://api.venlyfinance.com/v1/webhooks/{webhookId}/ping \
-H "Authorization: Bearer {access_token}"
```
This queues a synthetic `PING` event to that one webhook, exercising the real delivery path —
including the authentication header your handler expects.
Delivery is **asynchronous**. A `200` means the ping was accepted for delivery, not that your endpoint
received it. Confirm arrival in your own logs.
## Updating is a full replacement
[`PUT /webhooks/{webhookId}`](/api-reference/Finance-API/webhooks/update-a-webhook) replaces the whole
configuration — it is not a partial patch.
Because secrets are never returned by a read, you cannot fetch the current config, tweak one field,
and send it back: the secret would be missing. Always send every field you want to keep, including a
complete `authenticationMethod`.
This is also how you rotate a key or password.
## What you receive
Every delivery has the same two-field shape — the event type, and an event-specific `result`:
```json theme={null}
{
"eventType": "TRANSFER_COMPLETED",
"result": {
"transferId": "9d41f0c7-2b8e-4a15-93cf-6e07ba5d1284",
"senderAccountId": "4a7c33e1-59db-4f02-8c6a-b1e9d7205fa3",
"receiverAccountId": "c8e5b204-71fa-4d38-95b7-30a6f8c41de9",
"asset": "USDC",
"chain": "BASE",
"transactionHash": "0x4f7a2c9e18b3d05a6c7e94f120d8b3a5e6f1c07d92b4a8e35f6c1d0b7a92e438",
"occurredAt": "2026-08-19T09:14:52.318472901Z"
}
}
```
Two things to build around:
**No event carries an amount.** This is deliberate. Amounts, balances and bank coordinates stay
behind the authenticated REST API — an event tells you *what changed*, and you read the values back
over REST. So a `PAYOUT_RETURNED` tells you a return happened, not how much came back.
**Every webhook receives every event.** Registration has no event-type selection, so you cannot
narrow what an endpoint gets. Switch on `eventType` in your handler and ignore what you don't need.
`occurredAt` is when the state change happened, not when it was delivered — deliveries typically
land one to twenty seconds later, and a retry can arrive much later.
## Event catalogue
Sixteen event types:
| `eventType` | Fires when | `result` fields |
| - | - | - |
| `PING` | You call the test endpoint. Carries no business data. | `message` |
| `PAY_IN_SETTLED` | A pay-in's crypto credit is confirmed on-chain and available to the account. | `payInId`, `accountId`, `status`, `asset`, `chain`, `transactionHash`, `occurredAt` |
| `PAY_IN_FAILED` | A pay-in ended without ever crediting the account. No `PAY_IN_SETTLED` will follow. | `payInId`, `accountId`, `status`, `reason`, `occurredAt` |
| `TRANSFER_COMPLETED` | An account-to-account transfer settled with the receiving account. | `transferId`, `senderAccountId`, `receiverAccountId`, `asset`, `chain`, `transactionHash`, `occurredAt` |
| `TRANSFER_FAILED` | A transfer failed terminally. The funds remain with the sender. | `transferId`, `senderAccountId`, `receiverAccountId`, `asset`, `chain`, `reason`, `occurredAt` |
| `PAYOUT_PROCESSING` | The crypto left the account wallet and reached the banking partner. The send leg can no longer fail. | `payoutId`, `accountId`, `status`, `occurredAt` |
| `PAYOUT_COMPLETED` | The beneficiary has been paid. | `payoutId`, `accountId`, `status`, `occurredAt` |
| `PAYOUT_REJECTED` | The pay-out was refused **before** execution began, so no funds left the account. | `payoutId`, `accountId`, `status`, `reason`, `occurredAt` |
| `PAYOUT_FAILED` | Execution had already begun and then failed terminally. No money reached the beneficiary. | `payoutId`, `accountId`, `status`, `reason`, `occurredAt` |
| `PAYOUT_RETURNED` | The receiving bank sent the pay-out back. | `payoutId`, `accountId`, `status`, `reason`, `occurredAt` |
| `PAYOUT_ROUTE_ACTIVATED` | A [pay-out route](/guides/finance/payouts#setup) finished registering and can now be used. | `payoutRouteId`, `accountId`, `status`, `occurrenceId`, `occurredAt` |
| `PAYOUT_ROUTE_AWAITING_OWNERSHIP_PROOF` | A route is waiting for the account holder to [sign a wallet-ownership proof](/guides/finance/payouts#proving-you-control-the-source-wallet). Nothing moves until they do. | `payoutRouteId`, `accountId`, `status`, `occurrenceId`, `occurredAt` |
| `PAYOUT_ROUTE_REJECTED` | A route was turned down, so no pay-out can be requested over it. | `payoutRouteId`, `accountId`, `status`, `reason`, `occurrenceId`, `occurredAt` |
| `VIRTUAL_BANK_ACCOUNT_CREATED` | The account can now receive deposits. | `virtualBankAccountId`, `accountId`, `status`, `occurredAt` |
| `ACCOUNT_WALLETS_PROVISIONED` | An account's wallet pair cleared AML screening and can be used. | `accountId`, `walletPairId`, `walletType`, `chain`, `accountWalletAddress`, `escrowWalletAddress`, `amlStatus`, `occurredAt` |
| `PARTY_VERIFICATION_COMPLETED` | An individual party's verification produced an outcome. Read `status` — it is usually `VERIFIED` or `REJECTED`, but `VERIFICATION_PENDING` is also possible, so don't assume the event means a decision was reached. | `partyId`, `status`, `occurredAt` |
**Coming to this catalogue: partner-terms acceptance.** An event announcing that an end customer has
completed [partner-terms consent](/guides/finance/onboarding/partner-terms-consent) is being added, so
you will not have to poll the party to learn that onboarding can proceed. The event type is being
finalised — until it lands, read the party back to check its consent state.
Five of these need care:
* **`PAYOUT_REJECTED` vs `PAYOUT_FAILED`** — rejected means nothing was ever sent; failed means execution started and then broke. Only the second can leave a pay-out needing investigation.
* **`PAYOUT_RETURNED` does not imply an earlier `PAYOUT_COMPLETED`.** A return can be reported while the pay-out is still processing, so never treat it as a correction to a completion you must have received.
* **`VIRTUAL_BANK_ACCOUNT_CREATED` fires on readiness, not on creation.** The resource exists earlier, without usable bank coordinates. This event means deposits will now land.
* **`PARTY_VERIFICATION_COMPLETED` fires on every verdict**, including a re-verification of the same party. Others are keyed per resource, so a second delivery for the same resource is a duplicate; this one may be genuinely new.
* **The three `PAYOUT_ROUTE_*` events are keyed per occurrence, not per route.** A re-registered route can park for a fresh signature a second time, and each time needs acting on. Key your handler on `payoutRouteId` **plus `occurrenceId`**: a redelivery repeats the `occurrenceId`, a genuine new occurrence carries a new one.
## Event payloads
Crypto and fiat-originated transfers publish the **same** event with an identical payload — nothing reveals which one it was.
```json TRANSFER_COMPLETED theme={null}
{
"eventType": "TRANSFER_COMPLETED",
"result": {
"transferId": "9d41f0c7-2b8e-4a15-93cf-6e07ba5d1284",
"senderAccountId": "4a7c33e1-59db-4f02-8c6a-b1e9d7205fa3",
"receiverAccountId": "c8e5b204-71fa-4d38-95b7-30a6f8c41de9",
"asset": "USDC",
"chain": "BASE",
"transactionHash": "0x4f7a2c9e18b3d05a6c7e94f120d8b3a5e6f1c07d92b4a8e35f6c1d0b7a92e438",
"occurredAt": "2026-08-19T09:14:52.318472901Z"
}
}
```
```json TRANSFER_FAILED theme={null}
{
"eventType": "TRANSFER_FAILED",
"result": {
"transferId": "7b02ea56-c3d9-4718-8f45-2ac6be901f37",
"senderAccountId": "4a7c33e1-59db-4f02-8c6a-b1e9d7205fa3",
"receiverAccountId": "e1f7d9a3-46b2-4c85-91da-587c0e2b6d34",
"asset": "EURC",
"chain": "AVALANCHE",
"reason": "INSUFFICIENT_ALLOWANCE",
"occurredAt": "2026-08-19T09:21:07.664215330Z"
}
}
```
`reason` is one of `ON_CHAIN_FAILED`, `INSUFFICIENT_FUNDS`, `INSUFFICIENT_ALLOWANCE`, `NOT_SUBMITTED`, `UNKNOWN`.
A failed transfer has no `transactionHash` when it never reached the chain (`NOT_SUBMITTED`).
A pay-out publishes up to two events: one when it reaches the banking partner, and one for its outcome.
```json PAYOUT_PROCESSING theme={null}
{
"eventType": "PAYOUT_PROCESSING",
"result": {
"payoutId": "5c8fd317-a094-42be-b6e1-7d3f02951ac8",
"accountId": "83b16e40-df75-4a29-9c03-e5a18427bd6f",
"status": "PROVIDER_PROCESSING",
"occurredAt": "2026-08-19T10:02:41.127903558Z"
}
}
```
```json PAYOUT_COMPLETED theme={null}
{
"eventType": "PAYOUT_COMPLETED",
"result": {
"payoutId": "5c8fd317-a094-42be-b6e1-7d3f02951ac8",
"accountId": "83b16e40-df75-4a29-9c03-e5a18427bd6f",
"status": "COMPLETED",
"occurredAt": "2026-08-19T10:05:18.844061272Z"
}
}
```
```json PAYOUT_RETURNED theme={null}
{
"eventType": "PAYOUT_RETURNED",
"result": {
"payoutId": "a0d64b98-31ce-4f27-85ba-c9147e0d2f63",
"accountId": "83b16e40-df75-4a29-9c03-e5a18427bd6f",
"status": "RETURNED",
"reason": "RETURNED_BY_BANK",
"occurredAt": "2026-08-19T10:11:33.209774615Z"
}
}
```
`PAYOUT_REJECTED` and `PAYOUT_FAILED` have the same shape as `PAYOUT_RETURNED`, with `status` `REJECTED` or `FAILED`.
`reason` is one of `REJECTED_BY_PROVIDER`, `ROUTE_UNAVAILABLE`, `SEND_FAILED`, `RETURNED_BY_BANK`, `UNKNOWN`.
The `reason` is a fixed enum, never free text. Operational notes recorded against a pay-out internally are not delivered — if you need more detail than the enum, [read the pay-out back](/api-reference/Finance-API/payouts/get-a-payout).
A route raises an event for each state you can act on. `PENDING` and `REGISTERING` raise none.
```json PAYOUT_ROUTE_AWAITING_OWNERSHIP_PROOF theme={null}
{
"eventType": "PAYOUT_ROUTE_AWAITING_OWNERSHIP_PROOF",
"result": {
"payoutRouteId": "e4b19a07-6c3d-4f82-9a15-0d7e3b8c2f61",
"accountId": "83b16e40-df75-4a29-9c03-e5a18427bd6f",
"status": "AWAITING_OWNERSHIP_PROOF",
"occurrenceId": "1f6c2d8a-93b4-4e05-b7a1-5c0e9d3f4a28",
"occurredAt": "2026-10-02T09:41:17.502318774Z"
}
}
```
`PAYOUT_ROUTE_ACTIVATED` has the same shape, with `status` `ACTIVE`.
```json PAYOUT_ROUTE_REJECTED theme={null}
{
"eventType": "PAYOUT_ROUTE_REJECTED",
"result": {
"payoutRouteId": "e4b19a07-6c3d-4f82-9a15-0d7e3b8c2f61",
"accountId": "83b16e40-df75-4a29-9c03-e5a18427bd6f",
"status": "REJECTED",
"reason": "BENEFICIARY_DATA_INCOMPLETE",
"occurrenceId": "8a3e5f10-2b7c-4d96-a0e4-6f1b9c7d2e53",
"occurredAt": "2026-10-02T09:44:02.118930446Z"
}
}
```
| `reason` | Meaning | What to do |
| - | - | - |
| `REJECTED_BY_PROVIDER` | The banking partner refused the registration. Deliberately non-specific. | Nothing to correct on your side. |
| `BENEFICIARY_DATA_INCOMPLETE` | Beneficiary details the partner requires are missing or unusable. | Correct the party or payout bank account and register the route again. |
| `OWNERSHIP_PROOF_UNAVAILABLE` | The route's source wallet can't produce the ownership proof the partner requires. | Use an account wallet that can sign. |
| `REGISTRATION_FAILED` | Registration couldn't complete for a technical reason on our side. | Contact Venly. |
| `UNKNOWN` | Anything else, including a route an operator turned down. | Contact Venly. |
Treat `reason` as an open enum — handle an unrecognised value as `UNKNOWN`. It is a different vocabulary from the pay-out `reason` above: never map one onto the other.
The route's deposit address is **not** in the payload, even on activation. [Read the route](/api-reference/Finance-API/payout-routes/list-payout-routes) for it.
```json PAY_IN_SETTLED theme={null}
{
"eventType": "PAY_IN_SETTLED",
"result": {
"payInId": "b3719ce5-8f24-4d60-a917-4c8e05f2d6b1",
"accountId": "62ad8f14-70e3-49c5-b8f2-1d90ae637c05",
"status": "SETTLED",
"asset": "USDC",
"chain": "BASE",
"transactionHash": "0xa19c47f2e8b06d35c1f7094ae2b8d5306f4c19e7ad025b8f3c6019e74bd2f581",
"occurredAt": "2026-08-19T11:37:09.552840173Z"
}
}
```
```json PAY_IN_FAILED theme={null}
{
"eventType": "PAY_IN_FAILED",
"result": {
"payInId": "d5e8017b-49ac-4f31-96b0-8a27ce4f10d3",
"accountId": "62ad8f14-70e3-49c5-b8f2-1d90ae637c05",
"status": "FAILED",
"reason": "REJECTED_BY_PROVIDER",
"occurredAt": "2026-08-19T11:44:26.981037462Z"
}
}
```
`reason` is one of `SETTLEMENT_FAILED`, `REJECTED_BY_PROVIDER`, `UNKNOWN`.
A `PAY_IN_FAILED` is terminal for that pay-in — no `PAY_IN_SETTLED` will follow it.
```json ACCOUNT_WALLETS_PROVISIONED theme={null}
{
"eventType": "ACCOUNT_WALLETS_PROVISIONED",
"result": {
"accountId": "18fc6a92-b537-4e08-91d4-cb70a3f5e264",
"walletPairId": "7e3d0b85-1a4f-4926-83c7-59d2e0148fab",
"walletType": "VENLY_MANAGED",
"chain": "AVALANCHE",
"accountWalletAddress": "0x6D3fA92c15E0847bB1de5C90a7F328e4051cB7d9",
"escrowWalletAddress": "0xB84e07f1cA935D620387be4dF16c8a0952E7d34C",
"amlStatus": "APPROVED",
"occurredAt": "2026-08-19T08:52:14.703918046Z"
}
}
```
This is the one event that carries on-chain addresses — they are the point of it. One event per wallet pair, not per account.
```json PARTY_VERIFICATION_COMPLETED theme={null}
{
"eventType": "PARTY_VERIFICATION_COMPLETED",
"result": {
"partyId": "2f95c8e0-6b71-4a3d-8e14-07db5c9f2a68",
"status": "VERIFIED",
"occurredAt": "2026-08-19T13:26:55.410682937Z"
}
}
```
`status` is `VERIFIED`, `REJECTED`, or `VERIFICATION_PENDING`. The verdict and nothing else — no reasons, documents, or personal data. A rejection tells you the outcome, not why.
```json VIRTUAL_BANK_ACCOUNT_CREATED theme={null}
{
"eventType": "VIRTUAL_BANK_ACCOUNT_CREATED",
"result": {
"virtualBankAccountId": "4c1e73d9-a825-4b06-97fe-3d6082ca4157",
"accountId": "62ad8f14-70e3-49c5-b8f2-1d90ae637c05",
"status": "ACTIVE",
"occurredAt": "2026-08-19T12:08:47.336529814Z"
}
}
```
The bank coordinates are **not** in the event. Read them from
[Get a virtual bank account](/api-reference/Finance-API/virtual-bank-accounts/get-virtual-bank-account-details) once this arrives.
```json PING theme={null}
{
"eventType": "PING",
"result": { "message": "ping" }
}
```
Sent only by the [ping endpoint](/api-reference/Finance-API/webhooks/ping-a-webhook). Handle it as a no-op, but do return `2xx` — it is how you prove your endpoint works.
## Writing a handler
A few rules that hold regardless of which events you subscribe to:
Check the authentication header first and reject anything that fails, before parsing the body.
Acknowledge with a `2xx` immediately and hand the payload to a queue. Slow handlers look like
failures to any delivery system.
Treat delivery as at-least-once — retries do happen in practice. Key your processing on the
event's **resource id plus target status** so a repeat is a no-op — for a second delivery of
`TRANSFER_COMPLETED`, the `transferId` is already `COMPLETED` on your side and there is nothing
to do. Same discipline as [idempotency](/guides/finance/idempotency) on the request side. The
`PAYOUT_ROUTE_*` events are the exception: key those on `payoutRouteId` plus `occurrenceId`.
A later state can arrive before an earlier one, so compare against the state you already hold.
Do **not** simply discard anything that looks like a step backwards. Some transitions legitimately
reverse an outcome — a pay-out can go `COMPLETED` → `RETURNED` when the receiving bank sends it
back, sometimes long afterwards. Decide per event type which transitions are real, rather than
ordering states and dropping everything below the high-water mark.
For money movement, treat the event as a *hint that something changed* and confirm against the API
— for example [Get a payout](/api-reference/Finance-API/payouts/get-a-payout) — before crediting a
ledger or releasing goods.
## Managing registrations
| Operation | Endpoint |
| - | - |
| List all | [`GET /webhooks`](/api-reference/Finance-API/webhooks/list-webhooks) — not paginated |
| Read one | [`GET /webhooks/{webhookId}`](/api-reference/Finance-API/webhooks/get-webhook-details) |
| Replace | [`PUT /webhooks/{webhookId}`](/api-reference/Finance-API/webhooks/update-a-webhook) |
| Remove | [`DELETE /webhooks/{webhookId}`](/api-reference/Finance-API/webhooks/delete-a-webhook) |
Registering requires the `manage:webhooks` role; reading requires `view:webhooks`.
There is a per-company cap on registered webhooks — exceeding it returns `409`. If you need more
endpoints than the cap allows, fan out on your own side from a single receiver.
Webhook delivery is handled by Venly. A `503` on any of these endpoints means
that service was briefly unreachable, not that your registration is broken — retry with backoff.
## Debugging deliveries
When an event you expected never arrived, you no longer need support to find out why. Every webhook
keeps a **delivery history**:
| Operation | Endpoint | Scope |
| - | - | - |
| List deliveries | [`GET /webhooks/{webhookId}/deliveries`](/api-reference/Finance-API/webhooks/list-webhook-deliveries) | `view:webhooks` |
| Read one in full | [`GET /webhooks/{webhookId}/deliveries/{deliveryId}`](/api-reference/Finance-API/webhooks/get-a-webhook-delivery) | `view:webhooks` |
| Resend | [`POST /webhooks/{webhookId}/deliveries/{deliveryId}/resend`](/api-reference/Finance-API/webhooks/resend-a-webhook-delivery) | `manage:webhooks` |
Filter the list by `eventType`, `status` and a time window, then read the delivery you care about for
the exact `payload` that was sent and, per attempt, the HTTP status and body your endpoint returned —
or the transport error when there was no response at all.
| `status` | Meaning | Resendable |
| - | - | - |
| `SCHEDULED` / `PROCESSING` / `RETRY` | In flight or awaiting an automatic retry | No |
| `SUCCESS` | Your endpoint accepted it; `deliveredAt` is set | No |
| `FAILURE` | Your endpoint was called and did not accept it | **Yes** |
| `DISCARDED` | Nothing was ever sent — the endpoint was inactive, or the event was filtered | **Yes** |
`FAILURE` and `DISCARDED` are different problems: the first is your endpoint's fault, the second your
registration's. A **resend** replays the original payload as a further attempt on the same delivery,
with the same `notificationId` — so a duplicate-tolerant handler will process it, and a deduplicating
one will not discard it as a replay of something it already saw.
Three things this history is not. It is **not** the source of truth for your ledger — read the resource.
It does **not** include headers in either direction, because yours carry the credentials you registered.
And it does **not** reach back indefinitely — how far is the delivery service's retention window, so treat
older history as unavailable rather than as proof nothing was sent. The list is cursor-paginated: send
`nextPageCursor` back exactly as received.
## Next steps
Where webhooks matter most — a `201` isn't a success signal.
The request-side counterpart to duplicate-tolerant handlers.
# Account Management
Source: https://docs.venlyfinance.com/guides/payments/accounts
Configure company bank accounts and crypto wallets that source and receive funds during Fundflow ramp requests.
## Overview
Fundflow requires you to set up both **bank accounts** and **crypto wallets** to facilitate on-ramp and off-ramp transactions:
For fiat currency transactions (EUR, USD, GBP)
For cryptocurrency transactions across multiple blockchains
**Both account types are required:**
* **On-Ramp** (Fiat → Crypto): Bank account → Crypto wallet
* **Off-Ramp** (Crypto → Fiat): Crypto wallet → Bank account
***
## Bank Accounts
### Supported Bank Account Types
Fundflow supports multiple payment networks across different regions:
| Type | Region | Description | Required Fields |
| - | - | - | - |
| **EUR\_SEPA** | Europe | SEPA transfers | IBAN, BIC |
| **USD\_WIRE** | United States | Wire transfers | Account number, Routing number |
| **USD\_ACH** | United States | ACH transfers | Account number, Routing number |
| **USD\_SWIFT** | United States | SWIFT transfers | BIC, Account number, Bank address |
| **GBP\_FPS** | United Kingdom | Faster Payments | Account number, Sort code |
| **GBP\_CHAPS** | United Kingdom | CHAPS transfers | Account number, Sort code |
| **OTHER\_SWIFT** | International | Other currencies via SWIFT | Currency code, BIC, IBAN or Account number |
### Adding a Bank Account
Use the [Create Company Bank Account](/api-reference/Fundflow-API/company-bank-accounts/create-a-company-bank-account) endpoint
Provide required information based on account type:
* Account holder details (company name, address)
* Bank details (name, country, account numbers)
* Contact information (email, phone)
* Supported ramp type (ON\_RAMP, OFF\_RAMP, or both)
Your bank account will be created with **PENDING** verification status
The Venly team will manually review and verify the account details
Once verified, the status changes to **VERIFIED** and the account can be used for transactions
Bank account verification is done manually to ensure security and compliance with financial regulations.
### Bank Account Requirements
* Account must be in your company's legal name
* Valid account numbers in the correct format for the account type
* Bank must be in a supported country
* Complete and accurate beneficiary address information
### Managing Bank Accounts
**List all bank accounts:**
```bash theme={null}
GET /v1/company-bank-accounts
```
**Get specific account:**
```bash theme={null}
GET /v1/company-bank-accounts/{id}
```
**Update account name:**
```bash theme={null}
PATCH /v1/company-bank-accounts/{id}
```
Updates use optimistic locking. Include the current `version` number to prevent concurrent modification conflicts.
***
## Crypto Wallets
### Supported Blockchain Networks
Fundflow supports wallets on the following blockchain networks:
* **Ethereum** (ETH, USDC, EURC, USDS)
* **Polygon** (POL)
* **Base**
* **Arbitrum**
* **Sui**
### Adding a Crypto Wallet
Use the [Create Company Wallet](/api-reference/Fundflow-API/company-wallets/create-a-company-wallet) endpoint
Provide:
* Wallet address (e.g., 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb)
* Blockchain network (ETHEREUM, POLYGON, BASE, ARBITRUM, or SUI)
* Optional description for identification
Your wallet will be created with **PENDING** verification status
Complete the verification process to prove wallet ownership
Once verified, the status changes to **VERIFIED** and the wallet can be used for on-ramp transactions
### Managing Crypto Wallets
**List all wallets:**
```bash theme={null}
GET /v1/company-wallets
```
Filter by:
* Verification status (PENDING, VERIFIED, DENIED)
* Blockchain network
* Wallet address
**Get specific wallet:**
```bash theme={null}
GET /v1/company-wallets/{id}
```
**Update wallet description:**
```bash theme={null}
PATCH /v1/company-wallets/{id}
```
***
## Account Statuses
| Status | Meaning | Actions Available |
| - | - | - |
| **PENDING** | Awaiting verification | Wait for review |
| **VERIFIED** | Ready to use | Can be used in ramp requests |
| **DENIED** | Verification failed | Review details and resubmit |
***
## Supported Ramp Types
When creating a bank account, specify which operations it supports:
* **ON\_RAMP**: Can receive fiat deposits (for buying crypto)
* **OFF\_RAMP**: Can send fiat withdrawals (for selling crypto)
* **ON\_AND\_OFF\_RAMP**: Supports both operations
Use descriptive names for your accounts like "EUR SEPA - Operations" or "Main Treasury Wallet" to easily identify them when creating ramp requests.
***
## API Reference
Manage company bank accounts
Manage company crypto wallets
View supported fiat currencies
View supported cryptocurrencies
***
## Best Practices
**Prepare Accounts Early**: Add and verify your bank accounts and wallets during your company onboarding process, before you need to create ramp requests.
**Multiple Accounts**: You can add multiple bank accounts and wallets to support different currencies and use cases.
**Security**: Never share private keys or seed phrases. Fundflow only needs your wallet address for verification.
***
## Next Steps
Start creating on-ramp and off-ramp transactions
Understand fee calculations
# API Authentication
Source: https://docs.venlyfinance.com/guides/payments/authentication
Authenticate with Venly Fundflow API using OAuth2 client credentials
## Overview
Venly Fundflow API uses **OAuth2 client credentials flow**. All API requests require a bearer token in the `Authorization` header.
***
## Get an Access Token
Send a `POST` request to the token endpoint for your environment:
| Environment | Endpoint |
| :- | :- |
| Staging | `POST https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
| Production | `POST https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` |
### Request Body
| Parameter | Value |
| :- | :- |
| `grant_type` | `client_credentials` |
| `client_id` | Your client ID |
| `client_secret` | Your client secret |
### Response
```json theme={null}
{
"access_token": "eyJhbGciOiJSUzI1NiIsIn......",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "email profile"
}
```
The response includes an `access_token` valid for **5 minutes** (300 seconds). Once expired, repeat the call above to get a new one.
***
## Use the Token
Pass the `access_token` as a bearer token in the `Authorization` header of every API call:
```bash theme={null}
curl -X GET https://api-fundflow.venly.io/v1/company \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json"
```
***
## Best Practices
* **Never** expose client secrets in client-side code, logs, or version control.
* Store credentials in environment variables or a secrets manager.
* Implement auto-refresh logic to request a new token before the current one expires.
# Dashboard
Source: https://docs.venlyfinance.com/guides/payments/dashboard
Fundflow API architecture — resource model, data types, response envelope, and the integration patterns that make production calls reliable.
## API Architecture
Fundflow API follows RESTful principles with a clear resource-based structure. All endpoints are accessible via the base URL:
```text theme={null}
https://api-fundflow.venly.io/v1
```
All requests require a Bearer token in the `Authorization` header. See [Authentication](/guides/payments/authentication) for how to obtain one.
For a full endpoint cheat sheet with examples, pagination, filtering, and status codes, see the [Quick Reference](/guides/payments/quick-reference).
***
## Data Types
### Common Fields
**Identifiers:**
* `id`: UUID format (e.g., `123e4567-e89b-12d3-a456-426614174000`)
**Timestamps:**
* `createdAt`, `updatedAt`, `verifiedAt`: ISO 8601 format (e.g., `2024-01-15T10:30:00Z`)
**Amounts:**
* Decimal numbers with up to 8 decimal places
* Minimum: 0.00000001
**Versions:**
* Integer for optimistic locking
* Increments with each update
***
## Best Practices
### Security
✅ **Do:**
* Store credentials securely (environment variables, vaults)
* Use HTTPS for all requests
* Implement token refresh logic
* Validate all input data
* Log API interactions for audit
❌ **Don't:**
* Expose credentials in client-side code
* Store tokens in version control
* Share access tokens between environments
* Ignore error responses
### Performance
✅ **Do:**
* Use pagination for large datasets
* Cache reference data
* Implement connection pooling
* Read a ramp request back rather than polling in a tight loop
❌ **Don't:**
* Poll frequently for status updates
* Fetch all data without pagination
* Make unnecessary API calls
* Ignore rate limits
### Error Handling
✅ **Do:**
* Handle all HTTP status codes
* Implement retry logic with backoff
* Log errors for debugging
* Provide meaningful error messages to users
* Handle version conflicts gracefully
❌ **Don't:**
* Ignore error responses
* Retry indefinitely
* Expose technical errors to end users
* Skip validation
***
## Quick Reference
Integration guide
Full API docs
Bank accounts & wallets
Ramp requests
Fee structure
Security practices
***
## Support
Need help with the API?
* **Contact**: [venlyfinance.com/contact](https://venlyfinance.com/contact)
* **API Status**: Monitor system status for any issues
# Fee Structure
Source: https://docs.venlyfinance.com/guides/payments/fees
How Fundflow ramp fees work — the calculate-fee endpoint, your fee configuration, exchange rates, and what appears on a ramp request.
## Overview
Fundflow applies fees to both on-ramp and off-ramp transactions. Fees are **company-specific** and may include volume-based tiers to provide better rates for higher transaction volumes.
Applied when converting fiat to crypto
Applied when converting crypto to fiat
***
## Fee Structure
### How Fees Work
Fees are calculated as a **percentage** of the transaction amount and are deducted from the total:
* **On-Ramp**: Fee is deducted from the fiat amount before conversion
* **Off-Ramp**: Fee is deducted from the crypto amount before conversion
### Fee Components
Each ramp request includes detailed fee information:
| Field | Description |
| - | - |
| **Fiat Amount** | Total fiat amount (including fees) |
| **Fiat Net Amount** | Net fiat after deducting fees |
| **Crypto Amount** | Cryptocurrency amount |
| **Fiat Fee Amount** | Fee amount in fiat currency |
| **Fee Percentage** | Percentage rate applied |
| **Exchange Rate** | Conversion rate used |
***
## Calculating Fees
### Using the API
Use the [Calculate Fee](/api-reference/Fundflow-API/fees/calculate-fee-for-a-ramp-request) endpoint to determine fees before creating a ramp request:
```bash theme={null}
POST /v1/fees/calculate
Content-Type: application/json
{
"amount": 1000.00,
"type": "ON_RAMP"
}
```
**Response:**
```json theme={null}
{
"success": true,
"result": {
"amount": 10.00,
"percentage": 1.0
}
}
```
The response tells you **your** fee for that request — the rate is configured per company, so read it
from the API rather than assuming a figure.
Rates are commercial terms and are not published here. For exact rates, or to request a quote, get in
touch at [venlyfinance.com/contact](https://venlyfinance.com/contact).
***
## Your Fee Configuration
Use the [Get Company Fee Configuration](/api-reference/Fundflow-API/fees/get-company-fee-configuration) endpoint:
```bash theme={null}
GET /v1/fees
```
**Response:**
```json theme={null}
{
"success": true,
"result": [
{
"id": "fee-tier-1-id",
"companyId": "your-company-id",
"name": "Standard Tier",
"type": "ON_RAMP",
"minVolume": 0,
"maxVolume": 10000,
"percentage": 1.5,
"version": 1
},
{
"id": "fee-tier-2-id",
"companyId": "your-company-id",
"name": "Volume Tier",
"type": "ON_RAMP",
"minVolume": 10001,
"maxVolume": 50000,
"percentage": 1.0,
"version": 1
}
]
}
```
***
## Fee Application
### When Fees Are Applied
Fees are applied at different stages depending on the ramp type:
```mermaid theme={null}
flowchart LR
subgraph On["On-ramp (fiat → crypto)"]
A1[Customer sends fiat] --> B1[Fee deducted from fiat]
B1 --> C1[Convert remaining fiat at FX rate]
C1 --> D1[Crypto credited to wallet]
end
subgraph Off["Off-ramp (crypto → fiat)"]
A2[Customer sends crypto] --> B2[Fee deducted from crypto]
B2 --> C2[Convert remaining crypto at FX rate]
C2 --> D2[Fiat credited to bank]
end
```
In both directions the fee is taken in the **input** currency, before conversion.
**On-Ramp:**
1. Fiat is sent to Venly deposit account
2. Fee is deducted from the fiat amount
3. Net amount is converted to crypto at current exchange rate
4. Crypto is sent to company wallet
**Off-Ramp:**
1. Crypto is sent to Venly deposit wallet
2. Fee is deducted from the crypto amount
3. Net amount is converted to fiat at current exchange rate
4. Fiat is sent to company bank account
### Fee Transparency
All fee information is clearly displayed in the ramp request:
```jsonc theme={null}
{
"id": "ramp-request-id",
"rampType": "ON_RAMP",
"fiatAmount": 1000.00,
"fiatNetAmount": 990.00,
"fiatFeeAmount": 10.00,
"feePercentage": 1.0,
"cryptoAmount": 990.00,
"exchangeRate": 1.0,
// …
}
```
***
## Exchange Rates
### How Rates Are Determined
Exchange rates are determined at the time of conversion and reflect current market conditions:
* Rates are sourced from reliable market data providers
* Rates include a small spread to account for market volatility
* Rates are locked when the transaction enters PROCESSING status
### Rate Visibility
The exchange rate used for each transaction is included in the ramp request details:
```bash theme={null}
GET /v1/ramp-requests/{id}
```
```jsonc theme={null}
{
"exchangeRate": 1.0012,
"fiatAmount": 1000.00,
"cryptoAmount": 998.80,
// …
}
```
Exchange rates may fluctuate between request creation and processing. The final rate is determined when the payment is received and conversion begins.
***
## Best Practices
### Before Creating Requests
**Calculate Fees First**: Always use the fee calculation endpoint before creating large ramp requests to understand the total cost.
### Optimizing Costs
**Plan Ahead**: Schedule large transactions during periods of lower market volatility to minimize exchange rate risk.
### Transparency
**Review Breakdowns**: Always review the complete fee and amount breakdown before approving ramp requests.
**Export Records**: Use the export functionality to maintain detailed records for accounting and tax purposes.
***
## Fee-Related API Endpoints
Calculate fees for a specific amount
View your company's fee tiers
Create a new transaction
Export transaction records
***
## Understanding Your Invoice
### What's Included
Your Fundflow invoice includes:
* Total transaction volume for the billing period
* Number of transactions processed
* Total fees collected
* Breakdown by transaction type (on-ramp vs off-ramp)
### Accessing Invoices
View your invoices through the company details:
```bash theme={null}
GET /v1/company
```
```jsonc theme={null}
{
"success": true,
"result": {
"id": "company-id",
"name": "Your Company",
"invoicesUrl": "https://portal.venly.io/invoices",
// …
}
}
```
***
## Frequently Asked Questions
Rates are agreed commercially rather than published, so they are not listed here. Your own
configuration is readable through the API on this page. For exact rates, or to request a quote,
get in touch at [venlyfinance.com/contact](https://venlyfinance.com/contact).
Fees can be configured differently for on-ramp and off-ramp operations. Check your fee configuration to see the specific rates for each type.
The fee calculation and the ramp request itself show the fee amount and percentage applied. Note that
the exchange rate also carries a spread, so the fee percentage is not the only component of the total
cost — compare the amount sent against the amount received to see the full picture.
Exchange rates are updated in real-time based on market conditions. The rate used for your transaction is locked when processing begins.
The exchange rate is locked when your transaction enters PROCESSING status (after payment is received). Any rate changes before that point will be reflected in the final conversion.
If a transaction fails before processing begins, no fees are charged. If it fails during processing, [contact us](https://venlyfinance.com/contact) to review the specific case.
***
## Next Steps
Start creating transactions
Set up bank accounts and wallets
Understand key terms
# Getting Started with Fundflow
Source: https://docs.venlyfinance.com/guides/payments/getting-started
End-to-end guide to integrate Fundflow — authentication, creating ramp requests, settlement, and going live with the on-ramp/off-ramp API.
## What is Fundflow?
Fundflow is Venly's enterprise API platform that enables businesses to seamlessly convert between traditional fiat currency and cryptocurrency through a comprehensive REST API.
Convert fiat to cryptocurrency via API
Convert cryptocurrency to fiat via API
## Key Features
### Enterprise-Grade Security
* OAuth2 authentication
* KYB (Know Your Business) verification
* Optimistic locking for concurrent updates
* Wallet ownership verification
* Role-based access control
### Transparent Costs
* Fee calculation before you commit to a request
* Full amount breakdown on every ramp request
* Clear exchange rates
### Multi-Currency Support
**Fiat Currencies:** EUR, USD, GBP
**Payment Networks:**
* EUR\_SEPA (European SEPA transfers)
* USD\_WIRE, USD\_ACH, USD\_SWIFT (US transfers)
* GBP\_FPS, GBP\_CHAPS (UK transfers)
* OTHER\_SWIFT (International transfers)
**Cryptocurrencies:**
* USDC (Ethereum)
* EURC (Ethereum)
* USDS (Ethereum)
* ETH (Ethereum)
* POL (Polygon)
**Blockchain Networks:**
* Ethereum
* Polygon
* Base
* Arbitrum
* Sui
***
## Prerequisites
Before you begin, ensure you have:
✅ **Company Account**: [Contact Venly](https://venlyfinance.com/contact) to set up your company account\
✅ **OAuth2 Credentials**: Client ID and secret for authentication\
✅ **API Access**: Staging: `https://api-fundflow-staging.venly.io/v1` · Production: `https://api-fundflow.venly.io/v1`\
✅ **Development Environment**: REST API client or SDK
***
## Integration Steps
```mermaid theme={null}
flowchart TD
A[Company onboarded] --> B[POST /company-bank-accounts]
B --> C{Bank account VERIFIED?}
C -- No --> C
C -- Yes --> D[POST /company-wallets]
D --> E{Wallet VERIFIED?}
E -- No --> E
E -- Yes --> F{Direction?}
F -- On-ramp --> G[POST /ramp-requests rampType=ON_RAMP]
F -- Off-ramp --> H[POST /ramp-requests rampType=OFF_RAMP]
G --> I[Approve → AWAITING_FUNDS]
H --> I
I --> J[Transaction lifecycle
see transactions guide]
```
Obtain an OAuth2 access token from Venly Identity Platform
```bash theme={null}
POST https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
```
Use the access token in all API requests:
```bash theme={null}
Authorization: Bearer YOUR_ACCESS_TOKEN
```
Check your company's KYB verification status
```bash theme={null}
GET /v1/company
```
**Response:**
```json theme={null}
{
"success": true,
"result": {
"id": "company-id",
"name": "Your Company",
"kybStatus": "VERIFIED",
"invoicesUrl": "https://portal.venly.io/invoices"
}
}
```
Your company must have `kybStatus: "VERIFIED"` to create ramp requests.
Register company bank accounts for fiat transactions
```bash theme={null}
POST /v1/company-bank-accounts
Content-Type: application/json
{
"bankAccountType": "EUR_SEPA",
"name": "Primary EUR Account",
"bankName": "Deutsche Bank",
"companyName": "Your Company Ltd",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"bankCountry": "DE",
"beneficiaryAddressLine1": "123 Main Street",
"beneficiaryCity": "Berlin",
"beneficiaryPostalCode": "10115",
"beneficiaryCountry": "DE",
"supportedRampType": "ON_AND_OFF_RAMP"
}
```
**Status Flow:** PENDING → VERIFIED (manual review by Venly)
Bank accounts require manual verification. This process may take 1-2 business days.
Register company cryptocurrency wallets
```bash theme={null}
POST /v1/company-wallets
Content-Type: application/json
{
"address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"chain": "ETHEREUM",
"description": "Main treasury wallet"
}
```
**Status Flow:** PENDING → VERIFIED
Complete the verification process to prove wallet ownership.
Retrieve supported currency pairs for your operations
**For On-Ramp:**
```bash theme={null}
GET /v1/ramp-requests/on-ramp/pairs
```
**For Off-Ramp:**
```bash theme={null}
GET /v1/ramp-requests/off-ramp/pairs
```
**Response:**
```json theme={null}
{
"success": true,
"result": [
{
"from": {
"id": "eur-id",
"currency": "EUR",
"label": "Euro"
},
"to": {
"id": "usdc-id",
"currency": "USDC",
"chain": "ETHEREUM",
"label": "USD Coin"
}
}
]
}
```
Calculate fees before creating ramp requests
```bash theme={null}
POST /v1/fees/calculate
Content-Type: application/json
{
"amount": 1000.00,
"type": "ON_RAMP"
}
```
**Response:**
```json theme={null}
{
"success": true,
"result": {
"amount": 10.00,
"percentage": 1.0
}
}
```
Create on-ramp or off-ramp transactions
**On-Ramp Example:**
```bash theme={null}
POST /v1/ramp-requests
Content-Type: application/json
{
"rampType": "ON_RAMP",
"amount": 1000.00,
"fiatCurrencyId": "eur-currency-id",
"cryptoCurrencyId": "usdc-ethereum-id",
"companyWalletId": "your-wallet-id"
}
```
**Off-Ramp Example:**
```bash theme={null}
POST /v1/ramp-requests
Content-Type: application/json
{
"rampType": "OFF_RAMP",
"amount": 0.5,
"cryptoCurrencyId": "usdc-ethereum-id",
"fiatCurrencyId": "eur-currency-id",
"companyBankAccountId": "your-bank-account-id"
}
```
Request is created in **AWAITING\_APPROVAL** status.
Approve the ramp request to proceed
```bash theme={null}
POST /v1/ramp-requests/{id}/approve
Content-Type: application/json
{
"version": 1
}
```
Status changes to **AWAITING\_FUNDS**.
* **On-Ramp**: Customer sends fiat to provided deposit bank account
* **Off-Ramp**: Customer sends crypto to provided deposit wallet
Read the ramp request back to monitor its status
```bash theme={null}
GET /v1/ramp-requests/{id}
```
**Status Flow:**
```
AWAITING_APPROVAL → AWAITING_FUNDS → PROCESSING → SUCCEEDED
```
***
## User Management
### Invite Users
```bash theme={null}
POST /v1/company/users/invite
Content-Type: application/json
{
"email": "user@example.com",
"role": "COMPANY_MANAGER",
"firstName": "John",
"lastName": "Doe"
}
```
### User Roles
| Role | Permissions |
| - | - |
| **COMPANY\_ADMIN** | Full access including user management |
| **COMPANY\_MANAGER** | Create and manage ramp requests |
| **COMPANY\_VIEWER** | Read-only access |
### Update User Role
```bash theme={null}
PUT /v1/company/users/{userId}/role
Content-Type: application/json
{
"role": "COMPANY_ADMIN"
}
```
***
## Best Practices
### Authentication
**Token Management**: Implement token refresh logic to handle expired tokens gracefully.
```javascript theme={null}
// Example: Token refresh logic
async function getAccessToken() {
if (tokenExpired()) {
return await refreshToken();
}
return currentToken;
}
```
### Error Handling
**Optimistic Locking**: Always handle HTTP 409 conflicts by fetching the latest version and retrying.
```javascript theme={null}
async function updateResource(id, data) {
try {
return await api.patch(`/resource/${id}`, data);
} catch (error) {
if (error.status === 409) {
// Fetch latest version and retry
const latest = await api.get(`/resource/${id}`);
data.version = latest.version;
return await api.patch(`/resource/${id}`, data);
}
throw error;
}
}
```
### Pagination
**Efficient pagination**: list endpoints are page-numbered — pass `page` and `size`. Keep `size` modest and walk the pages rather than requesting one large response.
```bash theme={null}
GET /v1/ramp-requests?page=1&size=50&sortOn=createdAt&sortOrder=DESC
```
### Tracking status
Read a ramp request back to follow its progress —
[Get ramp request by ID](/api-reference/Fundflow-API/ramp-requests/get-ramp-request-by-id). Poll at a
sensible interval rather than in a tight loop.
***
## Testing
### Staging Environment
Use the staging environment for testing:
* Test OAuth2 authentication
* Create test ramp requests
* Test error scenarios
### Test Scenarios
1. **Successful On-Ramp**: Create, approve, and complete an on-ramp request
2. **Successful Off-Ramp**: Create, approve, and complete an off-ramp request
3. **Cancellation**: Create and cancel a ramp request
4. **Rejection**: Create and reject a ramp request
5. **Version Conflict**: Test optimistic locking behavior
***
## Common Integration Patterns
### Pattern 1: Automated On-Ramp
```javascript theme={null}
// 1. Create ramp request
const request = await createRampRequest({
rampType: 'ON_RAMP',
amount: 1000,
fiatCurrencyId: eurId,
cryptoCurrencyId: usdcId,
companyWalletId: walletId
});
// 2. Auto-approve if within limits
if (request.amount <= autoApprovalLimit) {
await approveRampRequest(request.id, request.version);
}
// 3. Read the request back to follow its status
```
### Pattern 2: Working through pending requests
There is no batch endpoint — this lists the pending requests and approves them one at a time.
```javascript theme={null}
// Work through the requests awaiting approval
const requests = await listRampRequests({
status: 'AWAITING_APPROVAL',
page: 1,
size: 100
});
for (const request of requests.result) {
if (shouldApprove(request)) {
await approveRampRequest(request.id, request.version);
}
}
```
***
## Security Considerations
**Never expose:**
* OAuth2 client secrets
* Access tokens in client-side code
* Private keys or seed phrases
* API credentials in version control
### Secure Storage
* Store credentials in environment variables or secure vaults
* Use HTTPS for all API communications
* Implement proper access controls
* Rotate credentials regularly
### API Scopes
Request only the OAuth2 scopes you need:
* `view:ramp-request` - View ramp requests
* `create:ramp-request` - Create ramp requests
* `approve:ramp-request` - Approve ramp requests
* `manage:company-wallet` - Manage wallets
* `manage:company-bank-account` - Manage bank accounts
***
## Next Steps
Explore full API documentation
Manage bank accounts & wallets
Create and manage ramp requests
Understand fee structure
Security best practices
Quick API overview
***
## Support
Need help with integration?
* **Contact**: [venlyfinance.com/contact](https://venlyfinance.com/contact)
* **API Status**: Check system status for any ongoing issues
# Glossary
Source: https://docs.venlyfinance.com/guides/payments/glossary
Definitions for ramp requests, fees, KYC/KYB, on-ramp, off-ramp, and other Fundflow concepts you'll see in the docs and dashboard.
## Platform Terms
### 2FA (Two-Factor Authentication)
Security feature requiring password plus time-based code from authenticator app.
### Account Verification
Process of confirming ownership of bank accounts or wallets.
### Business Approval
KYB verification confirming business legitimacy before full platform access.
### Dashboard
Main overview page showing metrics, account status, and quick actions.
## Transaction Terms
### On-Ramp
Converting fiat currency into cryptocurrency.
### Off-Ramp
Converting cryptocurrency into fiat currency.
### Reference Code
Unique identifier for on-ramp transactions that must be included in bank transfers.
### Transaction ID
Unique identifier for any transaction in the platform.
### Transaction Status
Current state of a ramp request: `AWAITING_APPROVAL`, `AWAITING_FUNDS`, `PROCESSING`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `REJECTED`, `DENIED`, `BLOCKED`.
## Banking Terms
### IBAN
International Bank Account Number - standardized bank account identifier.
### Bank Transfer
Moving money from one bank account to another.
### Beneficiary
The person or business receiving a bank transfer.
## Blockchain & Crypto Terms
### Blockchain
Distributed digital ledger that records cryptocurrency transactions.
### Wallet
Digital account for storing, sending, and receiving cryptocurrency.
### Wallet Address
Unique identifier for a crypto wallet. The format depends on the chain — EVM chains use `0x` followed by 40 hexadecimal characters, and non-EVM chains use their own encoding.
### Private Key
Secret code proving wallet ownership. Never share this.
### Seed Phrase
Recovery phrase for a wallet. Never share this.
### Gas Fees
Transaction fees paid to blockchain validators.
### Blockchain Network
The specific blockchain infrastructure (Ethereum, Polygon, etc.).
### Confirmation
Validation of a transaction on the blockchain.
## Cryptocurrency Terms
### Stablecoin
Cryptocurrency pegged to a stable asset to minimize price volatility.
### USDC (USD Coin)
Stablecoin pegged to USD, issued by Circle.
### EURC (Euro Coin)
Stablecoin pegged to EUR.
### ETH (Ethereum)
Native cryptocurrency of the Ethereum blockchain.
### POL (Polygon)
Native cryptocurrency of the Polygon network.
## Compliance Terms
### KYB (Know Your Business)
Verification process to confirm business identity and legitimacy.
### KYC (Know Your Customer)
Identity verification process for individuals.
### AML (Anti-Money Laundering)
Regulations to prevent money laundering.
### Compliance Monitoring
Automated checks on transactions to ensure regulatory compliance.
## Exchange Terms
### Exchange Rate
Ratio at which one currency converts to another.
### Rate Lock
Exchange rate is fixed when transaction is confirmed.
## Status Terms
### Verified
Account passed verification and is ready to use.
### Active
Account is operational and ready for transactions.
### Pending
Awaiting review, confirmation, or processing.
### Failed
Transaction or verification encountered an error.
### In Progress
Process is currently being reviewed.
## Abbreviations
| Abbreviation | Full Term |
| - | - |
| 2FA | Two-Factor Authentication |
| AML | Anti-Money Laundering |
| IBAN | International Bank Account Number |
| KYB | Know Your Business |
| KYC | Know Your Customer |
| EUR | Euro |
| USD | United States Dollar |
| GBP | British Pound |
| ETH | Ethereum |
| POL | Polygon |
## Support Contacts
* **Contact us**: [venlyfinance.com/contact](https://venlyfinance.com/contact)
* **Platform**: [venlyfinance.com](https://venlyfinance.com)
## Still Have Questions?
Explore documentation
Get in touch with our team
# Quick Reference Guide
Source: https://docs.venlyfinance.com/guides/payments/quick-reference
Cheat sheet for Fundflow — base URLs, auth flow, ramp request lifecycle, and the most-used endpoints on a single page.
## Platform Overview
Venly Fundflow enables businesses to convert between fiat and crypto through a comprehensive API with enterprise-grade security and compliance.
## Key Concepts
Fiat → Crypto conversion
Crypto → Fiat conversion
## Supported Currencies
### Fiat Currencies
**EUR, USD, GBP** - via multiple payment networks (SEPA, ACH, SWIFT, FPS, CHAPS)
### Cryptocurrencies
* **USDC** (Ethereum)
* **EURC** (Ethereum)
* **USDS** (Ethereum)
* **ETH** (Ethereum)
* **POL** (Polygon)
### Blockchain Networks
Ethereum, Polygon, Base, Arbitrum, Sui
## Quick Start Flow
Obtain OAuth2 access token via Venly Identity Platform
Verify company KYB status: `GET /v1/company`
Create company bank accounts: `POST /v1/company-bank-accounts`
Status: PENDING → VERIFIED (manual review)
Register company wallets: `POST /v1/company-wallets`
Status: PENDING → VERIFIED
Create on-ramp or off-ramp: `POST /v1/ramp-requests`
## Ramp Request Lifecycle
```text theme={null}
AWAITING_APPROVAL → AWAITING_FUNDS → PROCESSING → SUCCEEDED
↓ ↓
CANCELLED REJECTED
REJECTED DENIED
DENIED BLOCKED
BLOCKED
```
## Bank Account Types
| Type | Region | Required Fields |
| - | - | - |
| **EUR\_SEPA** | Europe | IBAN, BIC |
| **USD\_WIRE** | US | Account #, Routing # |
| **USD\_ACH** | US | Account #, Routing # |
| **USD\_SWIFT** | US | BIC, Account #, Bank address |
| **GBP\_FPS** | UK | Account #, Sort code |
| **GBP\_CHAPS** | UK | Account #, Sort code |
| **OTHER\_SWIFT** | International | Currency, BIC, IBAN/Account # |
## Common API Endpoints
### Authentication
```bash theme={null}
# Staging token endpoint
POST https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token
# Production token endpoint
POST https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token
```
### Company & Users
```bash theme={null}
GET /v1/company
GET /v1/company/users
POST /v1/company/users/invite
```
### Bank Accounts
```bash theme={null}
GET /v1/company-bank-accounts
POST /v1/company-bank-accounts
GET /v1/company-bank-accounts/{id}
PATCH /v1/company-bank-accounts/{id}
```
### Crypto Wallets
```bash theme={null}
GET /v1/company-wallets
POST /v1/company-wallets
GET /v1/company-wallets/{id}
PATCH /v1/company-wallets/{id}
```
### Ramp Requests
```bash theme={null}
GET /v1/ramp-requests
POST /v1/ramp-requests
GET /v1/ramp-requests/{id}
POST /v1/ramp-requests/{id}/approve
POST /v1/ramp-requests/{id}/reject
POST /v1/ramp-requests/{id}/cancel
PUT /v1/ramp-requests/{id}/amount
PATCH /v1/ramp-requests/{id}/tx-hash
```
### Currency Pairs
```bash theme={null}
GET /v1/ramp-requests/on-ramp/pairs
GET /v1/ramp-requests/off-ramp/pairs
```
### Fees
```bash theme={null}
GET /v1/fees
POST /v1/fees/calculate
```
### Reference Data
```bash theme={null}
GET /v1/fiat-currencies
GET /v1/crypto-currencies
GET /v1/chains
GET /v1/bank-accounts/config
```
## On-Ramp Example
```bash theme={null}
# 1. Get available pairs
GET /v1/ramp-requests/on-ramp/pairs
# 2. Create request
POST /v1/ramp-requests
{
"rampType": "ON_RAMP",
"amount": 1000.00,
"fiatCurrencyId": "eur-id",
"cryptoCurrencyId": "usdc-id",
"companyWalletId": "wallet-id"
}
# 3. Approve request
POST /v1/ramp-requests/{id}/approve
{
"version": 1
}
# 4. Send fiat from company bank account to Venly deposit account
# (use depositBankAccount details from response + paymentReference)
# 5. System processes and sends crypto to company wallet
```
## Off-Ramp Example
```bash theme={null}
# 1. Get available pairs
GET /v1/ramp-requests/off-ramp/pairs
# 2. Create request
POST /v1/ramp-requests
{
"rampType": "OFF_RAMP",
"amount": 1000,
"cryptoCurrencyId": "usdc-id",
"fiatCurrencyId": "eur-id",
"companyBankAccountId": "bank-id"
}
# 3. Approve request
POST /v1/ramp-requests/{id}/approve
{
"version": 1
}
# 4. Send crypto from company wallet to Venly deposit wallet
# (use depositWallet address from response)
# 5. Add transaction hash
PATCH /v1/ramp-requests/{id}/tx-hash
{
"blockchainTransactionHash": "0x...",
"version": 2
}
# 6. System processes and sends fiat to company bank account
```
## User Roles
| Role | Permissions |
| - | - |
| **COMPANY\_ADMIN** | Full access + user management |
| **COMPANY\_MANAGER** | Create/manage ramp requests |
| **COMPANY\_VIEWER** | Read-only access |
## Optimistic Locking
All update operations require a `version` field:
```jsonc theme={null}
{
"version": 1,
// ... other fields
}
```
**Version conflict (HTTP 409)?** → Fetch latest version and retry
## Status Codes
| Code | Meaning |
| - | - |
| **200** | Success |
| **201** | Created |
| **400** | Bad Request |
| **401** | Unauthorized |
| **403** | Forbidden |
| **404** | Not Found |
| **409** | Version Conflict |
| **500** | Server Error |
## Pagination
```bash theme={null}
GET /v1/ramp-requests?page=1&size=50&sortOn=createdAt&sortOrder=DESC
```
**Response includes:**
* `pagination`: Page info
* `sort`: Sort configuration
* `result`: Array of items
## Filtering
```bash theme={null}
# Filter ramp requests
GET /v1/ramp-requests?rampType=ON_RAMP&status=AWAITING_APPROVAL&fromDate=2024-01-01&toDate=2024-12-31
# Filter wallets
GET /v1/company-wallets?chain=ETHEREUM&verificationStatus=VERIFIED
# Filter bank accounts
GET /v1/company-bank-accounts?verificationStatus=VERIFIED
```
## Security Best Practices
**Never expose:**
* OAuth2 access tokens
* Client secrets
* Private keys
* Seed phrases
✅ **Do:**
* Use OAuth2 with appropriate scopes
* Implement token refresh
* Store credentials securely
* Use HTTPS only
## Common Issues
**Version conflict (409)?**
→ Fetch latest resource and retry with new version
**Request stuck in AWAITING\_FUNDS?**
→ Verify payment was sent with correct reference
**Wallet verification failed?**
→ Check verification process requirements
**Cannot create ramp request?**
→ Ensure company KYB is VERIFIED and accounts are VERIFIED
## Rate Limits
Follow standard API rate limiting practices:
* Implement exponential backoff
* Cache reference data
* Use pagination efficiently
## Quick Links
Complete setup guide
Full API documentation
Bank accounts & wallets
Ramp requests guide
Fee structure
Security best practices
## Support
**Need help?** [Contact us](https://venlyfinance.com/contact)
# Security & Best Practices
Source: https://docs.venlyfinance.com/guides/payments/security
Credential management, token security, and defensive patterns for Fundflow API integrations
## Credentials
Your **Client ID** and **Client Secret** are the root of access to both APIs. Treat them like passwords.
Never hard-code credentials in source code, commit them to version control, or include them in client-side bundles. A leaked secret provides full API access under your account.
**Do:**
* Store credentials in environment variables (`CLIENT_ID`, `CLIENT_SECRET`)
* Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) in production
* Rotate secrets on a regular schedule and immediately if a leak is suspected
* Use separate credentials for staging and production
**Don't:**
* Log credential values anywhere
* Pass secrets through URL query parameters
* Share credentials between team members — provision individual service accounts where possible
***
## Token Security
Access tokens are short-lived (5 minutes) Bearer tokens. They are equivalent to session credentials.
* Never log token values — they grant full API access for their lifetime
* Do not store tokens persistently (database, disk) — request a fresh one as needed
* After a `401 Unauthorized` response, re-authenticate once and retry; do not retry indefinitely
* Use HTTPS on all requests — tokens sent over plain HTTP can be intercepted
***
## Transport Security
All API and token endpoint traffic must use **HTTPS**. The endpoints enforce TLS — plain HTTP connections are rejected.
Verify your HTTP client does not disable certificate validation. Disabling it (e.g. `verify=False` in Python requests) exposes you to man-in-the-middle attacks in production.
***
## Optimistic Locking
Fundflow uses **optimistic locking** to prevent race conditions on shared resources. Every mutable resource carries a `version` integer.
Include the current `version` on all update requests:
```json theme={null}
{
"version": 2,
"amount": 1500.00
}
```
If two processes update the same resource concurrently, the second write returns **HTTP 409 Conflict**:
```json theme={null}
{
"success": false,
"errors": [{
"code": "OPTIMISTIC_LOCK_EXCEPTION",
"message": "The resource has been modified. Please fetch the latest version and retry."
}]
}
```
On a 409, fetch the resource again to get the latest `version`, apply your changes, and retry.
***
## Idempotency (Finance API)
Relevant Finance API endpoints accept an `idempotencyKey` field in the request body — a UUID you generate per request:
```json theme={null}
{
"idempotencyKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"...": "other fields"
}
```
Use a fresh UUID for each new logical operation. On a network failure, retry with the **same key** — the API returns the original response without creating a duplicate.
Re-using a key with a different request body returns **HTTP 422**.
***
## Environment Isolation
Never mix staging and production credentials or base URLs. Maintain separate configuration for each environment and gate the switch explicitly — do not infer the environment from other runtime state.
```bash theme={null}
# Staging
TOKEN_URL=https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token
FUNDFLOW_BASE_URL=https://api-fundflow-staging.venly.io/v1
FINANCE_BASE_URL=https://api-staging.venlyfinance.com/v1
# Production
TOKEN_URL=https://login.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token
FUNDFLOW_BASE_URL=https://api-fundflow.venly.io/v1
FINANCE_BASE_URL=https://api.venlyfinance.com/v1
```
***
## Audit Logging
Log the following for every API call your system makes:
* Timestamp
* HTTP method and path (no query strings containing sensitive data)
* HTTP status code
* Request ID from the response (if present)
* Duration
Do **not** log request bodies containing credentials, tokens, or PII.
***
## Reporting Security Issues
**Contact:** [venlyfinance.com/contact](https://venlyfinance.com/contact) — response within 2 hours for security issues.
API error codes and common fixes
Token management and refresh patterns
# Transactions
Source: https://docs.venlyfinance.com/guides/payments/transactions
Create, approve, and track Fundflow ramp requests — the lifecycle of an on-ramp or off-ramp transaction from initiation to settlement.
## Overview
Ramp requests are the core transactions in Fundflow that enable conversion between fiat currencies and cryptocurrencies.
**Fiat → Crypto**: Convert fiat currency to cryptocurrency
**Crypto → Fiat**: Convert cryptocurrency to fiat currency
***
## Prerequisites
Before creating ramp requests, ensure you have:
✅ **Company verified** (KYB completed)\
✅ **Bank account verified** (for receiving/sending fiat)\
✅ **Crypto wallet verified** (for receiving/sending crypto)\
✅ **Appropriate user role** (COMPANY\_ADMIN or COMPANY\_MANAGER)
***
## Ramp Request Lifecycle
### Status Flow
```mermaid theme={null}
graph LR
A[AWAITING_APPROVAL] --> B[AWAITING_FUNDS]
A --> C[CANCELLED]
A --> D[REJECTED]
A --> E[DENIED]
A --> F[BLOCKED]
B --> G[PROCESSING]
G --> H[SUCCEEDED]
G --> I[FAILED]
```
| Status | Description | Next Actions |
| - | - | - |
| **AWAITING\_APPROVAL** | Request created, pending approval | Approve, Reject, or Cancel |
| **AWAITING\_FUNDS** | Approved, waiting for payment | Customer sends payment |
| **PROCESSING** | Payment received, conversion in progress | Wait for completion |
| **SUCCEEDED** | Transaction completed successfully | None |
| **FAILED** | Transaction failed | Review and retry |
| **CANCELLED** | Cancelled by requester | None |
| **REJECTED** | Rejected by company admin | None |
| **DENIED** | Denied by Venly admin | [Contact us](https://venlyfinance.com/contact) |
| **BLOCKED** | Blocked by Venly admin | [Contact us](https://venlyfinance.com/contact) |
### On-Ramp Lifecycle
The actor sequence that drives an on-ramp request from creation to settlement:
```mermaid theme={null}
sequenceDiagram
autonumber
participant Dev as Your App
participant API as Fundflow API
participant Customer
participant Chain as Crypto Network
Dev->>API: POST /ramp-requests (rampType=ON_RAMP)
API-->>Dev: rampRequestId, status=AWAITING_APPROVAL
Dev->>API: POST /ramp-requests/{id}/approve
API-->>Dev: status=AWAITING_FUNDS, bankReference
Customer->>API: SEPA/wire transfer with bankReference
API->>API: Match reference, convert at FX rate
API->>Chain: Send crypto to company wallet
API-->>Dev: status=SUCCEEDED, txHash
```
### Off-Ramp Lifecycle
The actor sequence for an off-ramp request — note the developer-provided `blockchainTransactionHash` step:
```mermaid theme={null}
sequenceDiagram
autonumber
participant Dev as Your App
participant API as Fundflow API
participant Chain as Blockchain
participant Bank as SEPA/SWIFT
Dev->>API: POST /ramp-requests (rampType=OFF_RAMP)
API-->>Dev: rampRequestId, status=AWAITING_APPROVAL
Dev->>API: POST /ramp-requests/{id}/approve
API-->>Dev: status=AWAITING_FUNDS, depositAddress
Dev->>Chain: Send crypto to depositAddress
Dev->>API: PATCH /ramp-requests/{id}/tx-hash
Chain->>API: On-chain confirmation
API->>API: Convert at FX rate
API->>Bank: Fiat credit to company bank
API-->>Dev: status=SUCCEEDED
```
***
## Creating On-Ramp Requests
### On-ramp process overview
Use the [Create Ramp Request](/api-reference/Fundflow-API/ramp-requests/create-a-new-ramp-request) endpoint
```json theme={null}
{
"rampType": "ON_RAMP",
"amount": 1000.00,
"fiatCurrencyId": "uuid-of-eur",
"cryptoCurrencyId": "uuid-of-usdc",
"companyWalletId": "uuid-of-your-wallet"
}
```
The request is created in **AWAITING\_APPROVAL** status
Use the [Approve Ramp Request](/api-reference/Fundflow-API/ramp-requests/approve-a-ramp-request) endpoint
Status changes to **AWAITING\_FUNDS**
You'll receive deposit bank account details with a unique payment reference
Transfer the fiat amount to the provided Venly deposit bank account
**Important**: Include the payment reference in your bank transfer
Once Venly receives the payment, status changes to **PROCESSING**
The system converts fiat to crypto at the current exchange rate
Crypto is sent to your company wallet
Status changes to **SUCCEEDED**
**Always include the payment reference** in your bank transfer. This ensures proper matching of your payment to the ramp request.
### On-Ramp Example
```bash theme={null}
# 1. Get available currency pairs
GET /v1/ramp-requests/on-ramp/pairs
# 2. Create on-ramp request
POST /v1/ramp-requests
{
"rampType": "ON_RAMP",
"amount": 1000.00,
"fiatCurrencyId": "eur-currency-id",
"cryptoCurrencyId": "usdc-ethereum-id",
"companyWalletId": "your-wallet-id"
}
# 3. Approve the request
POST /v1/ramp-requests/{id}/approve
{
"version": 1
}
# 4. Customer sends EUR 1000 to deposit account with payment reference
# 5. System processes and sends USDC to company wallet
```
***
## Creating Off-Ramp Requests
### Off-ramp process overview
Use the [Create Ramp Request](/api-reference/Fundflow-API/ramp-requests/create-a-new-ramp-request) endpoint
```json theme={null}
{
"rampType": "OFF_RAMP",
"amount": 0.5,
"cryptoCurrencyId": "uuid-of-usdc",
"fiatCurrencyId": "uuid-of-eur",
"companyBankAccountId": "uuid-of-your-bank-account"
}
```
The request is created in **AWAITING\_APPROVAL** status
Use the [Approve Ramp Request](/api-reference/Fundflow-API/ramp-requests/approve-a-ramp-request) endpoint
Status changes to **AWAITING\_FUNDS**
You'll receive the Venly deposit wallet address
Transfer the crypto amount from your company wallet to the Venly deposit wallet
Use the [Update Transaction Hash](/api-reference/Fundflow-API/ramp-requests/update-transaction-hash) endpoint
```json theme={null}
{
"blockchainTransactionHash": "0x1234...",
"version": 2
}
```
Once blockchain confirms the transaction, status changes to **PROCESSING**
The system converts crypto to fiat at the current exchange rate
Fiat is sent to your company bank account
Status changes to **SUCCEEDED**
### Off-Ramp Example
```bash theme={null}
# 1. Get available currency pairs
GET /v1/ramp-requests/off-ramp/pairs
# 2. Create off-ramp request
POST /v1/ramp-requests
{
"rampType": "OFF_RAMP",
"amount": 0.5,
"cryptoCurrencyId": "usdc-ethereum-id",
"fiatCurrencyId": "eur-currency-id",
"companyBankAccountId": "your-bank-account-id"
}
# 3. Approve the request
POST /v1/ramp-requests/{id}/approve
{
"version": 1
}
# 4. Customer sends 0.5 USDC to deposit wallet
# 5. Add transaction hash
PATCH /v1/ramp-requests/{id}/tx-hash
{
"blockchainTransactionHash": "0x1234...",
"version": 2
}
# 6. System processes and sends EUR to company bank account
```
***
## Managing Ramp Requests
### Listing Requests
Use the [List Ramp Requests](/api-reference/Fundflow-API/ramp-requests/list-all-ramp-requests) endpoint with filters:
```bash theme={null}
GET /v1/ramp-requests?rampType=ON_RAMP&status=AWAITING_APPROVAL&page=1&size=50
```
**Available filters:**
* `rampType`: ON\_RAMP or OFF\_RAMP
* `status`: Filter by status
* `fromDate`: Start date (YYYY-MM-DD)
* `toDate`: End date (YYYY-MM-DD)
* `paymentReference`: Specific payment reference
* `sortOn`: Field to sort by
* `sortOrder`: ASC or DESC
### Getting Request Details
```bash theme={null}
GET /v1/ramp-requests/{id}
```
Returns complete details including:
* Current status and timestamps
* Amount breakdown (fiat, crypto, fees)
* Exchange rate used
* Associated accounts/wallets
* Complete event history
* Payment information
### Modifying Requests
**Edit amount** (only in AWAITING\_APPROVAL status):
```bash theme={null}
PUT /v1/ramp-requests/{id}/amount
{
"amount": 1500.00,
"version": 1
}
```
**Cancel request** (requester, in AWAITING\_APPROVAL):
```bash theme={null}
POST /v1/ramp-requests/{id}/cancel
{
"version": 1
}
```
**Reject request** (admin, in AWAITING\_APPROVAL):
```bash theme={null}
POST /v1/ramp-requests/{id}/reject
{
"version": 1
}
```
***
## Optimistic Locking
Fundflow uses optimistic locking to prevent concurrent updates:
Every update request must include the current `version` number
If the version has changed (HTTP 409), another user modified the resource
Fetch the latest version and retry your operation
```jsonc theme={null}
// Version conflict response (HTTP 409)
{
"success": false,
"errors": [{
"code": "OPTIMISTIC_LOCK_EXCEPTION",
"message": "The resource has been modified. Please fetch the latest version and retry."
}]
}
```
***
## Fees and Exchange Rates
### Fee Calculation
Fees are company-specific and may include volume tiers. Use the [Calculate Fee](/api-reference/Fundflow-API/fees/calculate-fee-for-a-ramp-request) endpoint:
```bash theme={null}
POST /v1/fees/calculate
{
"amount": 1000.00,
"type": "ON_RAMP"
}
```
Response:
```json theme={null}
{
"success": true,
"result": {
"amount": 10.00,
"percentage": 1.0
}
}
```
### Amount Breakdown
For each ramp request:
* **Fiat Amount**: Total fiat amount (including fees)
* **Fiat Net Amount**: Net fiat after deducting fees
* **Crypto Amount**: Cryptocurrency amount
* **Fiat Fee Amount**: Fee in fiat currency
* **Fee Percentage**: Percentage applied
* **Exchange Rate**: Conversion rate used
***
## Exporting Requests
Export all ramp requests for accounting and auditing:
```bash theme={null}
GET /v1/ramp-requests/export
```
Returns a downloadable file (CSV/Excel) with all transaction data.
***
## User Roles and Permissions
| Role | Create | Approve | Reject | Cancel | View |
| - | - | - | - | - | - |
| **COMPANY\_ADMIN** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **COMPANY\_MANAGER** | ✅ | ❌ | ❌ | ✅ (own) | ✅ |
| **COMPANY\_VIEWER** | ❌ | ❌ | ❌ | ❌ | ✅ |
***
## Best Practices
**Check Currency Pairs**: Always verify available currency pairs before creating requests using the `/on-ramp/pairs` or `/off-ramp/pairs` endpoints.
**Calculate Fees First**: Use the fee calculation endpoint to understand costs before creating large requests.
**Track Payment References**: Save payment references for your records to easily match bank transfers with ramp requests.
**Cannot Cancel After Approval**: Once a request is approved and moves to AWAITING\_FUNDS, it cannot be cancelled. Ensure all details are correct before approving.
**Blockchain Confirmations**: Off-ramp requests require blockchain confirmation before processing. This may take several minutes depending on network congestion.
***
## Troubleshooting
### On-Ramp Issues
**Request stuck in AWAITING\_FUNDS?**
* Verify you sent the exact amount specified
* Confirm you included the payment reference
* Check if the bank transfer has been processed
* [Contact us](https://venlyfinance.com/contact) if payment was sent correctly
**Wrong amount sent?**
* [Contact us](https://venlyfinance.com/contact) immediately
* Do not create a new request
* Provide transaction details for reconciliation
### Off-Ramp Issues
**Request not processing?**
* Verify you added the transaction hash
* Confirm the transaction is confirmed on the blockchain
* Check you sent to the correct deposit wallet address
* Ensure you sent the correct amount and token
**Wrong network used?**
* [Contact us](https://venlyfinance.com/contact) immediately
* Provide transaction hash and details
* Do not send additional transactions
***
## API Reference
Create new on-ramp or off-ramp
View all ramp requests
Approve a ramp request
Get available currency pairs
***
## Next Steps
Understand fee calculations
Manage bank accounts and wallets
Security best practices
# Troubleshooting
Source: https://docs.venlyfinance.com/guides/payments/troubleshooting
API error codes, common integration failures, and how to fix them
## HTTP Error Reference
### 400 Bad Request
The request body or parameters failed validation.
**Common causes:**
* Missing required fields
* Invalid field format (e.g. wrong UUID format, unsupported enum value)
* Finance API endpoint requiring `idempotencyKey` called without it in the request body
Check the `errors` array in the response body for field-level details:
```json theme={null}
{
"success": false,
"errors": [
{ "code": "REQUIRED_FIELD", "field": "fiatCurrencyId", "message": "Field is required" }
]
}
```
***
### 401 Unauthorized
The Bearer token is missing, expired, or malformed.
```json theme={null}
{ "success": false, "errors": [{ "code": "UNAUTHORIZED" }] }
```
**Fix:** Re-authenticate using the token endpoint and retry the request with the new token. Implement proactive refresh — request a new token \~30 seconds before the 5-minute expiry rather than waiting for a 401.
***
### 403 Forbidden
The token is valid but the caller lacks permission for this operation.
**Common causes:**
* User role is insufficient (e.g. `COMPANY_VIEWER` attempting to create a ramp request)
* Company KYB status is not `VERIFIED` — required before creating ramp requests
* Resource belongs to a different company
**Fix:** Check the role assigned to the credentials you are using, and verify `GET /v1/company` returns `kybStatus: "VERIFIED"`.
***
### 404 Not Found
The requested resource does not exist.
**Common causes:**
* Incorrect or stale ID in the URL path
* Resource was deleted
**Fix:** Verify the ID against a list endpoint (`GET /v1/ramp-requests`, `GET /v1/company-wallets`, etc.) to confirm the resource exists.
***
### 409 Conflict — Optimistic Lock
A concurrent modification was detected. Another process updated the resource between your read and write.
```json theme={null}
{
"success": false,
"errors": [{ "code": "OPTIMISTIC_LOCK_EXCEPTION", "message": "..." }]
}
```
**Fix:** Fetch the resource again (`GET /v1/ramp-requests/{id}`), read the current `version`, apply your change with the new version, and retry.
***
### 422 Unprocessable Entity (Finance API)
An `idempotencyKey` value was reused with a different request body.
**Fix:** Generate a new UUID for the new logical request. Only reuse a key when retrying the exact same operation after a network failure.
***
### 500 Internal Server Error
An unexpected error occurred on the server.
**Fix:** Retry with exponential backoff. If the error persists, [contact us](https://venlyfinance.com/contact) with the request timestamp and any request ID from the response headers.
***
## Common Integration Issues
### Ramp request stuck in AWAITING\_FUNDS
**On-ramp:** Confirm the bank transfer was sent for the exact amount specified, and that the unique payment reference was included. Missing the reference is the most common cause — the payment cannot be matched without it.
**Off-ramp:** Confirm the transaction hash has been submitted via `PATCH /v1/ramp-requests/{id}/tx-hash`, and that the blockchain transaction is confirmed (check a block explorer). Processing begins only after on-chain confirmation.
***
### Ramp request not moving to PROCESSING
* Blockchain congestion can delay confirmation — check the transaction status on a block explorer before [contacting us](https://venlyfinance.com/contact).
* For on-ramp, bank transfers can take 1-2 business days depending on the payment network. SEPA same-day cut-offs apply.
***
### Cannot create a ramp request
* `GET /v1/company` must return `kybStatus: "VERIFIED"`.
* The bank account or wallet used must have `verificationStatus: "VERIFIED"`. Accounts in `PENDING` status are not eligible.
* Check your user role — `COMPANY_VIEWER` cannot create requests.
***
### Wallet verification not completing (Fundflow)
Wallet ownership verification requires completing the verification process after submitting the wallet address. [Contact us](https://venlyfinance.com/contact) if the status has not moved from `PENDING` after the process is complete.
***
### 409 on every update attempt
You are sending a stale `version`. Always read the resource immediately before writing to get the current version — do not cache or hardcode version numbers.
***
## Contacting Support
Get in touch — response within 24 hours
Get in touch — response within 2 hours
When contacting support, include:
* Your company ID
* The affected resource ID (ramp request, wallet, etc.)
* The exact error response body
* Timestamps of the failed requests
Credential and token best practices
Status codes and endpoint list
Status values and terminology
# Venly Finance Developer Documentation
Source: https://docs.venlyfinance.com/index
Build with stablecoin settlement orchestration. One API. Multiple rails. Your economics.
## Build with Venly Finance
Cross-border settlement orchestration for high-volume operators. Integrate multi-rail payouts, embedded finance, and card issuance with one API.
## Products
Multi-rail payout orchestration. Route payments through SEPA, SWIFT, stablecoin, or local rails — automatically.
Accounts, wallets, IBANs, KYC/KYB, and transfers — one RESTful API to embed financial infrastructure.
## Quick Start
Make your first API call in under five minutes
Learn how to authenticate with Venly APIs
## Use Cases
Send crypto and have a bank beneficiary receive fiat — settlement in minutes, not days.
Embed accounts, wallets, IBANs, and payouts into your product with one API.
Convert, hold, and route stablecoins with approval workflows and full audit trails.
Skip 2 years of vendor integration. Get accounts, wallets, IBANs, and cards from day one.
## Why Venly Finance?
Seconds to minutes instead of T+2 to T+5
Settle in stablecoins instead of correspondent banking
Automatic routing through best available rail
Licensed and supervised
Independently audited security
EUR, USD, GBP, BRL, PHP, INR, MXN and more
## Resources
Visit our main website
Stay updated with insights
Get in touch with our team