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