- 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
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
Create an onramp Autoramp
Share the account
Approved, deposit_rails holds the account details. Show them to your customer.Funds arrive
Repeat
Before you create a virtual account
Set these in your shell first. Every command on this page reuses them.The customer is Active
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.The rail is Active for this customer
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.The recipient is registered
recipient_account:- Wallet: see Crypto Addresses. An unknown wallet returns
400with"Recipient wallet not verified for customer". - Bank account: see Fiat Addresses. It must reach
Registered. An account that isn’t registered yet returns400with"recipient fiat account not verified".
Onboard a customer from scratch
Full onboarding walkthrough (Sandbox)
Full onboarding walkthrough (Sandbox)
- 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
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.id. You need it in every subsequent step.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.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.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.url to your customer to complete verification. Save the identification id.Approve the identification (Sandbox only)
POST /api/sandbox/identification/{id}In Sandbox, you control whether an identification is approved or rejected.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.SigningsRequired whenever MoonPay Enterprise publishes a new document, and cannot transact until they sign it. See Signing new terms later.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.<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.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.
Full response from GET /api/autoramps/{autoramp_id} once Approved
Full response from GET /api/autoramps/{autoramp_id} once Approved
deposit_account field is left out. Read deposit_rails instead.deposit_rails your customer sees:
USD
EUR
GBP
BRL
Error response
Client errors return a plain string body. Arecipient_account that doesn’t match a wallet you registered for this customer returns 400:
Third-party deposits
By default an Autoramp expects your customer to pay from their own bank account. Setsource_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, andthird_party_classification, the check on this specific payment:FirstParty,ThirdParty,Inconclusive(a partial name match), orUnknown(no payer name came with the payment). - When
third_party_classificationis 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 withsource_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 asFirstParty.
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 withsource_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 chargedfee_breakdown: each component, such astransaction_fee,banking_feefor the incoming bank payment, andnetwork_feefor delivering the stablecoinfee_settlement: whether the fee came out of the deposit (deductedImmediately) or goes on your invoice (settledLater)
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 theTransaction 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: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 atransaction 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 receivetransaction_status events. Use transaction_status (not the deprecated status field) for the current state:
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 fromGET /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
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.
Account holder name
The account is issued in your customer’s verified name, from their identification. It appears asbeneficiary_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.Simulate an incoming deposit
POST /api/sandbox/transaction builds a transaction directly from the route, reusing its currencies, customer, and recipient. amount_in, amount_out, currency_in, currency_out, and state. See Sandbox for the optional fee, fx_rate, and initial_state fields.Drive the transaction to Completed
PendingPayin, which reads as ConversionInProgress in transaction_status. Advance it to trigger the rest of the status webhooks.Check the webhooks you received
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.Create the route
Authorize the route
"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.Create the simulated deposit
- Customer: search by name, email, or ID and select the customer who owns the route.
- Ramp: pick it from the dropdown. This field still uses the older wording for a route. Entries are labelled by currency pair, for example .
- Amount: enter the deposit amount in the route’s input currency.
Settle the transaction
Check the result
Move to production
/api/sandbox/* call from your integration. Those endpoints do not exist in production.