Skip to main content
A virtual account is a bank account in your customer’s name that converts every deposit into stablecoins. Each one belongs to an onramp Autoramp: the Autoramp decides which stablecoin the fiat becomes and which wallet receives it. Virtual accounts don’t hold funds. Anyone with the account details can pay into them like a normal bank account, and each payment is converted and delivered to the Autoramp’s wallet. Typical use cases:
  • Freelancers in emerging markets receiving USD or EUR payments
  • Banking apps issuing multi-currency accounts
  • Crypto wallets offering named accounts on local payment rails

Coverage and eligibility

BRL is the only Latin American currency available for virtual accounts. Other currencies aren’t available as virtual accounts. Eligibility is decided per customer at onboarding. Read GET /api/customers/{customer_id}/abilities for the rails a specific customer can use: each rail under fiat_deposit reads Active once a virtual account can be opened on it, and the _thirdparty variant, for example sepa_thirdparty, covers third-party deposits. See Ability Status.

How it works

1

Create an onramp Autoramp

For example, EUR in, USDC on Solana out, delivered to the customer’s registered wallet.
2

Share the account

Once the Autoramp is Approved, deposit_rails holds the account details. Show them to your customer.
3

Funds arrive

The customer, or anyone they share the details with, pays in from their bank app. MoonPay Enterprise converts each deposit and delivers the stablecoin to the wallet.
4

Repeat

The account stays live. Every later deposit converts the same way until you cancel the Autoramp.
The account currency and the stablecoin don’t have to match. A customer can hold a EUR account whose deposits arrive as USDC.

Before you create a virtual account

Set these in your shell first. Every command on this page reuses them.
1

The customer is Active

The customer’s status is Active: terms signed, identification approved, and nothing outstanding in GET /api/customers/{customer_id}/required-signings. Otherwise POST /api/autoramps returns 403 with "Customer is not active". See Onboarding.
2

The rail is Active for this customer

An Active customer can still be waiting on a specific rail. Read GET /api/customers/{customer_id}/abilities and create the Autoramp only once its rail reads Active. A rail that isn’t Active yet returns 422 with "Customer is unable to complete the action. Consult the customer abilities API". Subscribe to the customer_fiat_abilities webhook instead of polling. See Ability Status.
3

The recipient is registered

Register the wallet or bank account that receives the converted funds before you create the Autoramp, and pass the same details in recipient_account:
  • Wallet: see Crypto Addresses. An unknown wallet returns 400 with "Recipient wallet not verified for customer".
  • Bank account: see Fiat Addresses. It must reach Registered. An account that isn’t registered yet returns 400 with "recipient fiat account not 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. Send X-API-Version: 2026-08-01 or later: country is still required, but every customer gets the same global terms. Without the header you get the regional terms for that country instead.
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.
Replace <DD/MM/YYYY> with today’s UTC date before your customer signs the message. A date more than one day away from today is rejected. See Verify a self-hosted crypto address. You can also register a hosted wallet if the wallet is custodied by another VASP.

Create a virtual account

Create an onramp Autoramp with a fiat source. In the dashboard, click New Virtual Account on the Onramp card under Routes. Through the API:
POST /api/autoramps requires an Idempotency-Key header. A missing key returns 400. Only successful responses are cached, so a retry after an error runs again. See Idempotency.
201 Created
The response is trimmed. deposit_rails is empty until the account is provisioned. Poll GET /api/autoramps/{autoramp_id} or listen for register_autoramp_status until status is Approved, then read the account from deposit_rails. See Autoramp Status.
200 OK
The deprecated deposit_account field is left out. Read deposit_rails instead.
Each currency returns different rails and fields. The currency guides show the exact deposit_rails your customer sees:

USD

ACH, wire, SWIFT, and FedNow

EUR

SEPA accounts with an IBAN in the customer’s name

GBP

Sort code and account number for FPS and CHAPS

BRL

PIX key and BR code for individuals

Error response

Client errors return a plain string body. A recipient_account that doesn’t match a wallet you registered for this customer returns 400:
400 Bad Request
See Autoramp for the other errors partners hit most.

Third-party deposits

By default an Autoramp expects your customer to pay from their own bank account. Set source_is_third_party: true when someone else pays in, such as an employer, a client, or a marketplace.
  • The flag is fixed for the life of the Autoramp, and it only applies to onramp Autoramps. To change it, cancel the Autoramp and create a new one.
  • It decides which banking partner opens the account. USD supports it on every rail. EUR third-party accounts have their own eligibility rules. GBP and BRL don’t support it.
  • Each transaction carries is_third_party, copied from the Autoramp, and third_party_classification, the check on this specific payment: FirstParty, ThirdParty, Inconclusive (a partial name match), or Unknown (no payer name came with the payment).
  • When third_party_classification is set depends on the account. GBP accounts and some EUR accounts set it on every deposit. Other EUR accounts and some USD accounts set it only on Autoramps created with source_is_third_party, and only when the payer’s name comes with the payment. Other USD accounts and PIX deposits never carry it, and it’s never set in sandbox. Treat a missing value as “not evaluated”, not as FirstParty.

A third-party payment on a first-party Autoramp

Only some first-party EUR accounts turn these away. The bank returns a payment from anyone other than your customer. Every other virtual account accepts payments from third parties. To accept third-party EUR payments, create the Autoramp with source_is_third_party: true. See EUR virtual accounts. When a third-party deposit is returned, it goes back to the account that sent it, not to your customer.

Fees on a deposit

Each deposit creates a transaction that reports its fees in the source currency:
  • fee: the total fee charged
  • fee_breakdown: each component, such as transaction_fee, banking_fee for the incoming bank payment, and network_fee for delivering the stablecoin
  • fee_settlement: whether the fee came out of the deposit (deductedImmediately) or goes on your invoice (settledLater)
The Autoramp’s fee profile decides the amounts. See Fees and Fees and Invoices.

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 virtual account, 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. The stablecoin is sent from one of several MoonPay Enterprise wallets, picked per transaction, so the sending address changes over time. Match deliveries on transaction_hash, not on the sender’s address.

Who sent the payment

Each transaction from GET /api/autoramp-transactions or GET /api/autoramp-transactions/ids carries a source object with the sender’s bank details: name, bank_name, iban and bic, account_number and routing_number for US accounts, and account_number and sort_code for UK accounts. Which fields are filled depends on the rail and on what the sender’s bank passed along. PIX deposits have no source. The incoming payment’s reference is on the transaction itself, as inbound_payment_reference.

Transaction status reference

Your webhook endpoint must return any 2xx status to acknowledge receipt. Any other status, a timeout, or a connection error triggers a retry with exponential backoff. See Webhooks for signature verification and full implementation details.

When a deposit doesn’t convert

  • No transaction shows up. The funds usually haven’t reached MoonPay Enterprise yet. See When No Transaction Appears.
  • The transaction is in FundsReviewInProgress. It’s held for a compliance or fraud check. See Transaction in Reviewing Funds.
  • The transaction is RejectedMinAmount. The deposit was below the minimum. See Limits and Minimums.
  • The deposit came back to the sender with no transaction. It matched no live Autoramp. That happens when the Autoramp is cancelled or in EditPending, the customer is suspended or archived, a pay-by-reference deposit has no matching reference, the payment is in a currency other than the account’s, or a deposit on a quote-based Autoramp matches no live quote.
Two cases aren’t returned automatically: unmatched ACH deposits to some USD accounts and PIX deposits. Returned deposits aren’t exposed through the API. If a payment left the sender’s account and never produced a transaction, contact support with the amount, currency, date, and reference used. In production, only a real bank transfer funds a virtual account. To test without one, simulate a deposit in sandbox below.

Account holder name

The account is issued in your customer’s verified name, from their identification. It appears as beneficiary_name on EUR and GBP rails and as recipient_name on USD rails. Senders should use that name when they pay in. If the name is wrong or cut short, contact support with the customer ID. Corrections are made by hand, at MoonPay Enterprise and at the bank that holds the account.

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.
On a quote-based Autoramp, set amount to the quote’s exact amount_in, as a real deposit must. Any other amount returns 400 because production would return that deposit. fee, fx_rate, and amount_out come from the quote and return 400 if you send them.
2

Drive the transaction to Completed

The simulated transaction starts in PendingPayin, which reads as ConversionInProgress in transaction_status. Advance it to trigger the rest of the status webhooks.
3

Check the webhooks you received

Creating the transaction sent a transaction event and a transaction_status event with ConversionInProgress. Advancing it sent a second transaction_status event with Completed. The sandbox writes the state you ask for straight onto the transaction, so it skips the intermediate PayoutInProgress status that a live deposit produces.If the customer has a pending EDD review, or the deposit takes the customer over the sandbox volume threshold, the transaction starts in FundsReviewInProgress instead. Approve the pending identification before you advance it.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.

Autoramp

Edit, cancel, fees, and limits for every Autoramp

Payouts

The reverse Autoramp: stablecoins to a bank account

Quotes

Lock a rate instead of converting at the current market rate

Limits and Minimums

Per-customer and per-transaction thresholds