Skip to main content
Spin up named virtual fiat bank accounts in USD, EUR, and GBP for your users in minutes. All incoming funds are immediately auto-minted to stablecoins (USDC, EURC, etc.) and delivered directly to users’ self-custodial wallets linked to MoonPay Enterprise. No customer funds ever sit idle in a bank. Typical use cases:
  • Freelancers in emerging markets receiving USD or EUR payments
  • Banking apps issuing multi-currency accounts
  • Crypto/DeFi wallets offering named accounts with local payment rails
Each virtual account is tied to an autoramp, the standing rule that says which stablecoin the incoming fiat becomes and which wallet receives it. Examples:
  • A vIBAN which turns EUR into USDC on Solana address 1
  • A vIBAN which turns EUR into USDC on Arbitrum address 1
  • A vIBAN which turns EUR into EURC on Arbitrum address 2

Named accounts

Each virtual account is issued in the user’s own name, not a pooled or generic account. Incoming payments clear faster, pass compliance checks more reliably, and feel like a real bank account to the sender. Example: a USD account issued for Jane Doe A sender wiring money to Jane uses these details as they would any US bank account. Once received, MoonPay Enterprise auto-mints the funds to USDC and delivers them to Jane’s self-custodial wallet.

How it works

1
Max sets up a vIBAN with an autoramp which turns EUR into USDC on Solana address 3DkN…BV4Lm
2
Max triggers a EUR 1000 transfer from their banking app
3
MoonPay Enterprise monitors the vIBAN. As soon as funds arrive, USDC is delivered to 3DkN…BV4Lm
4
Max can transfer more funds into this vIBAN at any time and they autoramp to USDC. It is persistent.
A user can hold European bank credentials (IBAN) while having all funds auto-ramped into a USD-denominated stablecoin like USDC. No manual conversion required.

Key concepts

Virtual accounts are payment routing targets, not holding accounts. They’re designed for efficient payment ingestion and reconciliation.
  • Payment routing only: virtual accounts don’t hold funds. Every received payment is immediately processed per your configured autoramp settings.
  • Open to any sender: the account works like a normal bank account. Anyone can send fiat to it using the account credentials. No sender pre-approval is required.
  • Named to your customer: each account is issued in the customer’s verified name, so incoming payments clear faster and pass compliance checks more reliably.
  • Automatic returns: payments that fail compliance checks or arrive with incorrect details are returned to the sender.

Prerequisites

Every step must complete before moving to the next. Sandbox-only steps are marked. Set these in your shell first. Every command on this page reuses them.
1

Customer is Active

Your customer must have Active status: terms signed and identification (KYC/KYB) approved, with nothing outstanding in required-signings. Active alone is not always enough — check that the specific rail you need is Active in the customer’s abilities too.Expand the walkthrough below if you have not onboarded a customer yet.
2

Register the recipient wallet address

Register the destination wallet via Crypto Addresses for Travel Rule compliance. Self-hosted wallets require a signed proof-of-ownership message; hosted wallets require the custodian’s DID.The wallet you pass in recipient_account must match an address you have already registered for this customer.
3

(Optional) Get a quote

If you need a locked rate (and lower-bps fee profiles), request a quote via GET /api/autoramps/quote. See Quotes below. Otherwise, the onramp executes at the current mid-market rate.
4

Create the onramp autoramp

Create the autoramp via POST /api/autoramps using the registered wallet address as the recipient. See the implementation example below.
5

Sandbox: approve the autoramp

An autoramp is created in Authorized status. In Sandbox you advance it yourself, which also provisions the mock deposit account that a simulated deposit needs:
In production, an autoramp reaches Approved on its own once its deposit account is provisioned and verified.

Onboard a customer from scratch

This covers everything before the autoramp: create a customer, sign the terms, complete identification, and register a wallet. See Onboarding for the full lifecycle.Prerequisites:
  • A Sandbox API key from the Partner Dashboard
  • An Ethereum wallet with signing capability (e.g. ethers.js, viem, or a similar library), required to sign a proof-of-ownership message
1

Create a customer

POST /api/customersCreate a customer record with customer_type set to Person (individual) or Business (company). Sending X-API-Version: 2026-08-01 starts the customer in SigningsRequired, so the terms step comes first; without it the customer starts in IdentificationRequired. See API Versions.
Example response:
Save the id. You need it in every subsequent step.
2

Present and sign the terms and conditions

GET /api/customers/{id}/required-signingsFetch and present the terms, then record acceptance via POST /api/customers/{id}/signings. The terms contain the data sharing agreement, so your customer must accept them before MoonPay Enterprise collects KYC data. See Terms and Conditions.GET /api/terms-and-conditions?country={ISO3} returns the same document and is the endpoint to use before a customer record exists. country is required there but no longer affects which document you get back.
Pass the url from the terms response to your customer for review. The signing request takes content_id (the id from the terms response) and signed: true.
3

Create a Link identification

POST /api/customers/{id}/identifications/v2Create an identification with type: "Link". MoonPay Enterprise returns a URL where your customer completes KYC (individuals) or KYB (businesses) through the hosted verification interface.
Example response:
In production, pass the url to your customer to complete verification. Save the identification id.
4

Approve the identification (Sandbox only)

POST /api/sandbox/identification/{id}In Sandbox, you control whether an identification is approved or rejected.
In production, the MoonPay Enterprise compliance team handles this review (typically 24-48 hours).
5

Confirm the customer is Active

GET /api/customers/{id}/required-signingsCheck the customer status after approval. Having signed the terms earlier, it should be Active — nothing is outstanding and you can move on.If the status is SigningsRequired instead, documents are waiting: call required-signings, present each returned document, and mark each as signed. The customer becomes Active once none are left. 2026-08-01 and later return 200 with an empty list when nothing is outstanding; earlier versions return 409 Conflict.
Keep this code path available after onboarding. A customer returns to SigningsRequired whenever MoonPay Enterprise publishes a new document, and cannot transact until they sign it. See Signing new terms later.
6

Register a crypto address

POST /api/addresses/crypto/selfhostedRegister the wallet where stablecoins will be delivered. Required for Travel Rule compliance before creating any autoramp.
The proof message must include today’s date in DD/MM/YYYY format (UTC). You can also register a hosted wallet if the wallet is custodied by another VASP.

Implementation

Create an autoramp that converts EUR to USDC on Ethereum.
To try an onramp end to end without writing API calls, the mp CLI creates one against your own account: mp virtual-account onramp create --fiat EUR --stablecoin USDC --wallet <registered-wallet-address>. See CLI. The API flow below is the one to integrate when you are onramping on behalf of your customers.
POST /api/autoramps requires an Idempotency-Key header. Send the same key to safely retry a request without creating a duplicate autoramp.

Request

Capturing the ID into $AUTORAMP_ID lets you paste the remaining commands on this page straight into the same shell.

Response

deposit_rails is empty at Authorized. Poll GET /api/autoramps/{id} or subscribe to webhooks until status = Approved before sharing deposit details with end users. See Autoramp Status.

Deposit rails

Once the autoramp reaches Approved, the response includes the virtual account under deposit_rails. Display these to your customer so they (or third parties) can send fiat payments.
Each rail is discriminated by type. A EUR account is SEPA, a USD account is one of ACH, Wire, RTP, or FedNow, and a GBP account is CHAPS or FPS. SWIFT, Pix, Spei, MobileMoney, AfricanBankTransfer, and Crypto cover the remaining rails. Fields differ per type; a SEPA rail carries: Virtual accounts are named. No payment references or special deposit instructions are required. Any fiat sent to this account auto-converts to the destination stablecoin and delivers to the registered wallet.
The deposit_rails array contains the payment details. The legacy deposit_account field is deprecated.

Error response

Validation failures return the error as a plain string body, not a structured object. A recipient_account that does not match a wallet address you’ve already registered via Crypto Addresses returns 400:
A 422 covers the cases where the request is well formed but the customer cannot complete the action yet, for example "Customer is unable to complete the action. Consult the customer abilities API", which points you at GET /api/customers/{id}/abilities (see Onboarding).

Lock a rate with a quote

By default an onramp executes at the current mid-market rate. Request a quote first if you need a locked rate. Pegged pairs (EUR to a EUR-stablecoin, USD to a USD-stablecoin) can lock for up to 2 days. Cross-currency pairs such as EUR to USDC lock for up to 10 minutes.
Pass the signed quote verbatim to POST /api/autoramps to create a quote-source autoramp. Quote-source autoramps can keep accepting new quotes over time via POST /api/autoramps/{autoramp_id}/quotes; standalone autoramps created without an initial quote cannot. See Quotes for rate lock caps, expiry policies, and multi-currency input.

Test the full flow in Sandbox

Creating the is half the integration. Simulate a deposit to confirm your webhook handler and reconciliation logic work before you take real money.
1

Simulate an incoming deposit

POST /api/sandbox/transaction builds a transaction directly from the route, reusing its currencies, customer, and recipient.
The response carries amount_in, amount_out, currency_in, currency_out, and state. See Sandbox for the optional fee, fx_rate, and initial_state fields.
2

Drive the transaction to Completed

The simulated transaction starts in Pending. Advance it to trigger the rest of the status webhooks.
3

Check the webhooks you received

Your endpoint should have received a transaction event when the deposit landed, followed by transaction_status events as it moved through FundsReviewInProgress, ConversionInProgress, PayoutInProgress, and Completed.Read transaction_status, not the deprecated status field, for the current state. Payload shapes and the full status table are in Monitoring payments; signature verification is in Webhooks.

Simulate it in the Dashboard

You can run this same flow without writing a single API call. The Partner Dashboard covers every step, from creating the route to settling a simulated deposit.
1

Create the route

Switch the sidebar to the App view and pick your customer in the customer selector at the top. Open Operations → Routes, then click on the card and choose .
2

Authorize the route

Go to Developer → Sandbox and open the Autoramps tab. Your new route appears under Unverified Autoramps. Set its status to Authorized using the dropdown on the right.
The dropdown offers Created, Authorized, and Rejected. Pick Authorized: that provisions the mock deposit account, which is what the simulated deposit needs. Sending "Approved" to PUT /api/sandbox/autoramp/{id} provisions the same account and also moves the route to Approved, so use the API call if you want to see the Approved status your integration waits for.
3

Create the simulated deposit

Still under Developer → Sandbox, click New Transaction in the top right. On the Create New Sandbox Transaction screen:
  1. Customer: search by name, email, or ID and select the customer who owns the route.
  2. Ramp: pick it from the dropdown. This field still uses the older wording for a route. Entries are labelled by currency pair, for example .
  3. Amount: enter the deposit amount in the route’s input currency.
Click Execute Transaction. This bypasses the normal deposit flow and builds the transaction straight from the route configuration.
4

Settle the transaction

Back on the Sandbox page, open the Transactions tab. Your transaction is listed under Pending Transactions with Accept and Reject buttons. Choose Accept to complete it, or use the Change state dropdown to move it to a specific state and test how your integration reacts.
5

Check the result

Open the transaction from Operations → Transactions to see amounts, fees, and . Webhooks fire exactly as they do on the API path, so this is a good way to exercise your endpoint before you write any integration code.
Use Reset on the Sandbox page to clear all customers, wallets, fiat accounts, transactions, and autoramps and start from a clean state.

Move to production

Remove every /api/sandbox/* call from your integration. Those endpoints do not exist in production.

Monitoring payments

Webhooks are the recommended way to track incoming payments. Without them, you would need to poll the API for transaction updates.

Setup

Register a webhook endpoint in the Partner Dashboard under Developer → Webhooks, or use the Webhooks API. Subscribe to at least the Transaction and TransactionStatus topics. Topic names are PascalCase; the type on a delivered event is the snake_case form (transaction, transaction_status).

Relevant events

When a payment arrives at a virtual account, MoonPay Enterprise fires the following events in order: The transaction event tells you a payment arrived. The transaction_status events tell you where it is in the conversion pipeline.

Example: transaction received

When fiat lands on the vIBAN, you receive a transaction event with the customer_id and a transaction id you can use to query details:

Example: transaction status update

As the payment moves through conversion and payout, you receive transaction_status events. Use transaction_status (not the deprecated status field) for the current state:
Once the stablecoin is delivered to the wallet, transaction_status becomes Completed and transaction_hash contains the on-chain transaction hash.

Transaction status reference

Your webhook endpoint must return 200 OK to acknowledge receipt. Non-2xx responses trigger retries with exponential backoff. See Webhooks for signature verification and full implementation details.

Offramp

The reverse route: stablecoins to a bank account

Autoramp Status

Every status an autoramp moves through, and what unblocks each one

Crypto Addresses

Register self-hosted or hosted wallets for Travel Rule compliance

Webhooks

Signature verification, payload schemas, and retry behaviour

Quotes

Lock a rate instead of executing at the current mid-market rate

Limits and Minimums

Per-customer and per-transaction thresholds