# 2FA
Source: https://dev.enterprise.moonpay.com/2fa
Email-based two-factor authentication for sensitive operations like autoramp destination changes, fiat address registration, and customer email changes.
## Overview
MoonPay Enterprise requires email two-factor authentication (2FA) for sensitive actions. This checks that actions cannot be performed on behalf of a customer without their explicit approval.
2FA is required in the following cases:
* Editing the destination address of an Autoramp (fiat or wallet).
* Registering a fiat address that needs authorization.
* Reusing an existing vIBAN or account number (for example, cancelling and re-creating an Autoramp that reuses the same vIBAN).
* Changing a customer's email address.
During customer onboarding, MoonPay Enterprise collects verified email addresses from individuals legally associated with the customer. 2FA emails go to these addresses.
When an action triggers 2FA, the affected entity enters a blocking state until the customer confirms it: a fiat address moves to `AuthorizationRequired`, an Autoramp moves to `EditPending`. See [Autoramp status](/autoramp-status) for the full list of Autoramp states.
## How it works
Your app calls an endpoint that requires 2FA, such as editing an Autoramp destination. The Autoramp moves to `EditPending` and MoonPay Enterprise emails the customer a 2FA code with a confirmation link.
If the customer clicks the confirmation link in the email, the action completes and no further work is needed. If they don't, they can enter the code manually in your app.
Look up the pending authentication code by the entity's ID to get the authentication code ID you need for submission.
```bash bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/authentication-codes/entity/{entity_id}" \
-H "X-API-Key: $API_KEY"
```
Submit the code the customer entered against the authentication code ID from the previous step.
```bash bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/authentication-codes/{auth_code_id}" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{ "code": 123456 }'
```
A successful submission returns the authentication code with `"status": "Confirmed"` and the entity leaves its blocking 2FA state.
## Get the pending authentication code
`GET /api/authentication-codes/entity/{id}`
Returns the pending authentication code for an entity in a blocking 2FA state (`AuthorizationRequired` for a fiat address, `EditPending` for an Autoramp). The `{id}` is the ID of the entity itself (the Autoramp, fiat address, or email change), not the authentication code.
```bash bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/authentication-codes/entity/4b85d15e-f343-41c0-809c-85314cae2fa6" \
-H "X-API-Key: $API_KEY"
```
### Response
```json JSON theme={null}
{
"id": "9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"status": "Pending",
"attempts": 0,
"entity_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"expires_at": "2025-01-15T10:45:00Z"
}
```
| Field | Type | Description |
| - | - | - |
| `id` | UUID | The authentication code ID. Use it to submit the code. |
| `status` | string | One of `Pending`, `Expired`, `Confirmed`, `Rejected` |
| `attempts` | integer | Number of submission attempts so far |
| `entity_id` | UUID | The entity this authentication relates to |
| `expires_at` | timestamp | When the code expires |
If no pending code exists for the entity, this endpoint returns `404 Not Found`. This happens when the code expired or was already confirmed.
## Submit an authentication code
`PUT /api/authentication-codes/{id}`
Submits the code the customer received by email. The `{id}` is the authentication code ID from the previous endpoint.
```bash bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/authentication-codes/9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{ "code": 123456 }'
```
### Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `code` | integer | Yes | The 2FA code from the customer's email |
`code` is an integer, not a string. Send `123456`, not `"123456"`. A string value fails validation.
### Response
Returns the updated authentication code. Check `status` to confirm the result:
```json JSON theme={null}
{
"id": "9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"status": "Confirmed",
"attempts": 1,
"entity_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"expires_at": "2025-01-15T10:45:00Z"
}
```
## Authentication code statuses
| Status | Description |
| - | - |
| `Pending` | The code was sent and is waiting for confirmation. |
| `Confirmed` | The customer confirmed the action. The entity leaves its blocking 2FA state. |
| `Expired` | The code passed its `expires_at` time without confirmation. |
| `Rejected` | The submitted code was wrong too many times. |
## Sub-partner scoping
Both endpoints accept an optional `X-SUB-PARTNER-ID` header. When provided, the authentication code lookup and submission are scoped to that sub-partner.
## Common errors
| Error | Cause |
| - | - |
| `401 Unauthorized` | Missing or invalid `X-API-Key` |
| `404 Not Found` | No pending authentication code for this entity, or the authentication code ID doesn't exist |
| `500 Internal Error` | Returns `{ "message": "...", "trace_id": "..." }`. Reference the `trace_id` when reporting issues. |
The endpoints `POST /api/autoramps/{autoramp_id}/retry-auth` and `POST /api/addresses/fiat/{id}/retry-auth` are deprecated. Don't build on them. If no pending code exists for an entity, re-trigger the underlying action instead.
## Sandbox testing
In sandbox, the static code `123456` confirms any pending authentication. Submit it as an integer:
```json JSON theme={null}
{ "code": 123456 }
```
## Related guides
# API Access
Source: https://dev.enterprise.moonpay.com/api-access
Obtain and manage API keys for authenticating with the MoonPay Enterprise API. Covers Sandbox and Production environments, key creation, and permissions.
To access the MoonPay Enterprise API, you need to include a valid API key with each request. The API key acts as a unique identifier for secure communication between your application and the API.
## Environments
The API offers two environments: Sandbox and Production. The Sandbox environment is designed for testing and experimentation, letting you simulate API interactions without affecting real data. The Production environment handles live operations and requires verified credentials. Throughout this documentation, API endpoint references default to the Sandbox environment.
### Sandbox
* **API Base URL:** `https://api.sandbox.iron.xyz/api`
* **Partner Dashboard:** [https://app.sandbox.iron.xyz](https://app.sandbox.iron.xyz)
### Production
* **API Base URL:** `https://api.iron.xyz/api`
* **Partner Dashboard:** [https://app.enterprise.moonpay.com](https://app.enterprise.moonpay.com)
## Managing API Keys in the Partner Dashboard
You can create and manage API keys in the [Partner Dashboard](/partner-dashboard).
1. Log in to the Partner Dashboard.
2. Navigate to **Developer → API Keys**.
### Creating an API Key
When creating a new API key, you need to provide the following details:
* **Role**: Define the key's permissions:
* **Read Only**: Read-only access.
* **Read / Write**: Read and write access.
* **Label**: A descriptive name to help you identify the key, especially if you manage multiple keys.
### Revoking an API Key
You can revoke an API key at any time through the Partner Dashboard.
## Using an API Key
Once you’ve created an API key, include it in the `X-API-Key` header of each request.
### Example Request
Here’s an example using `curl` to fetch a list of customers:
```bash Bash theme={null}
curl --request GET \
--url https://api.sandbox.iron.xyz/api/customers \
--header 'X-API-Key: ' \
--header 'Accept: application/json; charset=utf-8'
```
Include the `X-API-Key` header in every request. Replace `` with the key you generated in the Partner Dashboard.
Every endpoint under `/api` also reads an optional `X-API-Version` header that pins response shapes to a dated API version. Omit it and you get the default version. See [API Versions](/versioning).
## Rate Limiting
MoonPay Enterprise applies multiple layers of rate limiting for fair usage and system stability. This includes per-API-key limits for authenticated requests as well as infrastructure-level protections against unauthorized access attempts and abuse.
When rate limits are exceeded, the API returns an HTTP `429 Too Many Requests` status code. Your application should handle this response appropriately, typically with exponential backoff retry logic.
If you have specific requirements or anticipate high-volume usage, contact our team to discuss your needs.
## Errors
| Status | Cause |
| - | - |
| 401 | Missing or invalid `X-API-Key` |
| 403 | Partner account is not active |
| 429 | Rate limit exceeded |
Client errors (`401`, `403`, `429`) return a plain JSON string:
```json 401 Unauthorized theme={null}
"Missing or invalid authentication. Please provide X-API-Key"
```
```json 403 Forbidden theme={null}
"Partner is not active. Please contact iron support"
```
```json 429 Too Many Requests theme={null}
"Rate limit exceeded"
```
Server errors (`500`) return `{ "message": "...", "trace_id": "..." }`. Reference the `trace_id` when reporting issues.
## MCP Server
MoonPay Enterprise publishes an MCP server: [`@ironxyz/mcp-server`](https://www.npmjs.com/package/@ironxyz/mcp-server).
Point an MCP-aware editor at it and a single prompt runs end to end. In Cursor:
1. Cursor locates the MoonPay Enterprise MCP server.
2. It retrieves a summary list of all available endpoints and identifies the correct one to use.
3. It fetches the complete OpenAPI specification for the selected endpoint.
4. It implements a full Golang backend based on that spec.
5. It makes a real API call to the sandbox endpoint and returns the live response within the Cursor chat.
# API parity
Source: https://dev.enterprise.moonpay.com/api-parity
The endpoint behind every Customer operations dashboard action, for automating the same workflow programmatically.
Each dashboard action has a matching API endpoint. Once you know which cases come up most for your customers, you can automate the same workflow.
| In the panel | Endpoint |
| - | - |
| See what is outstanding | `GET /api/customers/{id}/requirements` |
| Status, step status, reject comment, EDD flag | `GET /api/identifications/{id}` |
| Mint a verification link to share | `POST /api/customers/{id}/identifications/v2` with `{ "type": "Link" }`. The response carries the `url` to send. |
| Start EDD | The same call with `"with_edd": true` |
| Read the submitted questionnaire | `GET /api/identifications/{id}/compliance-questionnaire` |
| Create new KYC | `POST /api/customers/{id}/identifications/v2` |
| Archive an identification | `PUT /api/identifications/{id}/archive` |
| Terms signing state | `GET /api/customers/{id}/required-signings` and `GET /api/customers/{id}/signings` |
Two panel actions have no public endpoint today: **Upload documents**, and the combined onboarding link that covers verification and terms signing in one URL. Programmatically, mint a verification link with `identifications/v2` and drive signings through the signings endpoints. Everything else in the table is a direct equivalent.
See [Onboarding](/onboarding) for the full API flow, and [Webhooks](/webhooks) to get told when a customer's status changes instead of watching this page.
# Authentication
Source: https://dev.enterprise.moonpay.com/authentication
Authenticate MoonPay Enterprise API requests using the X-API-Key header. Includes example requests, 401 error handling, and common troubleshooting steps.
## Authenticating Requests with API Keys
To make successful requests to the MoonPay Enterprise API, you must authenticate every request by including a valid API key in the request headers. Authentication checks that only authorized users can access resources or perform actions within the API. [Learn how to generate API Keys here.](/api-access)
## Including the API Key
Include your API key in the `X-API-Key` header of every request. If the API key is missing, incorrect, or revoked, the request will fail.
### Example Request
The following example demonstrates how to use your API key with a `GET` request to fetch a list of customers:
```bash Bash theme={null}
curl --request GET \
--url https://api.sandbox.iron.xyz/api/customers \
--header 'X-API-Key: YOUR-API-KEY' \
--header 'accept: application/json; charset=utf-8'
```
```typescript TypeScript theme={null}
const response = await fetch("https://api.sandbox.iron.xyz/api/customers", {
headers: {
"X-API-Key": "YOUR-API-KEY",
"accept": "application/json; charset=utf-8",
},
});
const customers = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.sandbox.iron.xyz/api/customers",
headers={
"X-API-Key": "YOUR-API-KEY",
"accept": "application/json; charset=utf-8",
},
)
customers = response.json()
```
Replace `YOUR-API-KEY` with the actual API key you generated. The `X-API-Key` header is required for every request to authenticate your access. This example uses the Sandbox base URL. For Production, send requests to `https://api.iron.xyz`. See [API Access](/api-access) for both environments.
## Handling Authentication Errors
If the API key is missing, invalid, or revoked, the server responds with a `401 Unauthorized` error. All three cases return the same response:
```bash theme={null}
< HTTP/2 401
< content-type: text/plain; charset=utf-8
< content-length: 59
<
Missing or invalid authentication. Please provide X-API-Key
```
Unlike other API errors, authentication failures return a plain-text body, not JSON. Don't parse the `401` response body as JSON.
### Common Causes of Authentication Errors
1. **Incorrect API Key**: Check that the key matches the one you generated.
2. **Revoked API Key**: Verify that the key has not been revoked.
3. **Missing API Key**: Confirm the `X-API-Key` header is included in your request.
### How to Resolve
* Double-check the API key and its permissions in the Partner Dashboard.
* Generate a new key if necessary and update your requests with the new key.
# Autoramp
Source: https://dev.enterprise.moonpay.com/autoramp
Create standing conversion rules between fiat, crypto, and cross-chain assets. The core primitive powering onramp, offramp, and swap flows.
Use Autoramp to convert funds between rails: fiat, crypto, or across blockchains.
When you create an Autoramp, you define a source asset (for example, EUR in a bank account) and a destination asset (for example, USDC on Solana). You then send funds to the designated source address, and Autoramp automatically exchanges them into the target asset, delivering them to your self-custody wallet or specified bank account.
You can create and manage Autoramps using our API or through the Partner Dashboard. By using this conversion "primitive," you can build the ideal product experience for your customers, eliminating manual steps and simplifying on-ramping, off-ramping, or cross-chain swaps.
## How Autoramp Works
Specify the source and destination assets for your conversion. For example, convert EUR to USDC, or USDC to EUR.
Provide the associated payment instructions (like an IBAN or wallet address) to your customers or teams.
When funds arrive in the source asset, Autoramp automatically converts them into the destination asset and forwards them to the specified account or wallet.
You can treat each Autoramp as a "standing order." After setup, Autoramp continuously monitors all incoming transactions for the specified source asset and performs the conversion without additional steps.
## Common Use Cases
* **On-ramp:** Receive EUR in a bank account and convert it instantly to crypto (for example, USDC on Solana).
* **Off-ramp:** Convert crypto assets (for example, USDC or other stablecoins) directly to fiat and deposit the resulting funds into a linked bank account.
* **Cross-chain swap:** Convert an incoming stablecoin on one chain into another stablecoin or token on a different chain.
* **Automatic payouts:** Provide a single wallet or payment address. Autoramp handles the conversions automatically, so you don’t need to track or execute manual swaps each time.
## Create an Autoramp
You can create an Autoramp in your **Partner Dashboard** or by using our **API**.
Open **Routes**. To create Autoramps programmatically, call `POST /api/autoramps` instead.
Select the **Onramp**, **Offramp**, or **Stable to Stable** card, then choose your source and destination assets.
Choose your fiat currency or crypto asset, and provide the bank account information or wallet address where incoming funds will arrive.
Choose your target asset and specify the bank account or wallet address where you want to receive converted funds.
If you selected a fiat source, copy the IBAN or payment instructions. If you selected a crypto source, copy the unique deposit address.
Send a small test transaction to confirm that funds are converted and delivered as expected.
`POST /api/autoramps` and `POST /api/autoramps/{autoramp_id}/quotes` require an `Idempotency-Key` header. A missing key returns 400. Only successful responses are cached, so a retry after an error runs again. See [Idempotency](/idempotency).
You can create multiple Autoramps for different assets. Each Autoramp has its own unique source information (IBAN or crypto address) and destination.
**Shared EVM deposit address.** An Autoramp gets one deposit address per source chain, and every EVM chain on the same Autoramp shares one address, so the EVM entries in `deposit_rails` repeat the same string. Only the token and chain combinations listed in `source_currencies` are converted: a deposit arriving on an EVM chain you did not declare is not swept, even though the address matches. Declare every chain you want to accept when you create the Autoramp. Non-EVM chains such as Solana each get their own distinct deposit address.
### API: optional pay by reference
Set `pay_by_reference: true` on `POST /api/autoramps` (either standard parameters or a signed quote from `GET /api/autoramps/quote`) to share a single virtual account across multiple Autoramps for the same customer. Each PBR Autoramp returns an `inbound_payment_reference` of the form `IR-XXXXXX` that senders must include verbatim on every inbound transfer for it to route correctly.
PBR is currently supported only when all of the following hold:
* **Kind**: `Onramp` or `Mint`
* **Deposit account type**: `Iban`, `AchWire`, `Swift`, or `ChapsFps`. Pix, SPEI, African bank transfer, and mobile money Autoramps work, but not with pay by reference.
* **Source currencies**: include at least one fiat currency
A PBR-enabled virtual account is exclusive on the inbound side: every deposit must carry a matching reference, otherwise it is returned to sender. When you create a PBR Autoramp, MoonPay Enterprise will not reuse a virtual account that has a live non-PBR Autoramp; you'll either get a fresh VA or share one with another PBR Autoramp for the same customer.
### API: attach quotes over time
A quote-source Autoramp can hold multiple active quotes at once. After creation, request a new signed rate quote with `GET /api/autoramps/{autoramp_id}/quote` and attach it via `POST /api/autoramps/{autoramp_id}/quotes`. Inbound deposits match the newest active quote whose `amount_in` value and source currency match the deposit. See [Quotes](/quotes#attach-a-new-quote-to-an-existing-autoramp).
## Manage Your Autoramps
View and edit Autoramps under **Routes** in the Dashboard, or read them with `GET /api/autoramps` and `GET /api/autoramps/{autoramp_id}`. Each Autoramp entry shows:
* **Source asset and address** (IBAN or crypto wallet)
* **Destination asset and address**
* **Recent conversion activity**
* **Status**, one of the lifecycle values in [Autoramp Status](/autoramp-status)
Cancelling is permanent. `DELETE /api/autoramps/{autoramp_id}` moves the Autoramp to `Cancelled`, a terminal status, and it stops converting deposits. To accept funds again, create a new Autoramp.
## Important Considerations
* **Wallet registration required:** Every wallet address used as a recipient in an Autoramp must be [registered beforehand](/crypto-addresses) for [Travel Rule](/travel-rule) compliance. This applies to all flows: onramp, offramp, and swap.
* **Self-custody:** You remain in control of your wallets and funds.
* **Supported assets:** Confirm that the assets you want to convert are supported by your banking or custody providers.
* **Transaction fees:** Autoramp includes any applicable network and processing fees. Verify fees before sending high-volume transactions.
* **Compliance checks:** We run KYC/AML checks. Source and destination must comply with regulations.
* **Timely settlements:** Funds settle in the destination asset shortly after arrival, reducing operational delays.
# Autoramp Status
Source: https://dev.enterprise.moonpay.com/autoramp-status
This document outlines how to interpret autoramp statuses returned by MoonPay Enterprise.
## Autoramp Status Mapping
| State Type | API Status | Description |
| :- | :- | :- |
| Blocking | `Created` | The autoramp has been created and is waiting for authorization via Two-Factor Authentication. Autoramps that require 2FA for creation (disabled by default) start in this state. |
| Authorized | `Authorized` | The autoramp has been authorized and is waiting for the deposit account to be created and verified. By default (2FA for creation disabled), autoramps start in this state. |
| Blocking | `EditPending` | The autoramp has pending edits that require authorization |
| Progress | `DepositAccountAdded` | A deposit account has been added to the autoramp |
| Progress | `Approved` | The autoramp has been approved and deposit account verified |
| Terminal | `Rejected` | The autoramp has been rejected |
| Terminal | `Cancelled` | The autoramp has been cancelled |
`deposit_rails` is populated once a deposit account is attached, at `DepositAccountAdded`. Earlier states return `deposit_rails: []`. The account isn't verified until `Approved`, so don't share deposit details with end users until the autoramp reaches `Approved`. Poll `GET /api/autoramps/{autoramp_id}` or subscribe to webhooks to detect the transition.
# API changelog
Source: https://dev.enterprise.moonpay.com/changelog
Changes to what the MoonPay Enterprise API accepts, returns, and does, newest first. Covers dated API versions and changes that apply to every version.
This page lists changes to the API contract: new endpoints and fields, changed behavior, and new validation. For new products and features, see [What's new](/whats-new).
There are two kinds of entry:
* **Every version.** Dated by the day it shipped, and applies whatever `X-API-Version` you send, including none. Most are additive. Entries tagged `Action required` add a check that can reject a request that used to succeed.
* **API version entries** are tagged `API version`. Each dated version is a frozen contract: once cut, its response and request shapes never change under you. The entry lists what changed relative to the version before it, and only applies when you send that `X-API-Version`.
Every change also says who it affects: `Business customers`, `Individual customers`, or `All customers`.
`GET /spec/versions` lists the supported versions machine-readably, and each version's full shape is at `/spec/`. See [API Versions](/versioning) for the header itself.
Errors from `POST /api/customers/{id}/identifications/v2` below name the `code` you get on `2026-08-01` or later. Earlier versions return the `message` as a plain string instead.
**You don't need a tax ID for parties in jurisdictions that don't issue one.**
*Relaxed · Individual customers, Business customers*
On `POST /api/customers/{id}/identifications/v2`, when a party's tax residence is a jurisdiction that doesn't issue tax IDs, any `tax_identification_number` you send is ignored and an identifier from the submission is used instead:
* `Person` and person beneficiaries: `identity.identity_number`, from any identity document type.
* Companies, including nested business beneficiaries: `registration_number`.
`company_info.tax_identification_number` is now optional in the schema. Companies registered elsewhere still need it, and a missing one returns `400` [`tin_required`](/errors#tin_required). US persons (US address, US nationality, or a US-issued identity document) are never covered by this. See [Tax ID where none is issued](/kyb#tax-id-where-none-is-issued).
**Beneficiaries tax-resident there need `identity.identity_number`.**
*Action required · Business customers*
A person beneficiary whose role needs a tax ID, and whose `tax_jurisdiction_country_code` is a jurisdiction that doesn't issue tax IDs, must send `identity.identity_number`. Without it the submission returns `400` [`tin_required`](/errors#tin_required), even if you sent `tax_identification_number`.
**`webhook-id` identifies the event and stays the same across retries.**
*Webhooks · All customers*
Every retry of an event now carries the same `webhook-id`, including a retry you trigger from the Partner Area. Earlier, each delivery attempt got a new one. Store the `webhook-id` of each event you process and skip any delivery whose `webhook-id` you've already handled. The value is still a UUID, but no longer a version 4 UUID, so don't validate its version. See [Webhooks](/webhooks#http-headers).
**`intended_use` splits payments into family and third parties.**
| | |
| - | - |
| Affects | Request |
| Kind | Additive (new values, every version), deprecation, version-gated rejection |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Person`, `Token`) |
`intended_use` gains two values: `PaymentToFriendsAndFamily` for payments to people your customer knows, and `PaymentToThirdParties` for payments to anyone else. Both are accepted on every version, including unversioned calls.
`PaymentToFriendsFamilyorOthers` is deprecated. Under this version or later, a submission that sends it is rejected with `400` [`intended_use_deprecated`](/errors#intended_use_deprecated). Earlier and unversioned callers can keep sending it, but should switch to one of the two new values.
**`source_of_wealth` gains six values.**
| | |
| - | - |
| Affects | Request |
| Kind | Additive (new values, every version) |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Person`, `Token`) |
The KYC questionnaire's `source_of_wealth` now also accepts `SaleOfAssetsOrSettlement`, `Pension`, `CompanyProfits`, `SoleProprietorIncome`, `RentalIncome`, and `Inheritance`. They're accepted on every version. See [KYC questionnaire fields](/reliance-kyc-token-sharing#kyc-questionnaire-fields).
**Business beneficiaries declare why they are a UBO.**
| | |
| - | - |
| Affects | Request, validation |
| Kind | Additive fields, version-gated requirement |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
`CompanyBeneficiary` gains `ubo_subtype` and `nature_of_control`. `ubo_subtype` is `Ownership` for an individual who owns part of the company, or `Control` for an individual who controls it without owning it, for example through voting rights or a trust. A `Control` beneficiary sets `nature_of_control` (up to 255 characters) and sends no `share_percentage`.
Under this version or later:
* Every root-level beneficiary with the `UltimateBeneficialOwner` role must set `ubo_subtype`. Nested UBOs can omit it, and accept only `Ownership`. A missing value is rejected with `400` [`ubo_subtype_invalid`](/errors#ubo_subtype_invalid).
* A company other than a sole proprietorship must list at least one person beneficiary with the `UltimateBeneficialOwner` role. A corporate beneficiary doesn't satisfy this. If nobody owns enough to qualify, submit the individual who controls the company as a `Control` beneficiary. Without any, the submission is rejected with [`structure_invalid`](/errors#structure_invalid).
* The ownership disclosure threshold applies to `Ownership` beneficiaries only. A person with `ubo_subtype: "Ownership"` below it is rejected with [`ubo_share_below_threshold`](/errors#ubo_share_below_threshold), including when they hold another role.
* `Control` is accepted only on root-level person beneficiaries of a company that isn't a sole proprietorship.
Earlier and unversioned callers can't send the new fields. A request that includes them is rejected with `ubo_subtype_invalid`. Everything else about their submissions is unchanged. See [Migrating to API Version 2026-10-15](/kyb#migrating-to-api-version-2026-10-15) for the steps and examples.
**`Person` submissions require `tax_residence_country`.**
| | |
| - | - |
| Affects | Validation |
| Kind | Version-gated requirement |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Person`) |
Under this version or later, every `Person` submission must set `tax_residence_country`, whether or not it includes `tax_identification_number`. A missing value is rejected with `400` [`tin_required`](/errors#tin_required). Earlier and unversioned callers can still omit it when no tax ID is sent. See [TIN Format Validation](/tin-format-validation#tax-residence-country).
**UK postal codes on fiat addresses are validated.**
*Action required · All customers*
`POST /api/addresses/fiat` now checks the format of `bank_details.address.postal_code` when the address country is `GB`. A postal code that isn't a valid UK format, such as `LE3-2JB` or `LE3 2J`, is rejected with `400` and the plain-string message `"Invalid UK postal code format"`. A valid code is stored uppercased with a single space, so `le32jb` is saved as `LE3 2JB`. Send the full UK postcode for UK recipients. See [Fiat Addresses](/fiat-addresses#request-fields).
**`wire_message` accepts line breaks.**
*Relaxed · All customers*
On `POST /api/autoramps`, `wire_message` now accepts newline characters (`\n`) alongside letters, digits, spaces, and `/ - ? : ( ) . +`. Use them to split the reference across up to 4 lines of 35 characters. The 140-character limit is unchanged. See [Offramp](/offramp).
**`requires_aml_program`, `estimated_annual_revenue`, and `customer_funds_handling_details` are no longer part of the `Generic` business questionnaire.**
*Changed · Business customers*
On `Business` submissions with `questionnaire.type: "Generic"`, these three fields have been removed from the schema, so you don't need to send them. If you still do, the request is accepted and the values are ignored. The `SoleProp` questionnaire still requires `estimated_annual_revenue`. See [KYB](/kyb).
**Person beneficiaries accept the identity document's expiry date.**
*Added · Business customers*
On `Business` submissions, a person beneficiary's `identity` object accepts an optional `identity_document_valid_until` (`YYYY-MM-DD`). Send it when the document has an expiry date. See [KYB](/kyb).
**Fee profiles accept fractional basis points.**
*Added · All customers*
`POST /api/fee-profiles` accepts `transaction_fee_bips_decimal`, a decimal string such as `"12.5"`. It takes precedence over `transaction_fee_bips` when you send both. Fee responses always include `bips_decimal`. The integer `bips` and `transaction_fee_bips` fields are deprecated and still required on requests. They return the value rounded to the nearest whole bip. See [Fees and invoices](/fees-and-invoices).
**`birth_place` is optional on person beneficiaries, and companies can carry up to 8 documents.**
*Changed · Business customers*
On `Business` submissions, a person beneficiary's `birth_place` is no longer required. When you send it, the same rules apply as before, including `state` for countries with administrative subdivisions. Each company in a submission (the root company, and each nested business beneficiary) can now carry up to 8 documents, up from 6. All other document validation is unchanged. See [KYB](/kyb).
**Customer requirements come back as a structured list.**
| | |
| - | - |
| Affects | Response |
| Kind | New response shape (version-gated), deprecation |
| Endpoints | `GET /api/customers/{id}/requirements` |
Under this version or later, the response sorts every requirement into four lists: `complete`, `pendingReview`, `pending`, and `issues`. It also returns `requirements_due`, `preflight_summary`, `general_review_comment`, and a nullable top-level `identification_id`. Each requirement has a typed `code` and a `description`. A pending questionnaire lists its `missing_questions`.
When migrating, read `pending` for work awaiting customer input (previously `missing`) and `pendingReview` for work awaiting review (previously `pending`). Read every bucket: a customer can have items awaiting review and corrections to make at the same time.
Problems with a submission come back as data, not as an HTTP error. The request returns `200`, and each problem is an entry in `issues` with `retryable`, `review_comment`, `reject_reasons[].error_code`, and per-document `steps`. Read `reject_reasons[].error_code` instead of legacy `reject_labels`, including the reasons in each document step. Switch on `error_code`, not on `description`. See [Verification rejection codes](/errors#verification-rejection-codes) for their meanings.
Show customer-facing feedback from `general_review_comment`, `issues[].review_comment`, and `issues[].steps[].review_comment`. An issue's nullable `retryable` value tells you whether a correction is supported; `null` does not authorize a retry. See [Checking Outstanding Requirements](/onboarding#checking-outstanding-requirements) for the response example and [Displaying Onboarding Comments](/onboarding#displaying-onboarding-comments-to-your-customer) for implementation guidance.
Earlier and unversioned callers keep the previous response shape, and the endpoint is marked deprecated for them. `step_status` on the identification object is deprecated under this version. It keeps working, but read outstanding requirements from this endpoint instead.
**Webhook deliveries send a `User-Agent` header.**
*Webhooks · All customers*
Every webhook delivery now carries `User-Agent: MoonPay-Enterprise-Webhooks/1.0`. Earlier deliveries sent none, which some firewalls and CDNs block. If you allowlist by user agent, match the `MoonPay-Enterprise-Webhooks/` prefix so a version bump doesn't break you. The signature is unchanged. See [Webhooks](/webhooks#http-headers).
**Unsupported company countries return `400`.**
*Changed · Business customers*
`POST /api/customers/{id}/identifications/v2` with a `Business` submission whose company country we don't support now returns `400` with `code` [`country_not_supported`](/errors#country_not_supported) on `X-API-Version` `2026-08-01` or later. Earlier it returned `500`. Check the country against [Countries](/country) before you submit, and don't retry.
**A liveness RFI closes on its own when the new selfie is approved.**
*Changed · All customers*
A `liveness` RFI now moves straight to `closed` when the identification it reset is approved. When that identification is rejected, the RFI moves to `needs_review` as before. If you wait for `needs_review` before treating a liveness RFI as answered, also handle `closed`. See [Information Requests](/rfi).
**List completed RFIs, and `outcome` leaves the `Rfi` object.**
*Added · Changed · All customers*
`GET /api/rfis` takes a new `state` query parameter: `open` (the default) returns RFIs waiting on you or under review, and `completed` returns closed RFIs, newest closure first. The `Rfi` object no longer includes `outcome`. A closed RFI has `status` `closed` and a `closed_at` timestamp, and there's nothing for you to do on it. If your code reads `outcome`, switch to `status`. See [Information Requests](/rfi).
**Steps awaiting review return `Pending`.**
*Changed · All customers*
In `step_status` on an `Identification` (for example from `GET /api/identifications/{id}`), a step your customer has submitted but that hasn't been reviewed yet now returns `{ "result": "Pending", "comment": null, "retry": false }`. Earlier it returned `null`, the same as a step that was never asked for. Steps still waiting on your customer stay `null`, so `step_status` now agrees with `GET /api/customers/{id}/requirements`. If your code treats every non-null step as reviewed, handle `Pending`.
**SWIFT `account_number` must be 1-34 letters, digits, or spaces.**
*Action required · All customers*
Requests that carry a `BankAccountSWIFTIdentifier`, such as `POST /api/addresses/fiat` with a SWIFT account, now reject an `account_number` that is longer than 34 characters, ends in a space, or contains anything other than ASCII letters, digits, and spaces. Invisible formatting characters, such as those pasted from a PDF, fail too. The request returns `400`. Strip the value before you send it. See [Fiat addresses](/fiat-addresses).
**RFIs gain a `liveness` type.**
*Added · All customers*
An RFI with `type` `liveness` asks your customer to redo the liveness check. It carries a `liveness_url` to send them, takes no answers (`POST /api/rfis/{rfi_id}/rounds/{round_id}/response` returns `400`), and moves to `needs_review` once the new identification is approved or rejected. If you switch on `type` with no default branch, add one for `liveness`. See [Information Requests](/rfi).
**AML/CFT policy document and up to 6 documents per company.**
*Added · Business customers*
`AmlCftPolicy` is a new company document type on `Business` submissions. It's always optional and doesn't count toward any required document group. Each company in a submission (the root company, and each nested business beneficiary) can now carry up to 6 documents, up from 4. See [KYB](/kyb).
**`blocking_abilities` shows which rails a customer can't use, and why.**
*Added · All customers*
The `Identification` object and `GET /api/customers/{id}/abilities` gain `blocking_abilities`: a map from a rail (for example `ach` or `sepa_thirdparty`) to the reason that rail is unavailable for the customer. It's always present, and empty when nothing is blocked.
**A `Person` submission is rejected when every deposit rail is blocked.**
*Action required · Individual customers*
When every deposit rail configured for your account is blocked for the applicant, a `Person` submission to `POST /api/customers/{id}/identifications/v2` now returns `400` with one `all_deposit_rails_blocked` entry per rail, and no identification is created. When only some rails are blocked, the submission goes ahead and the blocked rails are listed in `blocking_abilities`. `Business` and `Token` submissions aren't rejected by this check.
**Sandbox transactions price at the live FX rate.**
*Sandbox · All customers*
`POST /api/sandbox/transaction` without `fx_rate` now uses the live mid-market rate for the pair instead of `1.0`, and computes `amount_out` as `(amount - fee) * fx_rate`. If no rate is available for the pair, it returns `500`. See [Sandbox](/sandbox).
**Company address `state` is required for countries with subdivisions.**
*Action required · Business customers*
A `Business` submission whose `company_data.company_info.address` is in a country with ISO 3166-2 administrative subdivisions must include `state`. A missing or blank value returns `400` `invalid_request` ("Address state is required for countries with administrative subdivisions"). The same rule already applied to a beneficiary's `birth_place.state`.
**A non-US `PrivateFoundation` no longer needs a beneficiary with an ownership share.**
*Relaxed · Business customers*
A non-US `Business` with `company_type` `PrivateFoundation` skips the qualifying-owner check. It still needs at least one root `Director`. See [KYB](/kyb).
**Read and answer information requests through the API.**
*Added · All customers*
New endpoints for RFIs:
* `GET /api/rfis` and `GET /api/customers/{customer_id}/rfis` list requests.
* `GET /api/rfis/{rfi_id}` gets one.
* `POST /api/rfis/{rfi_id}/rounds/{round_id}/document-link-opened` records that the document upload link was opened.
* `POST /api/rfis/{rfi_id}/rounds/{round_id}/response` submits answers.
RFIs don't emit webhooks, so poll `GET /api/rfis`. See [Information Requests](/rfi).
**`Token` identifications accept a proof of address.**
*Added · Individual customers*
The `Token` variant of `POST /api/customers/{id}/identifications/v2` takes an optional `documents` array. It accepts one `ProofOfAddress` document: base64 `file`, a `name` with a file extension, 10 MB at most. See [SumSub Token Sharing](/reliance-kyc-token-sharing).
**Two active customers can't share an email.**
*Action required · All customers*
`POST /api/customers` and `PUT /api/customers/{id}` now return `400` when another of your `Active` customers already has the same email address. The comparison ignores case. Customers in any other status don't count.
```json theme={null}
"Email address already in use by another active customer"
```
**UK applicants are rejected at submission when UK onboarding isn't enabled.**
*Action required · All customers*
If UK onboarding isn't enabled for your account, a `Person` or `Business` submission for a UK applicant now fails at `POST /api/customers/{id}/identifications/v2` with `400` [`uk_customers_not_enabled`](/errors#uk_customers_not_enabled). Earlier it was only rejected later, at review.
**`DigitalDriversLicense` identity document type.**
*Added · All customers*
Identity documents accept `DigitalDriversLicense` for Brazil's single-image CNH Digital. Send the front image only. It's accepted when the document's issuing country is Brazil, on `Person` submissions and on `Business` beneficiaries. See [KYC](/kyc).
**`ip_address` must be a public address.**
*Action required · All customers*
A `Person` or `Business` submission to `POST /api/customers/{id}/identifications/v2` now rejects an `ip_address` that isn't globally routable (private, loopback, link-local, CGNAT, documentation, or reserved ranges, IPv4 and IPv6) with `400` `invalid_request` ("IP address is not a globally routable public address"). Send the IP of your customer's device, not your server's internal address. `Token` submissions aren't checked.
**`Miscellaneous` is no longer accepted as an industry.**
*Action required · Business customers*
A `Business` submission with `business_details.industry` `Miscellaneous` is rejected with [`miscellaneous_industry_deprecated`](/errors#miscellaneous_industry_deprecated). Pick the closest specific industry instead. See [KYB](/kyb).
**USD and SWIFT fiat addresses need fewer fields.**
*Relaxed · All customers*
`POST /api/addresses/fiat` no longer requires `bank_details.email_address` or `bank_details.phone_number` for USD accounts, or `bank_details.bank_address` for SWIFT accounts. A `phone_number` you do send is still validated and stored in E.164 format. SWIFT account numbers for banks in 19 European countries, including the UK, no longer have to be IBANs. See [Fiat addresses](/fiat-addresses).
**Prohibited industries are rejected at submission.**
*Changed · Business customers*
A `Business` submission whose industry resolves to a prohibited industry is now rejected at `POST /api/customers/{id}/identifications/v2` with `400` [`prohibited_industry`](/errors#prohibited_industry), instead of going to manual review. See [Prohibited industries](/kyb#prohibited-industries).
**Archiving a customer frees its `external_id`.**
*Relaxed · All customers*
`POST /api/customers` and `PUT /api/customers/{id}` no longer reject an `external_id` that only archived customers hold. `GET /api/customers/{external_id}/external` still searches archived customers too, so once you reuse an `external_id` it can return either the archived or the active customer. Look customers up by `id` after you reuse one.
**Sandbox auto-approval sends `Processed` before `Approved`.**
*Sandbox · Webhooks · All customers*
In sandbox, an auto-approved identification now sends an `identification_status` webhook with `Processed` and then one with `Approved`, the same sequence as a production review. It used to send only `Approved`. See [Sandbox](/sandbox).
**Ask an approved EEA customer for their tax ID.**
*Added · Individual customers*
`POST /api/customers/{id}/identifications/tin-step-up` returns a hosted link that asks an approved EEA customer only for their tax identification number. Their approved status doesn't change. It returns `400` if the customer isn't approved or isn't in the EEA, and `409` if a tax ID is already on file. See [Trigger Customer TIN Step-Up](/reference/customer/trigger-customer-tin-step-up).
**Backfill a customer's tax ID.**
*Added · Individual customers*
`PATCH /api/customers/{id}/identification-data` adds `tax_identification_number` and `tax_residence_country` to a person customer's existing identification, without re-verification. `tax_residence_country` is required whenever you send a `tax_identification_number`. See [Update Customer Identification Data](/reference/customer/update-customer-identification-data).
**US companies that match the official registry skip company documents.**
*Relaxed · Business customers*
A US `Business` that matches the official company registry by name and registration number no longer needs the root company's documents. Nested business beneficiaries still need theirs. See [Automatic verification for US companies](/kyb#automatic-verification-for-us-companies).
**`transaction_refunded` and `customer_fiat_abilities` webhooks.**
*Added · Webhooks · All customers*
`transaction_refunded` fires when a refund is recorded for a transaction. `customer_fiat_abilities` fires when a customer's fiat deposit or payout rails change, so you can stop polling `GET /api/customers/{id}/abilities`. See [Supported event types](/webhooks#supported-event-types).
**New customers sign the terms before identification.**
| | |
| - | - |
| Affects | Behavior, response |
| Kind | Changed (customer lifecycle, pinned per customer) |
| Endpoints | `POST /api/customers`, `GET /api/customers/{id}/required-signings`, `POST /api/customers/{id}/signings` |
A customer created under this version starts in `SigningsRequired` instead of `IdentificationRequired`. Fetch the document with `GET /api/customers/{id}/required-signings`, present it, and record acceptance with `POST /api/customers/{id}/signings`. The customer then moves to `IdentificationRequired`. The terms contain the data sharing agreement, so they now come before KYC or KYB data collection instead of after approval. New customers sign a single global terms document instead of a regional one.
The version is pinned to the customer when you create it and drives that customer's onboarding for its lifetime. Customers created on an earlier version keep the previous flow: they start in `IdentificationRequired` and sign after their identification is approved. See [Create a new customer](/onboarding).
**`required-signings` returns an empty list when nothing is outstanding.**
| | |
| - | - |
| Affects | Response |
| Kind | Changed (status code) |
| Endpoints | `GET /api/customers/{id}/required-signings` |
Under this version, the endpoint returns `200` with `[]` when the customer owes no documents. Earlier and unversioned callers keep getting `409 Conflict`. This follows the version you send, not the customer's pin.
**Identification failures return structured, machine-readable errors.**
| | |
| - | - |
| Affects | Response |
| Kind | Changed (`400` body shape) |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`, `Person`, `Token`) |
A failed `Business`, `Person`, or `Token` identification submission now returns `400` with an array of `{code, message, docs_url, beneficiary_index}` objects instead of a plain error string, so your integration can branch on `code` instead of pattern-matching English text. See [Identification Errors](/errors) for the full list of codes.
Most checks return a single entry, but the four document-requirement codes (`company_details_required`, `legal_presence_required`, `ownership_structure_required`, `control_structure_required`) can each appear once per company in the submission that's missing that group. A non-US `Business` submission checks all four independently, so several can genuinely be missing at once.
Callers on an earlier or unversioned `X-API-Version` keep receiving today's plain error string on `400`, unchanged.
**Business questionnaires get a granular `industry` field.**
| | |
| - | - |
| Affects | Request |
| Kind | Additive (new field), deprecation (`industry_type`), version-gated requirement |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
`business_details` on a `Business` submission gains a new field, `industry`, which exposes Sumsub's full industry taxonomy (136 values) in place of the 36 broad categories on the existing `industry_type` field. `industry_type` is now deprecated. It keeps working indefinitely, so no integration needs to change immediately, but `industry` is required for any submission made under this version or later. Callers on an earlier or unversioned `X-API-Version` can keep sending `industry_type` alone; `industry` stays optional for them. If a submission includes both fields, `industry` takes precedence and `industry_type` is silently ignored.
**A nested business beneficiary now needs only Company Details and Ownership Structure documents.**
| | |
| - | - |
| Affects | Behavior only, no request or response shape changes |
| Kind | Relaxed validation |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
A `Shareholder` that's a legal entity (a nested company) previously needed the same document groups as the root company, up to all four for a non-US root. It now always needs just Company Details and Ownership Structure, regardless of the root company's own requirement or the nested entity's registered country. See [Company Documents](/kyb#company-documents).
**A US company `Business` submission's CDD certification falls back to a 10% ownership threshold instead of requiring 25%.**
| | |
| - | - |
| Affects | Behavior only, no request or response shape changes |
| Kind | Relaxed validation |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
Previously, a US non-sole-proprietorship `Business` submission was rejected with `cdd_certification_invalid` if no beneficiary owned 25% or more, even when that's genuinely true of the company. The generated CDD certification (31 CFR 1010.230) now lists whoever owns 25% or more in its ownership section as before, but if nobody does, it falls back to listing whoever owns 10% or more instead of rejecting the submission. For a company where nobody owns even 10%, see the qualifying-owner requirement below, which applies this same threshold to every country. The attesting party's control-person section is still required as before. See [Company Documents](/kyb#company-documents).
**A `ubo_share_below_threshold` beneficiary now falls back to a 10% disclosure threshold, in every country.**
| | |
| - | - |
| Affects | Behavior only, no request or response shape changes |
| Kind | Relaxed validation |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
Previously, a beneficiary whose only role was `UltimateBeneficialOwner` was rejected with `ubo_share_below_threshold` whenever their `share_percentage` was 25 or below, with no fallback. The only way to submit such a beneficiary was to give them an additional role (for example `Director`), which dropped the `UltimateBeneficialOwner` tag entirely. Now, if nobody at that beneficiary's level (the root company, or a nested company) owns 25% or more, the threshold for keeping the `UltimateBeneficialOwner` tag drops to 10% instead of rejecting outright. A UBO-only beneficiary owning, say, 15% is now accepted and keeps the tag as long as nobody else at that level already owns 25% or more; nobody rejects until nobody at that level owns even 10%. This applies in every country, not just US submissions. See [`ubo_share_below_threshold`](/errors#ubo_share_below_threshold).
**A beneficiary's `birth_place.state` is optional for countries with no administrative subdivisions.**
| | |
| - | - |
| Affects | Request |
| Kind | Relaxed validation (field no longer required) |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
Previously, every beneficiary's `birth_place.state` was required, even for countries with no ISO 3166-2 administrative subdivisions (Vatican City, Malta, and Hong Kong, for example), leaving no legitimate value to send. `state` is now optional, and validation only rejects a missing value for a birth country that actually has subdivisions. Existing integrations that already send `state` are unaffected. See [Business API Field Reference](/kyb#business-api-field-reference).
**A `Business` submission now requires a root-level Director and a root-level qualifying owner, in every country.**
| | |
| - | - |
| Affects | Behavior only, no request or response shape changes |
| Kind | New validation (`400` on submissions that previously succeeded) |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
Previously, a non-sole-proprietorship `Business` submission outside the US could validate with zero beneficiaries, or with beneficiaries holding only, say, `Shareholder` or `UltimateBeneficialOwner` roles, with nobody holding `Director` and nobody disclosing enough ownership to matter. US submissions never fully hit this: the mandatory CDD attesting party always requires a Director, and (as of the CDD fallback-tier change above) a qualifying owner, but that's incidental to the US CDD flow, not an independent floor, and ROW had no equivalent at all. Every non-sole-proprietorship company, in every country, now must name at least one root-level beneficiary with the `Director` role, and at least one root-level beneficiary owning 25% or more (or 10% or more, if nobody owns 25% or more), or the submission is rejected with `structure_invalid`. A `Representative`-only beneficiary never satisfies the ownership half of this, regardless of their disclosed `share_percentage`: MoonPay Enterprise never independently verifies a Representative-only beneficiary's identity, so their disclosure can't count as ownership evidence. Combined shares of everyone who does count as a qualifying owner (any `UltimateBeneficialOwner`, or any other non-`Representative`-only beneficiary) still can't exceed 100%. Neither requirement applies to sole proprietorships (still exactly one beneficiary, 100% owned, no Director needed) or to nested business beneficiaries' own sub-beneficiary lists (which remain documentation-only). See [`structure_invalid`](/errors#structure_invalid).
**A `PrivateFoundation` `Business` submission now requires an accepted `purpose_and_intended_use`.**
| | |
| - | - |
| Affects | Behavior only, no request or response shape changes |
| Kind | New validation (`400` on submissions that previously succeeded) |
| Endpoints | `POST /customers/{id}/identifications/v2` (`Business`) |
A `Business` submission with `company_type` `PrivateFoundation` is now rejected with `foundation_intended_use_not_supported` unless `planned_activities.purpose_and_intended_use` is one of a limited set of values accepted for that entity type. See [`foundation_intended_use_not_supported`](/errors#foundation_intended_use_not_supported).
**Adds the FedNow deposit rail.**
| | |
| - | - |
| Affects | Response |
| Kind | Added (union variant) |
| Endpoints | Any response containing an `Autoramp` object: `POST /autoramps`, `GET /autoramps`, `GET /autoramps/{autoramp_id}`, `PATCH /autoramps/{autoramp_id}`, `GET /autoramps/{external_id}/external`, `POST /autoramps/{autoramp_id}/quotes` |
The `deposit_rails` array can now include a `FedNow` member, for autoramps whose deposit account supports FedNow. This is additive to the `DepositRail` union: every existing member (`Iban`, `AchWire`, `Swift`, and the rest) is unchanged.
If your integration matches on `deposit_rails` with an exhaustive switch and no default case, add a `FedNow` branch before sending `X-API-Version: 2026-07-01`. Callers on `2025-03-13` never see this variant, so there is no rush to migrate.
Initial version. Served when no `X-API-Version` header is sent.
# Countries
Source: https://dev.enterprise.moonpay.com/country
Supported onboarding countries organized by risk tier. Covers OFAC restrictions, country risk ratings, fiat rail availability, and virtual account eligibility.
MoonPay Enterprise 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 MoonPay Enterprise's Prohibited Country Risk Rating.
## Country Risk Ratings
**Rail columns:** USD covers ACH, Wire, SWIFT, and FedNow. EUR covers SEPA. GBP covers FPS and CHAPS. BRL covers PIX. For SWIFT country coverage by charge option, see [Collect / Send via SWIFT](/currencies-and-rails#collect-/-send-via-swift).
**How to read the cells:**
* **Supported (HR)** is Supported High Risk: customers in these jurisdictions are onboarded with enhanced due diligence.
* **Yes (Business)** means the rail is available to business customers only.
* **Yes (Individual)** means the rail is available to individual customers only.
* **Yes (1PP)** means first-party payment only: funds must be sent from a bank account held in the same name as the onboarded customer.
* **Yes** in the GBP column always means 1PP. GBP rails are first-party payment only wherever they are available.
| Jurisdiction | ISO3 | ISO2 | Risk Rating | USD | EUR | GBP | BRL |
| - | - | - | - | - | - | - | - |
| Afghanistan | AFG | AF | Prohibited | No | No | No | No |
| Albania | ALB | AL | Supported | No | Yes | Yes | Yes (Individual) |
| Algeria | DZA | DZ | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| American Samoa | ASM | AS | Prohibited | No | No | No | No |
| Andorra | AND | AD | Supported | Yes | Yes | Yes | Yes (Individual) |
| Angola | AGO | AO | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Anguilla | AIA | AI | Supported | Yes | Yes | Yes | Yes (Individual) |
| Antigua and Barbuda | ATG | AG | Supported | Yes | Yes | Yes | Yes (Individual) |
| Argentina | ARG | AR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Armenia | ARM | AM | Supported | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Aruba | ABW | AW | Supported | Yes | Yes | Yes | Yes (Individual) |
| Australia | AUS | AU | Not Supported | No | No | No | No |
| Austria | AUT | AT | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Azerbaijan | AZE | AZ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Bahamas | BHS | BS | Supported | Yes | Yes | Yes | Yes (Individual) |
| Bahrain | BHR | BH | Supported | Yes | Yes | Yes | Yes (Individual) |
| 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 | Yes | Yes (Individual) |
| Belize | BLZ | BZ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Benin | BEN | BJ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Bermuda | BMU | BM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Bhutan | BTN | BT | Supported | No | Yes | Yes | Yes (Individual) |
| Bolivia | BOL | BO | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Bosnia and Herzegovina | BIH | BA | Supported | No | Yes | Yes | Yes (Individual) |
| Botswana | BWA | BW | Supported | No | Yes | Yes | Yes (Individual) |
| Brazil | BRA | BR | Supported | Yes | Yes | Yes | Yes (Individual) |
| British Virgin Islands | VGB | VG | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Brunei Darussalam | BRN | BN | Supported | Yes | Yes | Yes | Yes (Individual) |
| Bulgaria | BGR | BG | Supported (HR) | Yes (Business) | Yes (1PP) | Yes | Yes (Individual) |
| Burkina Faso | BFA | BF | Prohibited | No | No | No | No |
| Burundi | BDI | BI | Supported | No | Yes | Yes | No |
| Cambodia | KHM | KH | Supported (HR) | No | Yes | Yes | Yes (Individual) |
| Cameroon | CMR | CM | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| Canada | CAN | CA | Prohibited | No | No | No | No |
| Cape Verde | CPV | CV | Supported | Yes | Yes | Yes | Yes (Individual) |
| Cayman Islands | CYM | KY | Supported | Yes | Yes | Yes | Yes (Individual) |
| Central African Republic | CAF | CF | Prohibited | No | No | No | No |
| Chad | TCD | TD | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Chile | CHL | CL | Supported | Yes | Yes | Yes | Yes (Individual) |
| China | CHN | CN | Prohibited | No | No | No | No |
| Colombia | COL | CO | Supported | Yes | Yes | Yes | Yes (Individual) |
| Comoros | COM | KM | Supported (HR) | Yes | Yes | Yes | Yes (Individual) |
| Cook Island | COK | CK | Supported | Yes | Yes | Yes | Yes (Individual) |
| Costa Rica | CRI | CR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Cote D'Ivoire | CIV | CI | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Croatia | HRV | HR | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Cuba | CUB | CU | Prohibited | No | No | No | No |
| Curacao | CUW | CW | Supported | Yes | Yes | Yes | Yes (Individual) |
| Cyprus | CYP | CY | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Czech Republic | CZE | CZ | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Denmark | DNK | DK | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Djibouti | DJI | DJ | Supported (HR) | Yes | Yes | Yes | Yes (Individual) |
| Dominica | DMA | DM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Dominican Republic | DOM | DO | Supported | Yes | Yes | Yes | Yes (Individual) |
| Ecuador | ECU | EC | Supported | Yes | Yes | Yes | Yes (Individual) |
| Egypt | EGY | EG | Supported | Yes | Yes | Yes | Yes (Individual) |
| El Salvador | SLV | SV | Supported | Yes | Yes | Yes | Yes (Individual) |
| Equatorial Guinea | GNQ | GQ | Supported (HR) | Yes | Yes | Yes | No |
| Eritrea | ERI | ER | Supported (HR) | No | Yes | Yes | No |
| Estonia | EST | EE | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Eswatini | SWZ | SZ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Ethiopia | ETH | ET | Supported | No | Yes | No | Yes (Individual) |
| Faroe Islands | FRO | FO | Supported | Yes | Yes | Yes | Yes (Individual) |
| Fiji | FJI | FJ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Finland | FIN | FI | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| France | FRA | FR | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Gabon | GAB | GA | Supported (HR) | Yes | Yes | Yes | Yes (Individual) |
| Gambia | GMB | GM | Supported | No | Yes | Yes | Yes (Individual) |
| Georgia | GEO | GE | Supported | Yes | Yes | Yes | Yes (Individual) |
| Germany | DEU | DE | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Ghana | GHA | GH | Supported | Yes | Yes | Yes | Yes (Individual) |
| Gibraltar | GIB | GI | Supported | Yes | Yes | Yes | Yes (Individual) |
| Greece | GRC | GR | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Grenada | GRD | GD | Supported | No | Yes | Yes | Yes (Individual) |
| Guam | GUM | GU | Supported | Yes | Yes | Yes | Yes (Individual) |
| Guatemala | GTM | GT | Supported | Yes | Yes | Yes | Yes (Individual) |
| Guernsey | GGY | GG | Supported | Yes | Yes | Yes | Yes (Individual) |
| Guinea | GIN | GN | Supported | No | Yes | Yes | Yes (Individual) |
| Guinea-Bissau | GNB | GW | Prohibited | No | No | No | No |
| Guyana | GUY | GY | Supported | Yes | Yes | Yes | Yes (Individual) |
| Haiti | HTI | HT | Prohibited | No | No | No | No |
| Honduras | HND | HN | Supported | Yes | Yes | Yes | Yes (Individual) |
| Hong Kong SAR, China | HKG | HK | Supported | Yes | Yes | Yes | Yes (Individual) |
| Hungary | HUN | HU | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Iceland | ISL | IS | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| India | IND | IN | Prohibited | No | No | No | No |
| Indonesia | IDN | ID | Supported | Yes | Yes | Yes | Yes (Individual) |
| Iran | IRN | IR | Prohibited | No | No | No | No |
| Iraq | IRQ | IQ | Prohibited | No | No | No | No |
| Ireland | IRL | IE | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Isle of Man | IMN | IM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Israel | ISR | IL | Supported | Yes | Yes | Yes | Yes (Individual) |
| Italy | ITA | IT | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Jamaica | JAM | JM | Prohibited | No | No | No | No |
| Japan | JPN | JP | Prohibited | No | No | No | No |
| Jersey | JEY | JE | Supported | Yes | Yes | Yes | Yes (Individual) |
| Jordan | JOR | JO | Supported | No | Yes | Yes | Yes (Individual) |
| Kazakhstan | KAZ | KZ | Supported | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Kenya | KEN | KE | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| Kiribati | KIR | KI | Supported | Yes | Yes | Yes | Yes (Individual) |
| Korea, North | PRK | KP | Prohibited | No | No | No | No |
| Korea, South | KOR | KR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Kosovo | XXK | XK | Prohibited | No | No | No | No |
| Kuwait | KWT | KW | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| Kyrgyzstan | KGZ | KG | Supported | Yes | Yes | Yes | Yes (Individual) |
| Laos | LAO | LA | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Latvia | LVA | LV | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Lebanon | LBN | LB | Prohibited | No | No | No | No |
| Lesotho | LSO | LS | Supported | Yes | Yes | Yes | Yes (Individual) |
| Liberia | LBR | LR | Prohibited | No | No | No | No |
| Libya | LBY | LY | Prohibited | No | No | No | No |
| Liechtenstein | LIE | LI | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Lithuania | LTU | LT | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Luxembourg | LUX | LU | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| 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 (Individual) |
| Malaysia | MYS | MY | Prohibited | No | No | No | No |
| Maldives | MDV | MV | Supported | Yes | Yes | Yes | Yes (Individual) |
| Mali | MLI | ML | Prohibited | No | No | No | No |
| Malta | MLT | MT | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Marshall Islands | MHL | MH | Supported | Yes | Yes | Yes | Yes (Individual) |
| Martinique | MTQ | MQ | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Mauritania | MRT | MR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Mauritius | MUS | MU | Supported | Yes | Yes | Yes | Yes (Individual) |
| Mexico | MEX | MX | Supported | Yes | Yes | Yes | Yes (Individual) |
| Moldova | MDA | MD | Supported | Yes | Yes | Yes | Yes (Individual) |
| Monaco | MCO | MC | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Mongolia | MNG | MN | Prohibited | No | No | No | No |
| Montenegro | MNE | ME | Supported | Yes | Yes | Yes | Yes (Individual) |
| Montserrat | MSR | MS | Supported | Yes | Yes | Yes | Yes (Individual) |
| Morocco | MAR | MA | Prohibited | No | No | No | No |
| Mozambique | MOZ | MZ | Supported | No | Yes (1PP) | Yes | Yes (Individual) |
| Myanmar | MMR | MM | Prohibited | No | No | No | No |
| Namibia | NAM | NA | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Nauru | NRU | NR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Nepal | NPL | NP | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| Netherlands | NLD | NL | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| New Caledonia | NCL | NC | Supported | Yes | Yes | Yes | Yes (Individual) |
| New Zealand | NZL | NZ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Nicaragua | NIC | NI | Prohibited | No | No | No | No |
| Niger | NER | NE | Supported (HR) | No | Yes | Yes | Yes (Individual) |
| Nigeria | NGA | NG | Supported | Yes | Yes | Yes | Yes (Individual) |
| Niue | NIU | NU | Supported | Yes | Yes | Yes | Yes (Individual) |
| North Macedonia | MKD | MK | Supported | No | Yes | Yes | Yes (Individual) |
| Norway | NOR | NO | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Oman | OMN | OM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Pakistan | PAK | PK | Prohibited | No | No | No | No |
| Palau | PLW | PW | Supported | Yes | Yes | Yes | Yes (Individual) |
| Palestine | PSE | PS | Prohibited | No | No | No | No |
| Panama | PAN | PA | Supported | Yes | Yes | Yes | Yes (Individual) |
| Papua New Guinea | PNG | PG | Supported (HR) | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Paraguay | PRY | PY | Supported | Yes | Yes | Yes | Yes (Individual) |
| Peru | PER | PE | Supported | Yes | Yes | Yes | Yes (Individual) |
| Philippines | PHL | PH | Supported | Yes | Yes | Yes | Yes (Individual) |
| Poland | POL | PL | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Portugal | PRT | PT | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Puerto Rico | PRI | PR | Supported | Yes | Yes | Yes | Yes (Individual) |
| Qatar | QAT | QA | Supported | No | Yes | Yes | Yes (Individual) |
| Republic of the Congo | COG | CG | Prohibited | No | No | No | No |
| Romania | ROU | RO | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Russia | RUS | RU | Prohibited | No | No | No | No |
| Rwanda | RWA | RW | Supported | Yes | Yes | Yes | No |
| Saint Helena, Ascension and Tristan | SHN | SH | Supported | Yes | Yes | Yes | Yes (Individual) |
| Saint Kitts and Nevis | KNA | KN | Supported | Yes | Yes | Yes | Yes (Individual) |
| Saint Lucia | LCA | LC | Supported | Yes | Yes | Yes | Yes (Individual) |
| Saint Pierre and Miquelon | SPM | PM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Saint Vincent and the Grenadines | VCT | VC | Supported | Yes | Yes | Yes | Yes (Individual) |
| Samoa | WSM | WS | Supported | Yes | Yes | Yes | Yes (Individual) |
| San Marino | SMR | SM | Supported | Yes | Yes | Yes | Yes (Individual) |
| Sao Tome and Principe | STP | ST | Supported | Yes | Yes | Yes | Yes (Individual) |
| Saudi Arabia | SAU | SA | Supported | Yes | Yes | Yes | Yes (Individual) |
| Senegal | SEN | SN | Prohibited | No | No | No | No |
| Serbia | SRB | RS | Supported | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Seychelles | SYC | SC | Supported | No | Yes | Yes | Yes (Individual) |
| Sierra Leone | SLE | SL | Supported | Yes | Yes | Yes | No |
| Singapore | SGP | SG | Supported | Yes | Yes | Yes | Yes (Individual) |
| Slovakia | SVK | SK | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Slovenia | SVN | SI | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Solomon Islands | SLB | SB | Supported | No | Yes | Yes | Yes (Individual) |
| Somalia | SOM | SO | Prohibited | No | No | No | No |
| South Africa | ZAF | ZA | Supported | No | Yes (1PP) | Yes | Yes (Individual) |
| South Sudan | SSD | SS | Prohibited | No | No | No | No |
| Spain | ESP | ES | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Sri Lanka | LKA | LK | Supported | Yes | Yes | Yes | Yes (Individual) |
| St Maarten | SXM | SX | Supported | Yes | Yes | Yes | Yes (Individual) |
| Sudan | SDN | SD | Prohibited | No | No | No | No |
| Suriname | SUR | SR | Supported | No | Yes | Yes | Yes (Individual) |
| Sweden | SWE | SE | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| Switzerland | CHE | CH | Supported | Yes | Yes | Yes | Yes (Individual) |
| Syrian Arab Republic | SYR | SY | Prohibited | No | No | No | No |
| Taiwan | TWN | TW | Supported | Yes | Yes | Yes | Yes (Individual) |
| Tajikistan | TJK | TJ | Supported | Yes | Yes | Yes | Yes (Individual) |
| Tanzania | TZA | TZ | Supported | No | Yes (1PP) | Yes | Yes (Individual) |
| Thailand | THA | TH | Supported | Yes | Yes | Yes | Yes (Individual) |
| The Democratic Republic Of The Congo | COD | CD | Prohibited | No | No | No | No |
| Timor-Leste | TLS | TL | Supported | Yes | Yes | Yes | Yes (Individual) |
| Togo | TGO | TG | Supported | No | Yes | Yes | Yes (Individual) |
| Tonga | TON | TO | Supported | No | Yes | Yes | Yes (Individual) |
| Trinidad and Tobago | TTO | TT | Supported | No | Yes (1PP) | Yes | Yes (Individual) |
| Tunisia | TUN | TN | Supported | Yes | Yes | Yes | Yes (Individual) |
| Turkey | TUR | TR | Supported | No | Yes | Yes | Yes (Individual) |
| Turkmenistan | TKM | TM | Supported (HR) | Yes | Yes | Yes | Yes (Individual) |
| Turks and Caicos Islands | TCA | TC | Supported | No | Yes | Yes | Yes (Individual) |
| 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 (Individual) |
| United Kingdom | GBR | GB | Supported | Yes (Business) | Yes | Yes | Yes (Individual) |
| United States | USA | US | Supported | Yes | Yes (1PP) | Yes | Yes (Individual) |
| United States Virgin Islands | VIR | VI | Prohibited | No | No | No | No |
| Uruguay | URY | UY | Supported | Yes | Yes | Yes | Yes (Individual) |
| Uzbekistan | UZB | UZ | Supported | Yes | Yes (1PP) | Yes | Yes (Individual) |
| Vanuatu | VUT | VU | Supported | No | Yes (1PP) | Yes | Yes (Individual) |
| Venezuela | VEN | VE | Prohibited | No | No | No | No |
| Vietnam | VNM | VN | Supported (HR) | No | Yes (1PP) | Yes | Yes (Individual) |
| 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 (Individual) |
| Zimbabwe | ZWE | ZW | Prohibited | No | No | No | No |
# Crypto Addresses
Source: https://dev.enterprise.moonpay.com/crypto-addresses
Register wallet addresses for Travel Rule compliance before using them in any Autoramp flow.
Every wallet address that interacts with MoonPay Enterprise must be registered (linked) to a customer **before** it can be used in any Autoramp flow: [onramp](/onramp), [offramp](/offramp), or [stable to stable](/stable-to-stable). This is a regulatory requirement under the **[Travel Rule](/travel-rule)**, which obliges Virtual Asset Service Providers to exchange originator and beneficiary information for crypto transfers.
MoonPay Enterprise supports two wallet types:
* **Self-hosted wallets**: the customer controls the private key. Register with a signed proof-of-ownership message. For customers in the **US** and **Rest of World** who cannot sign from their custody UI (e.g. Fireblocks), register with a self-attested declaration instead, with no signature.
* **Hosted wallets**: the wallet is custodied by another VASP (e.g. Coinbase, Kraken). Registration requires the VASP's DID so MoonPay Enterprise can exchange travel-rule data with the custodian.
Autoramp creation will fail if the recipient wallet address has not been registered first.
If the blockchain is Stellar, the wallet must already have an established USDC trustline. This applies to all three registration endpoints below. In production, registering a Stellar address without one returns a `400` with the message: "Stellar account does not have a USDC trustline established. Please add a USDC trustline before registering this address." The sandbox skips this check.
## Verify a self-hosted crypto address
Submit a signature proving ownership of your cryptocurrency wallet. The verification process requires signing a specific message with your wallet's private key.
```bash theme={null}
I am verifying ownership of the wallet address {wallet_address} as customer {customer_id}. This message was signed on {date} to confirm my control over this wallet.
```
Replace:
* `{customer_id}` with your customer ID
* `{wallet_address}` with your wallet address
* `{date}` with today's date in DD/MM/YYYY format using UTC timezone
Example:
```bash theme={null}
I am verifying ownership of the wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e as customer 4b85d15e-f343-41c0-809c-85314cae2fa6. This message was signed on 23/05/2025 to confirm my control over this wallet.
```
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/crypto/selfhosted \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"message": "I am verifying ownership of the wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e as customer 4b85d15e-f343-41c0-809c-85314cae2fa6. This message was signed on 23/05/2025 to confirm my control over this wallet.",
"signature": "0xa5f1751b75a28c12694f02590d29b8cdd68b4f5c783273a75823fc6cfeaa702f1a65fa7c1838ae799fe755c92443cb51631d000922329cdd2ce799eee75f42531b",
"blockchain": "Ethereum"
}'
```
The date must be in DD/MM/YYYY format and within **one day** of today's UTC date. Yesterday, today, and tomorrow are all accepted, which absorbs clock skew and local timezones that run ahead of or behind UTC. Anything outside that window fails, so don't carry a stale date over from a template.
`Idempotency-Key` is required on this `POST`, so a replayed signature registers the address only once. MoonPay Enterprise stores successful responses only, so a retry after an error runs the registration again. See [Idempotency](/idempotency).
The message must be **byte-identical** between sign-time and POST-time (no rewording, no trailing newline, no whitespace edits), and the `customer_id` inside the message must equal the `customer_id` in the request body. EVM signatures use standard EIP-191 `personal_sign` (the default for `cast wallet sign`, ethers `signMessage`, viem `signMessage`); smart-contract wallets verify via EIP-1271. Not-yet-deployed (counterfactual) smart-contract wallets sign per ERC-6492, which wraps the EIP-1271 signature so it can be verified before the wallet's first on-chain deployment. You can add additional text before or after the required message and verification still succeeds.
### Error response
A `400` is returned when the proof message or the signature does not validate. These address endpoints return the error as a plain string body, not a structured object:
```json theme={null}
"Invalid signature! Cannot register address"
```
Common causes:
* The proof message does not match the required format
* The date in the proof message is more than one day from today's UTC date
* The signature does not verify against the submitted address
* The `blockchain` does not match the address format
* The wallet address is already registered, which returns `409`
* The customer ID does not belong to your partner, which returns `401`
## Register a self-attested crypto address
For direct customers who cannot produce a signature from their custody UI (e.g. Fireblocks), register a self-hosted wallet by declaring ownership instead of signing a proof-of-ownership message. The wallet is still registered as self-hosted and carries the same [Travel Rule](/travel-rule) obligations as the signed flow.
Self-attestation is available only to customers in the **US** and **Rest of World** jurisdictions. Every other customer, including those in the UK, EEA, Canada, and Australia, must use the signed [self-hosted flow](#verify-a-self-hosted-crypto-address) and gets a `403` from this endpoint. A customer whose region MoonPay Enterprise cannot resolve also gets a `403`. The sandbox skips this region check, so the example below works for any sandbox customer.
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/crypto/attested \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174002" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"blockchain": "Ethereum",
"name": "Fireblocks vault"
}'
```
The registration requires:
* `customer_id`: The customer's UUID
* `address`: The wallet address the customer declares they control
* `blockchain`: The blockchain the address is on (e.g. `Ethereum`, `Solana`, `Polygon`, `Base`, `Arbitrum`)
* `name`: Optional display name for this wallet
The `Idempotency-Key` header is required so the address is only registered once on retries. Pass the optional `X-Sub-Partner-Id` header to register the wallet under a specific sub-partner.
A successful registration returns the verified address. Attested wallets come back with `address_type: "SelfHosted"` and the sentinel `proof_signature: "SELF_ATTESTED"`, which marks them as declared rather than cryptographically signed:
```json theme={null}
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"wallet_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"address_type": "SelfHosted",
"blockchain": "Ethereum",
"created_at": "2025-06-19T10:30:00+00:00",
"disabled": false,
"is_self": true,
"name": "Fireblocks vault",
"proof_message": "Self-attested ownership (no signature provided)",
"proof_signature": "SELF_ATTESTED",
"vasp_did": null
}
```
### Error response
A `409` is returned when the wallet address is already registered. These address endpoints return the error as a plain string body, not a structured object:
```json theme={null}
"A crypto address with this wallet address already exists"
```
## Register a hosted wallet
When the recipient wallet is custodied by another institution (VASP), for example Coinbase, Kraken, or Binance, you need to register the wallet address along with the institution's DID so MoonPay Enterprise can exchange travel-rule data with the custodian.
First, find the DID (Decentralized Identifier) of the institution that hosts the wallet:
```bash Bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/addresses/crypto/hosted/vasps?q=bitstamp&limit=10" \
-H "X-API-Key: $API_KEY"
```
Response:
```json theme={null}
[
{
"did": "did:ethr:0x367f004f1062f68c038ac8d3c3071fedc18fb689",
"name": "Bitstamp",
"country": "LU",
"website": "https://www.bitstamp.net/"
}
]
```
Submit the wallet address with the VASP's DID:
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/crypto/hosted \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174001" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"wallet_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"vasp_did": "did:ethr:0x367f004f1062f68c038ac8d3c3071fedc18fb689",
"blockchain": "Ethereum"
}'
```
The registration requires:
* `customer_id`: The customer's UUID
* `wallet_address`: The wallet address at the institution
* `vasp_did`: The DID (Decentralized Identifier) of the institution hosting the wallet (obtained from the VASP search)
* `blockchain`: The blockchain the address is on (e.g. `Ethereum`, `Solana`, `Polygon`, `Base`, `Arbitrum`)
You can also pass an optional `name` to set a display name for the wallet.
The wallet must be registered in your name at the hosting institution. MoonPay Enterprise will use the VASP DID to exchange travel rule information with the institution.
`Idempotency-Key` is required on this `POST`, so the address is registered only once on retries. MoonPay Enterprise stores successful responses only, so a retry after an error runs the registration again. See [Idempotency](/idempotency).
## List verified addresses
Retrieve all verified cryptocurrency addresses for a customer:
```bash Bash theme={null}
curl -X GET https://api.sandbox.iron.xyz/api/addresses/crypto/{customer_id} \
-H "X-API-Key: $API_KEY"
```
Optionally filter by address type using the `filter` query parameter:
* `?filter=Hosted`: Only hosted wallet addresses
* `?filter=SelfHosted`: Only self-hosted wallet addresses
* `?filter=All` or no filter: All addresses (default)
Example with filter:
```bash Bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/addresses/crypto/4b85d15e-f343-41c0-809c-85314cae2fa6?filter=SelfHosted" \
-H "X-API-Key: $API_KEY"
```
The response includes details about each verified address:
* `id`: Address ID
* `wallet_address`: The wallet address
* `address_type`: "Hosted" or "SelfHosted"
* `blockchain`: The blockchain (e.g. "Ethereum", "Solana")
* `created_at`: Registration timestamp
* `disabled`: Whether the address is disabled
* `is_self`: Whether the wallet belongs to the customer themselves or to a third party
* `name`: Display name, if one was set at registration
* `vasp_did`: (For hosted wallets only) The VASP's DID
* `proof_message`: (For self-hosted wallets only) The verification message
* `proof_signature`: (For self-hosted wallets only) The signature
Self-attested wallets are returned with `address_type: "SelfHosted"`, so `?filter=SelfHosted` includes them. To tell them apart from cryptographically signed wallets, check `proof_signature`: attested wallets carry the sentinel value `"SELF_ATTESTED"` and a `proof_message` that marks the wallet as self-attested rather than signed.
## Disable or enable an address
You can disable or enable a verified crypto address. Disabled addresses cannot be used for transactions.
```bash Bash theme={null}
curl -X PUT https://api.sandbox.iron.xyz/api/addresses/crypto/{address_id}/disabled \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"disabled": true
}'
```
Addresses that were automatically disabled due to high risk (e.g., flagged by Chainalysis) cannot be re-enabled.
## Related guides
# Currencies and Rails
Source: https://dev.enterprise.moonpay.com/currencies-and-rails
Supported fiat currencies, the payment rails that move them, virtual accounts, and bank cutoff windows. Several banking partners in the background provide redundancy and fallback.
MoonPay Enterprise routes fiat payments across several banking partners behind one API. If a banking partner cannot process a payment for a specific customer or industry, MoonPay Enterprise falls back to another. Redundancy is built in, so you never integrate a second banking provider.
For the coverage snapshot and the wider product set, see [Rails & Capabilities](/rails-and-capabilities). This page carries the per-rail detail: delivery speeds, availability, virtual account identifiers, and bank cutoff windows.
## Supported Currencies
These currencies support both onramp (fiat to crypto) and offramp (crypto to fiat) for Business-to-Business (B2B), Business-to-Individual (B2C), and Individual-to-Individual (I2I) transactions.
Coverage snapshot: September 2026. October 2026 launches are planned.
| Currency | Rails | Settlement | Status |
| - | - | - | - |
| 🇺🇸 USD | ACH, Wire, FedNow, SWIFT | T+0 / T+1 (SWIFT) | Live |
| 🇪🇺 EUR | SEPA Instant, SEPA Credit | Real-time / T+1 (SEPA Credit) | Live |
| 🇬🇧 GBP | Faster Payments, CHAPS | Real-time | Live |
| 🇧🇷 BRL | PIX | Real-time | Live |
| 🇲🇽 MXN | SPEI | Real-time | Planned: Oct 2026 |
| 🇦🇷 ARS | Local bank transfer | Real-time | Planned: Oct 2026 |
| 🇨🇴 COP | Local bank transfer | Real-time | Planned: Oct 2026 |
## Payment Rails
| **Rail** | **Description** | **Availability** |
| - | - | - |
| **SEPA (Instant)** | Instant EEA transactions sub €100k | ✅ |
| **SEPA (Credit)** | Usually 24 hours for transactions greater than €100k | ✅ |
| **ACH** | Common for domestic transfers within the US | ✅ |
| **Wire** | Common for domestic transfers within the US | ✅ |
| **FedNow** | Instant 24/7 domestic transfers within the US | ✅ |
| **CHAPS** | Common for domestic transfers within the UK | ✅ |
| **FPS** | Common for domestic transfers within the UK | ✅ |
| **PIX** | Instant domestic transfers within Brazil | ✅ |
| **SPEI** | Domestic transfers within Mexico | Planned: Oct 2026 |
| **SWIFT Inbound** | SWIFT Network payment from an external bank into the Virtual Account | ✅ |
| **SWIFT Outbound** | SWIFT Network payment outwards to an external fiat bank account | ✅ |
SWIFT is available in USD. Business and individual customers can use SWIFT for deposits and payouts, for both first-party and third-party payments.
## Collect / Send via SWIFT
SWIFT country coverage for USD depends on who pays the transfer charges. With **OUR**, the sender pays all charges. With **SHA**, the sender and the beneficiary share the charges. Expand a row to see the countries it covers.
| **Flow** | **USD** |
| - | - |
| **Businesses** · Collect · OUR | 46 countriesAustralia · Austria · Bahamas · Bahrain · Belgium · Bermuda · Brazil · Canada · Cayman Islands · Croatia · Cyprus · Czech Republic (Czechia) · Denmark · Estonia · Finland · France · Germany · Greece · Hong Kong · Hungary · Ireland · Israel · Italy · Latvia · Liechtenstein · Lithuania · Luxembourg · Malta · Mexico · Netherlands · Norway · Philippines · Poland · Portugal · Romania · San Marino · Serbia · Singapore · Slovakia · Slovenia · South Korea · Spain · Sweden · Switzerland · United Arab Emirates · United Kingdom |
| **Businesses** · Collect · SHA | 148 countriesAmerican Samoa · Andorra · Angola · Anguilla · Antigua and Barbuda · Argentina · Armenia · Aruba · Australia · Austria · Azerbaijan · Bahamas · Bahrain · Belgium · Belize · Benin · Bermuda · Bolivia · Brazil · Brunei Darussalam · Bulgaria · Canada · Cape Verde · Cayman Islands · Chad · Chile · Colombia · Comoros · Cook Island · Costa Rica · Cote D'Ivoire · Croatia · Curacao · Cyprus · Czech Republic · Denmark · Djibouti · Dominica · Dominican Republic · Ecuador · Egypt · El Salvador · Equatorial Guinea · Estonia · Eswatini · Faroe islands · Fiji · Finland · France · Gabon · Georgia · Germany · Ghana · Gibraltar · Greece · Guam · Guatemala · Guernsey · Guyana · Honduras · Hong Kong SAR, China · Hungary · Iceland · India · Indonesia · Ireland · Isle of Man · Israel · Italy · Japan · Jersey · Kazakhstan · Kiribati · Korea, South · Kyrgyzstan · Laos · Latvia · Lesotho · Liechtenstein · Lithuania · Luxembourg · Macao SAR, China · Malawi · Malaysia · Maldives · Malta · Marshall Islands · Martinique · Mauritania · Mauritius · Mexico · Moldova · Monaco · Mongolia · Montenegro · Montserrat · Namibia · Nauru · Netherlands · New Caledonia · New Zealand · Niue · Norway · Oman · Palau · Panama · Papua New Guinea · Paraguay · Peru · Philippines · Poland · Portugal · Puerto Rico · Romania · Rwanda · Saint Helena, Ascension and Tristan · Saint Kitts and Nevis · Saint Lucia · Saint Pierre and Miquelon · Saint Vincent and the Grenadines · Samoa · San Marino · Sao Tome and Principe · Saudi Arabia · Serbia · Sierra Leone · Singapore · Slovakia · Slovenia · Spain · Sri Lanka · St Maarten · Sweden · Switzerland · Taiwan · Tajikistan · Thailand · TimorLeste · Tunisia · Turkmenistan · United Arab Emirates · United Kingdom · United States · United States Virgin Islands · Uruguay · Uzbekistan · Virgin Islands British · Zambia |
| **Businesses** · Send · OUR | 46 countriesAustralia · Austria · Bahamas · Bahrain · Belgium · Bermuda · Brazil · Canada · Cayman Islands · Croatia · Cyprus · Czech Republic (Czechia) · Denmark · Estonia · Finland · France · Germany · Greece · Hong Kong · Hungary · Ireland · Israel · Italy · Latvia · Liechtenstein · Lithuania · Luxembourg · Malta · Mexico · Netherlands · Norway · Philippines · Poland · Portugal · Romania · San Marino · Serbia · Singapore · Slovakia · Slovenia · South Korea · Spain · Sweden · Switzerland · United Arab Emirates · United Kingdom |
| **Businesses** · Send · SHA | 148 countriesAmerican Samoa · Andorra · Angola · Anguilla · Antigua and Barbuda · Argentina · Armenia · Aruba · Australia · Austria · Azerbaijan · Bahamas · Bahrain · Belgium · Belize · Benin · Bermuda · Bolivia · Brazil · Brunei Darussalam · Bulgaria · Canada · Cape Verde · Cayman Islands · Chad · Chile · Colombia · Comoros · Cook Island · Costa Rica · Cote D'Ivoire · Croatia · Curacao · Cyprus · Czech Republic · Denmark · Djibouti · Dominica · Dominican Republic · Ecuador · Egypt · El Salvador · Equatorial Guinea · Estonia · Eswatini · Faroe islands · Fiji · Finland · France · Gabon · Georgia · Germany · Ghana · Gibraltar · Greece · Guam · Guatemala · Guernsey · Guyana · Honduras · Hong Kong SAR, China · Hungary · Iceland · India · Indonesia · Ireland · Isle of Man · Israel · Italy · Japan · Jersey · Kazakhstan · Kiribati · Korea, South · Kyrgyzstan · Laos · Latvia · Lesotho · Liechtenstein · Lithuania · Luxembourg · Macao SAR, China · Malawi · Malaysia · Maldives · Malta · Marshall Islands · Martinique · Mauritania · Mauritius · Mexico · Moldova · Monaco · Mongolia · Montenegro · Montserrat · Namibia · Nauru · Netherlands · New Caledonia · New Zealand · Niue · Norway · Oman · Palau · Panama · Papua New Guinea · Paraguay · Peru · Philippines · Poland · Portugal · Puerto Rico · Romania · Rwanda · Saint Helena, Ascension and Tristan · Saint Kitts and Nevis · Saint Lucia · Saint Pierre and Miquelon · Saint Vincent and the Grenadines · Samoa · San Marino · Sao Tome and Principe · Saudi Arabia · Serbia · Sierra Leone · Singapore · Slovakia · Slovenia · Spain · Sri Lanka · St Maarten · Sweden · Switzerland · Taiwan · Tajikistan · Thailand · TimorLeste · Tunisia · Turkmenistan · United Arab Emirates · United Kingdom · United States · United States Virgin Islands · Uruguay · Uzbekistan · Virgin Islands British · Zambia |
| **Individuals** · Collect · OUR | 46 countriesAustralia · Austria · Bahamas · Bahrain · Belgium · Bermuda · Brazil · Canada · Cayman Islands · Croatia · Cyprus · Czech Republic (Czechia) · Denmark · Estonia · Finland · France · Germany · Greece · Hong Kong · Hungary · Ireland · Israel · Italy · Latvia · Liechtenstein · Lithuania · Luxembourg · Malta · Mexico · Netherlands · Norway · Philippines · Poland · Portugal · Romania · San Marino · Serbia · Singapore · Slovakia · Slovenia · South Korea · Spain · Sweden · Switzerland · United Arab Emirates · United Kingdom |
| **Individuals** · Collect · SHA | 148 countriesAmerican Samoa · Andorra · Angola · Anguilla · Antigua and Barbuda · Argentina · Armenia · Aruba · Australia · Austria · Azerbaijan · Bahamas · Bahrain · Belgium · Belize · Benin · Bermuda · Bolivia · Brazil · Brunei Darussalam · Bulgaria · Canada · Cape Verde · Cayman Islands · Chad · Chile · Colombia · Comoros · Cook Island · Costa Rica · Cote D'Ivoire · Croatia · Curacao · Cyprus · Czech Republic · Denmark · Djibouti · Dominica · Dominican Republic · Ecuador · Egypt · El Salvador · Equatorial Guinea · Estonia · Eswatini · Faroe islands · Fiji · Finland · France · Gabon · Georgia · Germany · Ghana · Gibraltar · Greece · Guam · Guatemala · Guernsey · Guyana · Honduras · Hong Kong SAR, China · Hungary · Iceland · India · Indonesia · Ireland · Isle of Man · Israel · Italy · Japan · Jersey · Kazakhstan · Kiribati · Korea, South · Kyrgyzstan · Laos · Latvia · Lesotho · Liechtenstein · Lithuania · Luxembourg · Macao SAR, China · Malawi · Malaysia · Maldives · Malta · Marshall Islands · Martinique · Mauritania · Mauritius · Mexico · Moldova · Monaco · Mongolia · Montenegro · Montserrat · Namibia · Nauru · Netherlands · New Caledonia · New Zealand · Niue · Norway · Oman · Palau · Panama · Papua New Guinea · Paraguay · Peru · Philippines · Poland · Portugal · Puerto Rico · Romania · Rwanda · Saint Helena, Ascension and Tristan · Saint Kitts and Nevis · Saint Lucia · Saint Pierre and Miquelon · Saint Vincent and the Grenadines · Samoa · San Marino · Sao Tome and Principe · Saudi Arabia · Serbia · Sierra Leone · Singapore · Slovakia · Slovenia · Spain · Sri Lanka · St Maarten · Sweden · Switzerland · Taiwan · Tajikistan · Thailand · TimorLeste · Tunisia · Turkmenistan · United Arab Emirates · United Kingdom · United States · United States Virgin Islands · Uruguay · Uzbekistan · Virgin Islands British · Zambia |
| **Individuals** · Send · OUR | 46 countriesAustralia · Austria · Bahamas · Bahrain · Belgium · Bermuda · Brazil · Canada · Cayman Islands · Croatia · Cyprus · Czech Republic (Czechia) · Denmark · Estonia · Finland · France · Germany · Greece · Hong Kong · Hungary · Ireland · Israel · Italy · Latvia · Liechtenstein · Lithuania · Luxembourg · Malta · Mexico · Netherlands · Norway · Philippines · Poland · Portugal · Romania · San Marino · Serbia · Singapore · Slovakia · Slovenia · South Korea · Spain · Sweden · Switzerland · United Arab Emirates · United Kingdom |
| **Individuals** · Send · SHA | Not available |
## Virtual Accounts
A virtual account is an FBO account at one of MoonPay Enterprise's banking partners, in the name of the customer.
| **Identifier** | **Description** | **Availability** |
| - | - | - |
| **vIBAN** | EU IBAN enabling localized payments as if using a traditional bank account. | ✅ |
| **AN** | US account and routing number for receiving domestic USD payments. USD virtual accounts also include SWIFT details for international deposits. | ✅ |
| **Sort code + account number** | UK sort code and account number for receiving domestic GBP payments via CHAPS and FPS. | ✅ |
Availability to Retail and Business customers depends on the customer's region.
| Region | USD Virtual Account | EUR Virtual Account | GBP Virtual Account |
| :- | :- | :- | :- |
| **USA** | Retail + Business | Retail + Business | Retail + Business |
| **Rest of World** | Retail + Business | Retail + Business | Retail + Business |
| **EU** | Business only | Retail + Business | Retail + Business |
| **UK** | Business only | Retail + Business | Retail + Business |
Virtual Accounts serve as payment target addresses and are not intended for holding funds permanently. These accounts work for first-party payment and third-party payment. US virtual accounts are available in every state except Missouri.
## Bank Cutoff Windows
| **Bank** | **Rail** | **Cutoff Time** | **Notes** |
| - | - | - | - |
| **Lead Bank** | Same-Day ACH | Mon-Fri, 10:00 AM / 2:15 PM / 3:00 PM ET | Three daily windows, each ahead of the matching FedACH deadline. Same-Day submissions after 3:00 PM ET go out as Standard (Next-Day) ACH. |
| **Lead Bank** | Wire (FedWire) | Mon-Fri, 4:00 PM CT | Wires submitted after cutoff are processed the next business day. FedWire operates 8:00 PM CT (prior day) to 6:00 PM CT. |
| **Lead Bank** | FedNow | No cutoff | FedNow settles in real time, 24 hours a day, including weekends and holidays. There is no submission window to miss. |
| **Lead Bank** | SWIFT | No cutoff | No submission cutoff. Delivery depends on the destination corridor and the correspondent bank. |
| **Erebor** | ACH | Mon-Fri, 3:30 PM ET | Outgoing ACH submitted after 3:30 PM ET is sent the next business day. Incoming ACH is processed several times each business day. |
| **Erebor** | Wire (domestic) | Mon-Fri, 6:00 PM ET | Wires are sent and received 9:00 AM to 6:00 PM ET. Wires received after 6:00 PM ET, or on a weekend or holiday, are processed the next business day. |
| **Erebor** | SWIFT | Mon-Fri, mid-afternoon ET | The submission window opens at 9:00 PM ET the prior evening. The exact cutoff depends on the destination currency and corridor. Wires are processed up to 6:15 PM ET, but an afternoon submission can reach the beneficiary the next business day. |
## ACH Processing Windows
The tables below show the Lead Bank ACH submission windows. The FedACH network deadlines are later (10:30 AM, 2:45 PM, and 4:45 PM ET for Same-Day ACH) because the bank needs time to assemble and transmit its file. For Erebor, see [Bank Cutoff Windows](#bank-cutoff-windows) above.
Same-Day and Standard (Next-Day) ACH each run through several daily submission windows. Submissions after the last window for a given day, or on a weekend or holiday, queue for the next available window.
All times are Eastern Time (ET).
**Same-Day ACH**
| **Cutoff** | **Settlement Time** |
| - | - |
| 10:00 AM ET | 1:00 PM ET (same business day) |
| 2:15 PM ET | 5:00 PM ET (same business day) |
| 3:00 PM ET | 6:00 PM ET (same business day) |
**Standard (Next-Day) ACH**
| **Cutoff** | **Settlement Time** |
| - | - |
| 10:00 AM ET | 8:30 AM ET (next business day) |
| 2:15 PM ET | 8:30 AM ET (next business day) |
| 4:15 PM ET | 8:30 AM ET (next business day) |
| 7:30 PM ET | 8:30 AM ET (next business day) |
| 10:15 PM ET | 8:30 AM ET (next business day) |
| 11:00 PM ET | 8:30 AM ET (next business day) |
To register a payout destination for any of these rails, see [Fiat Addresses](/fiat-addresses). For end-to-end timing of a transaction, see [Transaction Status](/transaction-status).
# End of Day Settlement
Source: https://dev.enterprise.moonpay.com/end-of-day-settlement
Batch fiat payouts into a single daily settlement instead of paying out each transaction individually.
By default, every offramp transaction triggers an immediate payout to the recipient bank account. **End of Day (EOD) Settlement** changes this behavior: individual payouts are accumulated throughout the day and settled in a single batch transfer at the end of the business day.
This is useful when a customer receives many small offramp transactions and prefers fewer, consolidated bank transfers rather than one transfer per transaction.
## How It Works
### Without EOD Settlement (default)
Each offramp transaction is processed independently. As soon as conversion completes, MoonPay Enterprise initiates a separate fiat transfer to the recipient bank account.
```
Transaction 1 (100 USDC → EUR) → Payout 1 (€91.20)
Transaction 2 (250 USDC → EUR) → Payout 2 (€228.00)
Transaction 3 (50 USDC → EUR) → Payout 3 (€45.60)
```
### With EOD Settlement
Transactions are converted as they arrive, but payouts are held and combined into a single transfer at the end of the business day.
```
Transaction 1 (100 USDC → EUR) ┐
Transaction 2 (250 USDC → EUR) ├→ Single Payout (€364.80)
Transaction 3 (50 USDC → EUR) ┘
```
## Enabling EOD Settlement
Set `batch_payout` to `true` when creating an offramp Autoramp. This setting applies to all transactions processed through that Autoramp.
`POST` `/api/autoramps`
This endpoint requires an `Idempotency-Key` header. Send the same key to safely retry a request without creating a duplicate autoramp. A request without one returns `400`.
### Request fields
| Field | Type | Required | Description |
| - | - | - | - |
| `batch_payout` | boolean | No | If `true`, payouts are batched daily instead of sent individually. Defaults to `false`. Fiat destinations only. |
`batch_payout` only applies to offramp Autoramps with a fiat destination. Sending `batch_payout: true` with a crypto destination (onramp or swap) returns `400` with `"batch_payout is only supported for fiat destination currencies"`, and no Autoramp is created.
### Request
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: " \
-H "X-API-Key: " \
-d '{
"source_currencies": [{
"type": "Crypto",
"token": "USDC",
"blockchain": "Ethereum"
}],
"destination_currency": {
"type": "Fiat",
"code": "EUR"
},
"recipient_account": {
"type": "Fiat",
"account_identifier": {
"type": "SEPA",
"iban": "DE89370400440532013000"
}
},
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"source_is_third_party": false,
"batch_payout": true
}'
```
### Response
```json 201 Created theme={null}
{
"id": "9a7b6c5d-e3f1-4a2b-8c7d-6e5f4a3b2c1d",
"status": "Authorized",
"batch_payout": true,
"deposit_rails": [
{
"type": "Crypto",
"chain": "Ethereum",
"address": "0xYourEthereumAddress"
}
],
"created_at": "2025-01-20T13:45:23Z"
}
```
```json 400 Bad Request theme={null}
"Missing Idempotency-Key header"
```
The example above is trimmed to the fields most integrations need. The full `Autoramp` object also returns `kind`, `source`, `recipient`, `customer_id`, `source_currencies`, `destination_currency`, `is_third_party`, `fee_profile_id`, and `quotes`.
## Transaction Lifecycle
When EOD Settlement is enabled, transactions follow a modified lifecycle where conversion happens immediately but the fiat payout is deferred until end of day.
| Step | What happens | `transaction_status` | `status` (deprecated) |
| - | - | - | - |
| Deposit received | Crypto arrives at the Autoramp deposit address | `ConversionInProgress`, or `FundsReviewInProgress` if the deposit goes to compliance review | `Pending`, or `InAmlReview` |
| Conversion | Crypto is converted to fiat at the current or locked rate | `ConversionInProgress` | `Pending` |
| Awaiting batch | Converted fiat amount is held for end-of-day settlement | `ConversionInProgress` | `PayoutPending` |
| Batch payout | All pending amounts are paid out in a single fiat transfer | `Completed` | `Completed` |
Every row above arrives as a `transaction_status` webhook event. The first row also emits a `transaction` event.
`transaction_status` stays `ConversionInProgress` while a converted amount waits for the batch. It does not distinguish "converting" from "converted, held for settlement". Read `status: PayoutPending` when you need to tell those apart.
A batched transaction never reports `PayoutInProgress`. When the batch payout confirms, it goes straight from `ConversionInProgress` to `Completed`, so do not wait for a payout-in-progress event that will not arrive.
`PayoutPending` is a value of the deprecated `status` field, not of `transaction_status`. See [Webhooks](/webhooks) for both enums.
Individual transactions are still tracked separately. Only the fiat payout step is batched, so you retain full visibility into every transaction via the API and webhooks.
## Webhooks
When EOD Settlement is enabled, webhook behavior changes slightly:
* **`transaction` and `transaction_status` events** are still emitted for each individual transaction as it is received and converted.
* The `transaction_status` for individual transactions will reflect the conversion state. The payout portion settles at end of day.
* A final `transaction_status` webhook with `transaction_status: "Completed"` is sent once the batched payout has been executed and confirmed.
## Fees
Fees come from the same [fee profile](/fees-and-invoices) regardless of whether EOD Settlement is enabled. The transaction fee and the network fee are calculated and applied per transaction.
The banking fee is charged once per batched payout rather than per individual transaction, which can reduce overall costs for high-frequency offramp flows.
## Constraints
* **Fiat destinations only.** `batch_payout: true` is rejected with `400` for crypto-to-crypto swaps and onramp Autoramps. It only applies when the destination is a fiat bank account (offramp).
* **Autoramp-level setting.** The batch behavior is configured per Autoramp. You can have some Autoramps with immediate payouts and others with EOD settlement for the same customer.
* **Cannot be changed after creation.** To switch between immediate and batched payouts, create a new Autoramp with the desired setting.
## When to Use EOD Settlement
| Scenario | Recommended |
| - | - |
| High-frequency offramps (many small transactions per day) | **Yes**. Reduces bank transfer volume and banking fees |
| Treasury management with daily reconciliation | **Yes**. One settlement per day simplifies accounting |
| Real-time payouts required by end customers | **No**. Use default immediate payouts |
| Low-frequency offramps (a few transactions per week) | **No**. Batching provides little benefit |
## Sandbox Testing
In Sandbox, you can simulate the full EOD Settlement flow.
Use the request above with `"batch_payout": true`.
```bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/sandbox/autoramp/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Approved"'
```
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/sandbox/transaction" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"autoramp_id": "",
"amount": "100"
}'
```
Repeat to create several transactions against the same Autoramp.
```bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/sandbox/transaction//state" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "state": "Completed" }'
```
In Sandbox, batch settlement is simulated. Transactions complete immediately when you advance their state rather than waiting for end of day.
# Customer in EDD
Source: https://dev.enterprise.moonpay.com/enhanced-due-diligence
Confirm what enhanced due diligence is asking for, guide the customer through it, or trigger EDD on a standard customer.
Enhanced due diligence means MoonPay Enterprise needs more than standard verification before approving a customer, typically evidence of where their money comes from. See [Proof of address](/proof-of-address) and [Source of wealth](/source-of-wealth) for exactly which documents and rules a customer needs to meet for each.
### 1. Find your customer
Open **Compliance** > **EDD**: highest priority, these customers can't be approved until the extra checks clear.
### 2. Confirm what is being asked
**Compliance** > **EDD** > click the applicable customer > **Help your customer**: each outstanding item is listed there as its own row, for example:
* [**Proof of address**](/proof-of-address): a recent document showing their name and address
* [**Source of wealth**](/source-of-wealth): documents showing where their funds come from
Then copy the applicable message and the **KYC & Terms link**, and send them to the customer.
### 3. Guide them through it
* **Expand** either guidance item for a ready-to-send message listing the accepted documents and rules, with its own copy button. Send it alongside **KYC & Terms link** to take the customer into the EDD flow.
* **Upload documents**: if they've already emailed you the document, submit it for them instead.
### 4. Escalate a standard customer into EDD
For a customer not yet in EDD, **Trigger EDD** starts the enhanced process. It does not appear for customers already in EDD, because there is nothing to start. The customer then resubmits documents through the EDD flow before they can be approved, so only trigger it when EDD is required for that customer.
The API uses "source of funds" naming in two places: `source_of_funds_proof` carries the document in the EDD questionnaire, and `source_of_funds` is a business verification field. For an individual customer, what's being requested is **source of wealth** evidence either way.
# Error Codes
Source: https://dev.enterprise.moonpay.com/errors
Machine-readable codes for failed identification v2 submissions, and the rejection codes in customer requirements responses.
This page covers two code sets: [identification error codes](#identification-error-codes), returned as `400` errors by `POST /api/customers/{id}/identifications/v2`, and [customer requirement codes](#customer-requirement-codes), returned inside a `200` response by `GET /api/customers/{id}/requirements`.
## Identification error codes
`POST /api/customers/{id}/identifications/v2` validates `Business`, `Person`, and `Token` submissions synchronously. From `X-API-Version: 2026-08-01`, a failed submission returns `400` with an array of error objects. Earlier and unversioned callers get a plain string. See [API Versions](/versioning).
```json theme={null}
[
{
"code": "tin_required",
"message": "Beneficiary at index 1 is missing a tax identification number",
"docs_url": "https://docs.iron.xyz/errors#tin_required",
"beneficiary_index": 1
}
]
```
Always iterate the array. Most checks return one entry, but the four document checks (`company_details_required`, `legal_presence_required`, `ownership_structure_required`, `control_structure_required`) report every missing group at once.
Stable identifier from the table below. Match on this.
Details for this occurrence, such as the exact field or beneficiary. It can change without notice, so don't match on it.
Link to the code's row on this page: `https://docs.iron.xyz/errors#`.
Zero-based index into `company_data.beneficiaries`. Set only on `Business` errors tied to one beneficiary.
`Link` identifications only open a hosted flow, so they skip this validation. The one exception: `with_edd: true` on a `Business` customer returns `invalid_request`.
| Code | Cause | Fix |
| - | - | - |
| `invalid_request` | A field is empty, badly formatted (email, phone, country code, date), too large, or over a count limit. | Check `message` for the field. Add the same check client-side. |
| `selfie_required` | A `Director` beneficiary, or a `Representative` outside the US, has no selfie or `selfie_collected_at`. `Business` only: a `Person` missing a selfie returns `invalid_request`. | Submit a selfie with `selfie_collected_at`. |
| `selfie_expired` | The `Person` selfie is older than 6 months. | Capture a new selfie. |
| `tin_required` | A required tax field is missing. On a `Business`, the company's tax ID, or the tax ID for any `Director` or `UltimateBeneficialOwner`, a `Representative` outside the US, and a sole proprietorship's beneficiary. For a beneficiary tax-resident in a jurisdiction that doesn't issue tax IDs, `identity.identity_number` takes the tax ID's place. On a `Person`, the tax ID when the identity document is US-issued, or `tax_residence_country` when a tax ID is sent with a non-US document. From API version `2026-10-15`, `tax_residence_country` on every `Person`. | `Business`: send the missing `tax_identification_number`, or `identity.identity_number` when the message asks for it. See [Tax ID where none is issued](/kyb#tax-id-where-none-is-issued). `Person`: send `tax_identification_number`, plus `tax_residence_country` if the document isn't US. |
| `identity_document_rejected` | The document isn't accepted for a `Business` beneficiary. Outside the US, `ResidencePermit` isn't accepted. US companies, sole proprietorships included, accept `Passport` or `NationalId` from any country, and `DriverLicense` or `ResidencePermit` only if US-issued. `DigitalDriversLicense` is `BRA` only, also on a `Person`. An expired document also returns this code. | Resubmit with an accepted, valid document. |
| `identity_document_images_identical` | On a `Person`, the front and back images are the same file. | Upload distinct front and back images. |
| `structure_invalid` | The beneficiary structure breaks a rule. A sole proprietorship needs exactly one person beneficiary owning 100%. Other companies need a root `Director`. Ownership is claimed but no qualifying beneficiary holds 10% or more. Ownership counts as claimed when an owner or company beneficiary is listed without `share_percentage`, or a company beneficiary or `Representative`-only beneficiary holds 10% or more. Stakes under 10% don't count, so dispersed ownership passes. Before `2026-10-15`, non-US `PrivateFoundation`s are exempt. From `2026-10-15`, a `Control` UBO covers the gap, unless a nested owner reaches 10% indirectly. Qualifying shares add up to more than 100%. From `2026-10-15`, a company that isn't a sole proprietorship also needs at least one root `UltimateBeneficialOwner` person. | Check `message` and fix `company_data.beneficiaries`. From `2026-10-15`, submit the controlling person as a `Control` UBO if no one qualifies by ownership. |
| `attestation_invalid` | A US `Business`, sole proprietorships included, doesn't have exactly one root `Director` person with `attesting_party: true` and a past `attested_at`. | Check `message` for the problem. |
| `cdd_certification_invalid` | Iron can't generate the US beneficial-ownership certification from the submitted data, usually because of a missing ID or tax number. | Add the data named in `message`. See [Company Documents](/kyb#company-documents). |
| `business_description_insufficient` | `business_model_description` is under 80 characters after trimming. | Expand the description. |
| `industry_required` | `business_details.industry` is missing. Required from `2026-08-01`. `industry_type` alone no longer works. | Send `industry`. See [Business Questionnaire](/kyb#business-questionnaire). |
| `miscellaneous_industry_deprecated` | `industry` is `Miscellaneous`. | Pick a specific [`industry` value](/kyb#industry-values). |
| `intended_use_deprecated` | `intended_use` is `PaymentToFriendsFamilyorOthers`. Rejected from `2026-10-15`. | Send `PaymentToFriendsAndFamily` or `PaymentToThirdParties`. |
| `prohibited_industry` | The resolved industry is prohibited. | None. See [Prohibited Industries](/kyb#prohibited-industries). |
| `foundation_intended_use_not_supported` | A `PrivateFoundation` has an unsupported `purpose_and_intended_use`. | See [Foundations](/kyb#foundations). |
| `funds_flow_disclosure_required` | `sends_funds_on_customer_behalf: true` without full disclosure. | Send a description, a flow-of-funds diagram, and a source-of-funds document. |
| `license_details_required` | `has_licenses_or_registrations: true` without `license_details`. | Send `license_details`. |
| `company_details_required` | Missing Company Details document: state registry excerpt, certificate of incorporation, or certificate of good standing. A non-US sole proprietorship specifically needs `IncorporationCert`. | Upload one. `beneficiary_index` shows a nested company. See [Company Documents](/kyb#company-documents). |
| `legal_presence_required` | Non-US root companies only. Missing memorandum or articles of incorporation. | Upload one. |
| `ownership_structure_required` | Missing ownership document: shareholder registry, information statement, trust agreement, or certificate of incumbency. Applies to root and nested companies. US root companies get this from Iron's generated certification. | Upload one. |
| `control_structure_required` | Non-US root companies only. Missing directors registry, information statement, trust agreement, or certificate of incumbency. | Upload one. |
| `ubo_country_prohibited` | A UBO's residential country is prohibited for KYB. An unrated country doesn't trigger this. | None. The business can't onboard with this UBO at this address. |
| `ubo_share_below_threshold` | A UBO-only beneficiary is below the disclosure threshold: 25%, or 10% if nobody at that level reaches 25%. From `2026-10-15`, also fires for `ubo_subtype: "Ownership"` below the threshold, even with another role. `Control` UBOs are exempt. | Submit them under their other role, or remove the UBO role. |
| `ubo_subtype_invalid` | `ubo_subtype` or `nature_of_control` is missing or inconsistent. From `2026-10-15`, required on root-level UBOs; nested UBOs can omit it. Rejected on earlier versions. A root-level `Ownership` UBO needs `share_percentage`. `Control` needs `nature_of_control` (max 255 characters), no `share_percentage`, and a root-level person at a company that isn't a sole proprietorship. | Check `message` and `beneficiary_index`. |
| `proof_of_address_required_for_edd` | A `Person` has `edd_questionnaire` but no proof of address. | Add a `ProofOfAddress` document. |
| `outsourcing_agreement_not_signed` | Your outsourcing agreement isn't signed. | Contact your partnership representative. |
| `uk_customers_not_enabled` | Your account isn't enabled for UK customers. | Contact your partnership representative. |
| `country_not_supported` | The company's `country` isn't supported for business onboarding. | None. See [Countries](/country). |
| `duplicate_upload_blocked` | The provider blocked a byte-identical recent upload. Transient. | Retry shortly. |
| `provider_rejected_document` | The provider rejected a document: corrupted, too blurry, or the wrong type. | Check `message` and resubmit a corrected file. |
| `reuse_rejected` | A `Token` reuse failed: the identity isn't approved, the email doesn't match, or it fails an age restriction. | Run a fresh identification. |
| `all_deposit_rails_blocked` | On a `Person`, every deposit rail configured for your account is blocked for this applicant. No identification is created. One entry per blocked rail, with the reason in `message`. | Check `message` for each rail's reason. |
## Customer requirement codes
`GET /api/customers/{id}/requirements` with `X-API-Version: 2026-10-01` or later reports problems with a customer's submission as data, not as an HTTP error. The request returns `200`, and each problem is an entry in the `issues` array. These codes are separate from the identification error codes above: they describe why a requirement isn't satisfied yet, not why a request failed.
Each issue lists review rejections in `reject_reasons[].error_code`, and per document step in `steps[].reject_reasons[].error_code`. Values come from [Verification rejection codes](#verification-rejection-codes).
```json theme={null}
{
"identification_id": "4b0f6c1e-8d2a-4f7e-9a3c-2e5d7b1f6a90",
"issues": [
{
"code": "kyc_identity",
"description": "Identity documents required for personal verification.",
"review_comment": null,
"retryable": true,
"reject_reasons": [
{ "error_code": "verification_screenshots", "description": "An original image is required instead of a screenshot." }
],
"steps": [
{
"id": "9e2c4a71-3b5f-4d8e-a6c0-1f7b2d9e5c34",
"name": "PASSPORT",
"description": "A passport used to verify a person's identity.",
"review_comment": null,
"reject_reasons": [
{ "error_code": "verification_screenshots", "description": "An original image is required instead of a screenshot." }
]
}
]
}
]
}
```
Switch on `error_code`. The `description` text can change without notice. Use `retryable` on the issue to decide whether the customer can fix and resubmit: `true` permits a correction, `false` is terminal, and `null` means retryability is unknown. See [Checking Outstanding Requirements](/onboarding#checking-outstanding-requirements) for the full response and customer-facing feedback guidance.
### Verification rejection codes
Returned in `reject_reasons[].error_code` when the verification review rejects a requirement or one of its document steps. A review reason without a mapped code returns `verification_unknown_rejection`, so handle that value as a fallback. Screening, fraud, and policy rejections aren't returned on this endpoint, so an issue can come back with an empty `reject_reasons` array.
| Code | Meaning |
| - | - |
| `verification_additional_document_required` | Additional supporting documentation is required. |
| `verification_age_requirement_mismatch` | The age requirement is not satisfied. |
| `verification_applicant_interrupted_interview` | The verification interview was not completed. |
| `verification_back_side_missing` | The document's reverse side is missing. |
| `verification_bad_avatar` | The profile image does not meet requirements. |
| `verification_bad_face_matching` | The submitted facial images could not be matched. |
| `verification_bad_proof_of_address` | Address evidence does not meet quality requirements. |
| `verification_bad_proof_of_identity` | Identity evidence does not meet quality requirements. |
| `verification_bad_proof_of_payment` | Payment evidence does not meet quality requirements. |
| `verification_bad_selfie` | The selfie does not meet quality requirements. |
| `verification_bad_video_selfie` | The video selfie does not meet quality requirements. |
| `verification_black_and_white` | Color document images are required. |
| `verification_blurred_photo` | The image is too blurred to review. |
| `verification_check_unavailable` | A required verification check is temporarily unavailable. |
| `verification_company_data_mismatch` | Company information differs from registry records. |
| `verification_company_inactive_entity` | Registry records do not show an active entity. |
| `verification_company_incorrect_data` | Company information differs from supporting evidence. |
| `verification_company_not_defined_beneficiaries` | Beneficial ownership has not been established. |
| `verification_company_not_defined_ownership_structure` | Ownership information is missing or inconsistent. |
| `verification_company_not_defined_representatives` | Company representatives have not been identified. |
| `verification_company_not_defined_structure` | The company's control structure has not been established. |
| `verification_company_not_validated_beneficial_owners` | Beneficial-owner identity verification is incomplete. |
| `verification_company_not_validated_beneficiaries` | Beneficial-owner information has not been validated. |
| `verification_company_not_validated_directors` | Director verification is incomplete. |
| `verification_company_not_validated_representatives` | Representative verification is incomplete. |
| `verification_connection_interrupted` | The verification interview connection was interrupted. |
| `verification_custom_rejection` | The reviewer supplied a custom rejection reason. |
| `verification_db_data_mismatch` | Submitted information differs from verification records. |
| `verification_db_data_not_found` | Submitted information could not be found in verification records. |
| `verification_db_no_data_source` | No configured source supports this information check. |
| `verification_digital_document` | An accepted original document image is required. |
| `verification_document_damaged` | Document damage prevents acceptance. |
| `verification_document_expired` | The document is outside the accepted validity period. |
| `verification_document_missing` | A required document was not provided. |
| `verification_document_page_missing` | Required document pages are missing. |
| `verification_document_unsuitable` | The document type or certification does not meet requirements. |
| `verification_duplicate` | A duplicate verification profile was identified. |
| `verification_experience_requirement_mismatch` | The required experience criterion is not satisfied. |
| `verification_foreigner` | The applicable country or residency requirement is not satisfied. |
| `verification_front_side_missing` | The document's front side is missing. |
| `verification_gps_as_poa_skipped` | Location verification was skipped without alternative address evidence. |
| `verification_id_invalid` | The identity document is not valid for verification. |
| `verification_incompatible_language` | A document translation is required. |
| `verification_incomplete_document` | Required document information is incomplete or obscured. |
| `verification_incorrect_social_number` | The supplied tax or social identification number could not be validated. |
| `verification_low_quality` | Image quality prevents a reliable review. |
| `verification_more_documents_required` | Further documents are required to finish the review. |
| `verification_not_all_checks_completed` | Required verification checks remain incomplete. |
| `verification_not_document` | The uploaded content is not acceptable verification evidence. |
| `verification_other_rejection` | The reviewer supplied an unclassified rejection reason. |
| `verification_outdated_document_version` | A more recent document version is required. |
| `verification_problematic_applicant_data` | Applicant information could not be reconciled with supporting evidence. |
| `verification_requested_data_mismatch` | Submitted information differs from document information. |
| `verification_screenshots` | An original image is required instead of a screenshot. |
| `verification_selfie_mismatch` | The submitted profile image does not match identity evidence. |
| `verification_selfie_with_payment` | A selfie containing the requested supporting evidence is required. |
| `verification_spam` | The submission does not meet verification submission requirements. |
| `verification_unfilled_id` | Required signatures or stamps are missing. |
| `verification_unknown_rejection` | The review returned a rejection reason that is not yet classified. |
| `verification_unsatisfactory_photos` | Submitted images do not meet review requirements. |
| `verification_unsuitable_env` | The interview environment does not meet verification requirements. |
| `verification_unsupported_language` | The interview requires a supported language. |
| `verification_wrong_address` | The declared address does not match supporting evidence. |
| `verification_wrong_user_region` | The applicant's region is unsupported for this verification route. |
# Fees and Invoices
Source: https://dev.enterprise.moonpay.com/fees-and-invoices
Configure the fees charged to your customers and understand how MoonPay Enterprise settles them.
Each transaction can include a transaction fee, banking fee, and network fee. Your fee profile controls what the customer pays. Your invoicing profile controls what you owe MoonPay Enterprise.
## Fee Fields
Quote and transaction responses return the following fee fields:
| Field | Description |
| - | - |
| `transaction_fee` | The percentage-based fee deducted immediately from the transaction. It includes the `iron_fee` when `iron_fee_settlement` is `deductedImmediately`, plus your partner fee. It excludes banking and network fees. |
| `iron_fee` | The fee you owe MoonPay Enterprise under your agreement, regardless of when it is settled. |
| `partner_fee` | The fee you charge the customer. Set it with `transaction_fee_bips` when you create a transactional fee profile. |
| `banking_fee` | The banking rail fee deducted immediately from the transaction. |
| `network_fee` | The blockchain network fee deducted immediately from the transaction. |
| `total_fee` | The total fee calculated for the transaction, including fees that are settled later. Do not use this field as the amount charged to the customer. |
Banking and network fees that are settled later remain available in `network_fees_with_settlement`, together with their settlement method.
## Fee Profiles
MoonPay Enterprise uses two fee profile types:
* The **invoicing fee profile** contains the fees you owe MoonPay Enterprise. We configure this profile based on your agreement.
* A **transactional fee profile** contains the fees you charge your customers. You can create multiple profiles and assign them to different customers or Autoramps.
When you create a transactional fee profile with `POST /api/fee-profiles`, its Iron fee settings are inherited from your invoicing fee profile.
| Request field | What it controls |
| - | - |
| `transaction_fee_bips_decimal` | Your percentage fee charged to the customer, in basis points, with fractions allowed (for example `"12.5"`). `100` bps equals `1%`. This fee is deducted immediately. Takes precedence over `transaction_fee_bips`. |
| `transaction_fee_bips` | Deprecated. The same fee rounded to a whole basis point. Still required on every request. |
| `banking_fee_configs` | The fixed fee charged for each configured banking rail and transfer direction. Banking fees created through this endpoint are deducted immediately. |
| `network_fee_settlement` | Whether the blockchain network fee is deducted immediately or settled on your monthly invoice. |
The API does not let you change the Iron fee, its settlement method, the minimum fee, or whether network fees are enabled. Contact MoonPay Enterprise to change these invoicing settings.
## Settlement Methods
Fee settlement has two possible values:
* `deductedImmediately`: The fee is deducted from the transaction amount. The customer receives the net amount.
* `settledLater`: The fee is not deducted from the transaction. It is added to your monthly invoice.
Partner fees created with `transaction_fee_bips` are configured for immediate deduction. You choose `network_fee_settlement` when you create the transactional fee profile. MoonPay Enterprise configures the Iron fee settlement method on your invoicing profile.
## Monthly Invoicing
The `transaction_fee` collected from your customer is netted against the `iron_fee` you owe MoonPay Enterprise. For percentage-based fees calculated on the same transaction amount:
| Comparison | Invoice result |
| - | - |
| `transaction_fee` is higher than `iron_fee` | The difference is payable to you. |
| `transaction_fee` equals `iron_fee` | The fees offset each other. There is nothing left to settle for these fees. |
| `transaction_fee` is lower than `iron_fee` | You subsidize the difference. The balance is added to your invoice. |
Banking fees, network fees, minimum commitments, and other contracted charges are settled separately and also appear on the monthly invoice when applicable.
# Fiat Addresses
Source: https://dev.enterprise.moonpay.com/fiat-addresses
Register and manage bank accounts for payouts. Supports multiple payment rails including SEPA, ACH, Wire, and SWIFT.
## Overview
Fiat addresses represent bank accounts that can receive payouts from your Autoramps. Before initiating an offramp, you must register the destination bank account.
MoonPay Enterprise supports multiple payment rails:
| Type | Description | Currency |
| - | - | - |
| SEPA | European payments via IBAN | EUR |
| ACH | US domestic transfers | USD |
| Wire | US wire transfers | USD |
| SWIFT | International transfers | USD |
| CHAPS | UK high-value payments | GBP |
| FPS | UK Faster Payments | GBP |
RTP (US Real-Time Payments) is not yet supported — support is coming soon.
## Register a bank account
Register a bank account for a customer. The request structure varies based on the payment rail.
### SEPA (Europe)
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/fiat \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"currency": { "code": "EUR" },
"label": "Primary Business Account",
"bank_details": {
"recipient": {
"type": "Business",
"name": "Acme Corp GmbH"
},
"provider_name": "Deutsche Bank",
"provider_country": { "code": "DE" },
"account_identifier": {
"type": "SEPA",
"iban": "DE89370400440532013000"
},
"address": {
"street": "Crypto Street 123",
"city": "Berlin",
"state": "Berlin",
"country": { "code": "DE" },
"postal_code": "10115"
},
"is_third_party": false
}
}'
```
### ACH (US Domestic)
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/fiat \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174001" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"currency": { "code": "USD" },
"label": "Jane Doe Checking",
"bank_details": {
"recipient": {
"type": "Individual",
"given_name": "Jane",
"family_name": "Doe"
},
"provider_name": "Chase Bank",
"provider_country": { "code": "US" },
"account_identifier": {
"type": "ACH",
"routing_number": "021000021",
"account_number": "123456789012"
},
"address": {
"street": "123 Main Street",
"city": "New York",
"state": "NY",
"country": { "code": "US" },
"postal_code": "10001"
},
"email_address": { "email": "finance@acme.com" },
"phone_number": "+1-212-555-0123",
"is_third_party": false
}
}'
```
### SWIFT (International)
```bash Bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/addresses/fiat \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174002" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"currency": { "code": "USD" },
"label": "International Account",
"bank_details": {
"recipient": {
"type": "Business",
"name": "Acme Global Ltd"
},
"provider_name": "HSBC",
"provider_country": { "code": "GB" },
"account_identifier": {
"type": "SWIFT",
"bic": "HSBCGB2L",
"account_number": "12345678"
},
"address": {
"street": "10 Downing Street",
"city": "London",
"state": "England",
"country": { "code": "GB" },
"postal_code": "SW1A 2AA"
},
"bank_address": {
"street": "8 Canada Square",
"city": "London",
"state": "England",
"country": { "code": "GB" },
"postal_code": "E14 5HQ"
},
"is_third_party": false
}
}'
```
SWIFT payouts are available in USD for business customers, to both first-party and third-party accounts. SWIFT payouts for individual customers are coming soon. For inbound SWIFT deposits, see [Currencies and Rails](/currencies-and-rails).
### Request fields
| Field | Required | Description |
| - | - | - |
| `customer_id` | Yes | The customer's UUID |
| `currency` | Yes | Object with a `code` property (e.g. `{ "code": "EUR" }`) |
| `label` | No | Optional label for the account |
| `bank_details.recipient` | Yes | Object with a `type` discriminator. `Business`: requires `name`. `Individual`: requires `given_name` and `family_name`. |
| `bank_details.provider_name` | Yes | Bank name |
| `bank_details.provider_country` | Yes | Bank's country as `{ "code": "" }` |
| `bank_details.account_identifier` | Yes | Account details (varies by payment rail) |
| `bank_details.address` | Yes | Recipient's postal address. All fields including `state` are required. |
| `bank_details.email_address` | USD only | Required for USD accounts |
| `bank_details.phone_number` | USD only | Required for USD accounts |
| `bank_details.is_third_party` | Yes | Whether this is a third-party account |
| `bank_details.bank_address` | SWIFT only | Required for SWIFT transfers |
### Response
```json JSON theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"customer_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
"bank_details": { ... },
"label": "Primary Business Account",
"status": "Registered",
"currency": "EUR",
"bank_account_identifier": {
"type": "SEPA",
"iban": "DE89370400440532013000"
},
"country": "DE",
"bank_name": "Deutsche Bank",
"ownership_verified": true,
"is_third_party": false
}
```
## List bank accounts
Retrieve all registered bank accounts for a customer:
```bash Bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/addresses/fiat/{customer_id}" \
-H "X-API-Key: $API_KEY"
```
### Query parameters
| Parameter | Description |
| - | - |
| `cursor` | Pagination cursor from previous response |
| `page_size` | Number of results per page (max 100) |
| `search_term` | Filter by IBAN, label, or bank name |
### List by status
Retrieve all bank accounts with a specific status across all customers:
```bash Bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/addresses/fiat?status=Registered" \
-H "X-API-Key: $API_KEY"
```
## Get a specific bank account
```bash Bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/addresses/fiat/{customer_id}/{fiat_address_id}" \
-H "X-API-Key: $API_KEY"
```
## Delete a bank account
Soft-delete a registered bank account:
```bash Bash theme={null}
curl -X DELETE "https://api.sandbox.iron.xyz/api/addresses/fiat/{customer_id}/{fiat_address_id}" \
-H "X-API-Key: $API_KEY"
```
Deleted accounts cannot be used for new transactions. This operation cannot be undone.
## Address statuses
Beneficiary whitelisting is instant. There is no separate recipient verification step. Most accounts move from `RegistrationPending` to `Registered` within seconds. If they don't, the status tells you why.
| Status | Description |
| - | - |
| `RegistrationPending` | Registration in progress. Resolves within seconds in production. |
| `Registered` | Account is active and ready for payouts. |
| `RegistrationFailed` | The bank details were rejected. Common causes: invalid IBAN or routing number, mismatched recipient name, missing required fields. Fix the details and re-register. |
| `AuthorizationRequired` | In rare cases the beneficiary needs to be approved by the bank before payouts can run. |
| `AuthorizationFailed` | The beneficiary was rejected by compliance. The account cannot receive payouts. |
## Third-party accounts
To register a bank account that doesn't belong to the customer (a friend, family member, or business counterparty), set `is_third_party: true`. The registration flow is identical to first-party accounts: beneficiary whitelisting is instant, and the account is ready for payouts within seconds.
## Common errors
| Error | Cause |
| - | - |
| `400 Bad Request` | Invalid IBAN format, missing required fields, or invalid country |
| `404 Not Found` | Customer not found or does not belong to your partner |
| `409 Conflict` | Bank account already registered for this customer |
## Related guides
# Resume a stalled onboarding
Source: https://dev.enterprise.moonpay.com/finish-onboarding
Find customers who stalled partway through verification and send them a new KYC link, straight from the dashboard.
### 1. Find your customer
Open **Compliance** > **Incomplete**.
### 2. See what is missing
Click the customer's row, then open **Requirements** to see exactly which items are still missing.
### 3. Send them a link
Click **KYC & Terms link** to open the link dialog, covering verification and terms signing.
* **Help your customer** gives you a ready-to-send message naming what is missing, with its own copy button. Pair it with the link if you're messaging the customer yourself.
* Tick **Email the link to the customer** only if you'd rather MoonPay Enterprise send it for you.
* Click **Generate link** to mint the link and copy it straight to your clipboard.
### 4. Reuse it later
Once a link exists, the button changes to **Copy KYC & Terms link**, so following up with the customer reuses the same link instead of generating a new one.
# Hosted Identification
Source: https://dev.enterprise.moonpay.com/hosted
Create an identification with type Link and receive a URL to pass to your customer for completing verification. MoonPay Enterprise hosts the flow and handles review.
The returned URL is an interface hosted by MoonPay Enterprise. Once the customer has completed the flow, MoonPay Enterprise reviews the submission, performs verification, and decides on approval. MoonPay Enterprise is fully responsible for all compliance obligations, including final verification and ongoing monitoring.
Present and sign the terms and conditions before creating the identification. Fetch the document via `GET /api/customers/{id}/required-signings` and record acceptance via `POST /api/customers/{id}/signings`. The terms contain the data sharing agreement, so they must be signed before MoonPay Enterprise collects KYC data. Keep this flow reachable afterwards — customers must sign again when a new document is published. See [Terms and Conditions](/onboarding#terms-and-conditions).
## Create a Link Identification
`POST /api/customers/{id}/identifications/v2`
Full request and response schemas live in the [API reference](/reference/customer/create-customer-identification-v2).
The request body must include `"type": "Link"` to select the hosted flow. This is the discriminator that tells the API which identification method to use.
| Field | Required | Description |
| - | - | - |
| `type` | Yes | Must be `"Link"` for hosted identifications |
| `with_edd` | No | Set to `true` to start the full Enhanced Due Diligence process. Individual customers only |
| `Idempotency-Key` (header) | Yes | Unique UUID per request. Requests without it return `400` |
`with_edd` works for Individual customers only. Setting it to `true` on a Business customer returns `400` with the message `"with_edd is not supported for business customers"`.
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/customers//identifications/v2" \
-H "Content-Type: application/json; charset=utf-8" \
-H "IDEMPOTENCY-KEY: " \
-H "X-API-Key: " \
-d '{ "type": "Link" }'
```
```typescript TypeScript theme={null}
const response = await fetch(
"https://api.sandbox.iron.xyz/api/customers//identifications/v2",
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"Idempotency-Key": "",
"X-API-Key": "",
},
body: JSON.stringify({ type: "Link" }),
},
);
const identification = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.sandbox.iron.xyz/api/customers//identifications/v2",
headers={
"Content-Type": "application/json; charset=utf-8",
"Idempotency-Key": "",
"X-API-Key": "",
},
json={"type": "Link"},
)
identification = response.json()
```
`POST /api/customers/{id}/identifications/v2` requires an `Idempotency-Key` header. Requests without one return `400`. Retrying with the same key returns the original response. MoonPay Enterprise caches successful responses only, so a retry after an error executes the request again. Reusing a key with a different request body returns `409 Conflict`.
This endpoint is not a status refresh. If the customer's latest identification is `Pending`, `Processed`, `PendingReview`, or `Approved`, a call without `with_edd: true` returns that existing identification (same `id` and `url`) instead of creating another. A new record is created only when the latest identification is `Declined`, `Expired`, or `Archived`, or the customer has none, and the newest record then drives the customer's status. One exception: if the customer's latest identification is Sumsub-based (every hosted-flow identification is), calling again with `with_edd: true` upgrades that record for EDD instead of creating a new one.
Create one identification, then wait for it to reach a terminal status (`Approved`, `Declined`, `Expired`, or `Archived`) before creating another. To track progress, subscribe to the `identification_status` webhook or poll `GET /api/customers/{id}/identifications`. Never re-call this endpoint to refresh status.
**Example response (`201 Created`):**
```json JSON theme={null}
{
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"status": "Pending",
"url": "https://api.sandbox.iron.xyz/verify?",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
```
Pass the `url` to your customer. They complete the verification flow in the hosted interface, and MoonPay Enterprise handles the rest.
**Example errors:**
Most client errors (`400`, `401`, `403`, `404`, `429`) return a plain JSON string:
```json 404 Not Found theme={null}
"Create customer identification customer not found."
```
A `409` means the `Idempotency-Key` was already used for a request with a different body. It carries no explanatory body, just the status. Send a fresh UUID for each new identification, and reuse a key only when retrying that exact request. See [Idempotency](/idempotency).
The hosted flow does not reject duplicate payloads, so a `409` here never points at an existing identification. That check applies to the `Person` and `Business` flows only. See [KYC/KYB Sharing](/sharing#duplicate-submissions).
Server errors (`500`) return `{ "message": "...", "trace_id": "..." }`. Reference the `trace_id` when reporting issues.
In Sandbox, you can approve or reject the identification via `POST /api/sandbox/identification/{id}` instead of waiting for a manual review. See [Sandbox](/sandbox#update-identification-status).
# Idempotency
Source: https://dev.enterprise.moonpay.com/idempotency
Use the Idempotency-Key header to replay a POST safely. Covers what MoonPay Enterprise caches, what it does not, and why a retry after an error runs again.
Every `POST` under `/api` requires an `Idempotency-Key` header. The key lets you replay a request that timed out or failed in transit without creating the resource twice.
## Header: `Idempotency-Key`
Send a random UUID:
```bash theme={null}
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
```
The header name is case-insensitive, so `IDEMPOTENCY-KEY` and `idempotency-key` both work. The API reference writes it as `IDEMPOTENCY-KEY`.
A `POST` without the header returns `400` with the message `Missing Idempotency-Key header`. This check runs ahead of authentication, so it fires before an invalid API key is reported.
Idempotency applies to `POST` only. `PUT`, `PATCH`, and `DELETE` are not deduplicated and do not require the header. Sending one on those methods has no effect.
## What MoonPay Enterprise Stores
MoonPay Enterprise fingerprints each request as a hash of the **request body, the full path including the query string, and your identifying headers** (`Authorization`, `X-API-Key`, `X-Auth-Token`, `X-Sub-Partner-Id`, `X-API-Version`). The fingerprint is stored against your key alongside the response. Change any one of those parts on a replay and it is a different request.
MoonPay Enterprise runs the request. If the response status is `2xx`, MoonPay Enterprise stores the status code and response body against the key.
MoonPay Enterprise returns the stored status and body without running the operation again.
MoonPay Enterprise returns `409 Conflict`. The response carries no explanatory body.
**MoonPay Enterprise stores successful responses only.** If the first attempt returns `4xx` or `5xx`, nothing is stored, and a retry with the same key runs the operation from scratch. Retrying a failed request is not a safe replay.
This is how duplicate records get created. On `POST /api/customers/{id}/identifications/v2`, a partner who retries after an error can end up with several identifications for one customer, each one resetting the customer's status. Read the retry rules in [Onboarding Lifecycle](/onboarding) and [KYB](/kyb#resubmitting-and-correcting-a-submission) before you add retry logic to an onboarding flow.
## Key Considerations
* **One key per operation.** Generate a fresh UUID for each new operation, and reuse that same key only when retrying that exact request.
* **A new key means a new operation.** Retrying with a fresh UUID bypasses deduplication entirely and creates a second resource. This is the most common cause of duplicates.
* **Keys never expire.** There is no time window. A key you used last year still returns its stored response today.
* **The query string is fingerprinted.** Two requests to the same path with the same body but different query parameters are different requests. Replaying one key across both returns `409 Conflict`.
* **Idempotency does not make an endpoint an upsert.** It stops one request from running twice. It does not stop two different requests from creating two records.
## Example: Creating an Autoramp
### Request
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000" \
-H "X-API-Key: " \
-d '{
"source_currencies": [{
"type": "Fiat",
"code": "EUR"
}],
"destination_currency": {
"type": "Crypto",
"token": "USDC",
"blockchain": "Ethereum"
},
"recipient_account": {
"type": "Crypto",
"chain": "Ethereum",
"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
},
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"source_is_third_party": false
}'
```
### Response (First Request)
```json theme={null}
{
"id": "f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50",
"status": "Authorized",
"kind": "Onramp",
"created_at": "2025-01-20T12:34:56Z"
}
```
### Response (Retry With the Same Key and Body)
Byte for byte the stored response, including the original `created_at`:
```json theme={null}
{
"id": "f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50",
"status": "Authorized",
"kind": "Onramp",
"created_at": "2025-01-20T12:34:56Z"
}
```
A replayed response is served from the cache, so it carries the stored status and body without response headers. It has no `X-API-Version` echo. See [API Versioning](/versioning).
## Handling Errors Correctly
Because failures are never stored, decide what to do based on the status you got back:
| Response | What happened | What to do |
| - | - | - |
| `2xx` | Stored against your key | Safe to replay with the same key |
| `400`, `409`, `422` | Rejected, nothing stored | Fix the request. Do not blind-retry a create |
| `5xx` or a timeout | Unknown. The operation ran or it did not | Read current state with a `GET` before retrying |
On a timeout or `5xx` against a creating endpoint, check state before you retry. Poll `GET /api/customers/{id}/identifications` or the relevant collection to see whether the record already exists. A retry after a partial failure runs the operation again and creates a second record.
# Integration Models
Source: https://dev.enterprise.moonpay.com/integration-models
MoonPay Enterprise supports B2B, B2B2B, and B2B2C setups. Integrate via API, the Partner Dashboard, or offer MoonPay Enterprise App as a whitelabel frontend for your end customers.
MoonPay Enterprise supports three integration models: direct use (B2B), serving business customers (B2B2B), and serving individual consumers (B2B2C).
## At a Glance
| Model | Who uses MoonPay Enterprise's services | Example | Integration |
| - | - | - | - |
| **B2B** | Your company directly | Fintech using stablecoin rails for treasury | API or Dashboard |
| **B2B2B** | Your business customers | PSP offering stablecoin payouts to merchants | API or Dashboard + MoonPay Enterprise App |
| **B2B2C** | Your individual users | Neobank offering EUR virtual accounts to retail customers | API or Dashboard + MoonPay Enterprise App |
## B2B: Direct Use
Use MoonPay Enterprise for your own operations such as treasury, payroll, and settlement.
**Examples:**
* Cross-border payroll, converting USDC to EUR and paying employees via SEPA
* Exchange settlement through stablecoin-to-fiat [Offramps](/offramp)
* Corporate treasury across fiat and stablecoins via named virtual accounts ([Onramp](/onramp))
Integrate through the [API](/api-access) or the [Partner Dashboard](/partner-dashboard).
B2B is not exclusive. The same company can use MoonPay Enterprise directly and act as a Partner under B2B2B or B2B2C, onboarding its own customers or subsidiaries. See [Roles & Entities](/roles-entities).
## B2B2B: Serving Business Customers
Embed MoonPay Enterprise into your product. You act as a **Partner**. Your business customers become **Customers** of MoonPay Enterprise.
**Examples:**
* PSP offering stablecoin money transfers to merchant clients
* Banking-as-a-service platform enabling neobank clients to issue virtual EUR accounts
* Payroll provider letting companies pay contractors in stablecoins
Each business customer completes [KYB onboarding](/kyb) and gains access to [Onramp](/onramp), [Offramp](/offramp), [Stable to Stable](/stable-to-stable), and [Stablecoin Issuance](/issuance/create-your-stablecoin).
## B2B2C: Serving Individual Users
Embed MoonPay Enterprise into your consumer-facing product. You act as a **Partner**. Your individual users become **Customers** of MoonPay Enterprise.
**Examples:**
* Neobank offering named EUR and USD virtual bank accounts to retail customers
* Crypto wallet enabling users to onramp fiat to stablecoins via personal vIBAN
* Remittance app letting users send stablecoins and have recipients cash out to local bank accounts
Individual customers complete KYC through one of three frameworks:
| Framework | How it works |
| - | - |
| [KYC Reliance](/reliance-kyc-token-sharing) | MoonPay Enterprise relies on your existing KYC, shared via SumSub token sharing. Requires a tri-party agreement between MoonPay, your organization, and SumSub |
| [KYC/KYB Sharing](/sharing) | You collect KYC data and submit it via API, sharing it with MoonPay Enterprise |
| [Direct Onboarding](/hosted) | MoonPay Enterprise handles KYC through a hosted verification flow |
Regardless of the onboarding framework, every customer is a direct customer of MoonPay Enterprise for compliance purposes. See [Onboarding Lifecycle](/onboarding) for the full flow.
## MoonPay Enterprise App: Whitelabel Frontend
For B2B2B and B2B2C setups, MoonPay Enterprise offers **MoonPay Enterprise App**, a whitelabel frontend that your end customers (the last B or C) use directly.
MoonPay Enterprise App gives your customers account management, transaction history, onboarding, and access to MoonPay Enterprise's products. No custom UI needed. You manage configuration through the Partner Dashboard or API. MoonPay Enterprise App handles the end-customer experience.
**When to use MoonPay Enterprise App:**
* Go live without building a frontend
* End customers need direct access to onboarding, account management, and transaction history
* Keep the customer-facing experience up to date with new MoonPay Enterprise features automatically
## Customer email branding
Customer emails can use your company’s logo, sender name, and email domain. Contact the MoonPay Enterprise team to enable this option. We configure the sender and provide DNS records for you to publish to verify your domain and authorize email sending.
An optional custom MAIL FROM subdomain also lets emails use your domain for bounce handling and SPF alignment. Dashboard-related emails continue to come from MoonPay Enterprise.
## How to Integrate
Two integration paths. Use both simultaneously.
Create customers, onboard them, set up Autoramps, and monitor transactions programmatically.
Manage customers, Autoramps, API keys, and transactions at [app.enterprise.moonpay.com](https://app.enterprise.moonpay.com).
Most partners use the **API for programmatic workflows** (customer creation, Autoramp setup, webhook-driven monitoring) and the **Dashboard for operations** (reviewing customer status, managing API keys, inspecting transactions).
# Stablecoin issuance
Source: https://dev.enterprise.moonpay.com/issuance/create-your-stablecoin
Launch, manage, and scale a branded stablecoin with MoonPay as a fully regulated US issuer.
MoonPay lets you launch a US-regulated, branded stablecoin without building the regulatory, custody, or smart contract stack yourself. MoonPay USA LLC acts as issuer of record, handling licensing, reserve custody, compliance, and mint and burn execution. You keep the brand, the economics, and the user experience.
MoonPay USA LLC is the issuer of record for your stablecoin. At launch, your stablecoin can be bought and sold in 48 US states (excludes NY and CO).
Earn a share of reserve rewards on circulating supply, distributed as newly minted tokens on a cadence you set.
A solver network delivers USDC and USDT swaps with no slippage, no bridge delay, and no pre-seeded pools.
Live on the MoonPay Enterprise Virtual Accounts API, MoonPay Ramps, Swaps and Deposits from launch.
## How it works
Define token name, ticker, supported chains (EVM, Solana, or both), rewards recipient address, and access controls. MoonPay Enterprise deploys audited smart contracts on your behalf through MoonPay's partnership with M0.
Authorized counterparties send USD (wire, SWIFT, ACH), USDC, or USDT to MoonPay Enterprise. MoonPay Enterprise places funds in a segregated reserve account and mints the matching supply to the destination wallet.
MoonPay holds reserves in GENIUS-compliant assets. Reserve accounts are segregated and designed to protect holders in line with GENIUS regulations.
Rewards accrue per block to outstanding supply and compound continuously, then distribute as newly minted tokens to your designated recipient address.
MoonPay Enterprise burns the stablecoin, releases funds from the reserve account, and pays out. No minimums, no volume caps. USD wire, SWIFT, and ACH redemptions settle inside the mint and burn window below. USDC and USDT redemptions run any time.
The mint and burn window for USD wire, SWIFT, and ACH rails is Monday to Friday, 9:00 PM ET (previous day) to 5:00 PM ET. Requests received after 5:00 PM ET are processed at the start of the next window. Stablecoin-funded mints and redemptions (USDC, USDT) are not subject to this window.
## Next steps
Move between your stablecoin and USD, USDC, and USDT.
How MoonPay manages reserves and how to claim rewards.
Common questions about issuance, reserves, and rewards.
Launch a stablecoin backed 1:1 by PYUSD instead of USD reserves.
The primitive that powers mint, redeem, and swap.
## In the press
Bloomberg, Nov 2025
CoinDesk, Dec 2025
CoinDesk, Jan 2026
# MoonPay Stablecoin FAQ
Source: https://dev.enterprise.moonpay.com/issuance/faq
Common questions about issuance, reserves, rewards, and conversions.
The window for USD rails is Monday to Friday, 9:00 PM ET (previous day) to 5:00 PM ET. The Monday window opens at 9:00 PM ET on Sunday. Requests received after 5:00 PM ET are queued for the next window. USDC and USDT mint and redeem are not subject to this window.
Reserves are held exclusively in GENIUS-compliant assets: demand deposit accounts, US Treasuries (90 days or less to maturity), Treasury-collateralized reverse repos, and US Government Money Market Funds.
Reserves are held in segregated accounts, designed to protect holders in line with GENIUS regulations.
Rewards are set on-chain using M0 technology, applied per block to outstanding supply, and compounded continuously. They are distributed as newly minted tokens to a partner-designated address, on a configurable cadence.
No. There are no minimum mint or redeem volumes and no issuing or transaction volume caps.
MoonPay USA LLC, through the MoonPay Enterprise interface, converts between the issued stablecoin and USD, USDC, and USDT. Permissionless conversion to USDC and USDT is also available on-chain.
# Mint, Redeem and Swap
Source: https://dev.enterprise.moonpay.com/issuance/mint-redeem-swap
Move between your stablecoin and USD, USDC and USDT.
MoonPay Enterprise enables you to transact in three ways with your stablecoin. You can Mint your stablecoin via USD Wire, SWIFT and ACH. You can Redeem your stablecoin to USD via USD Wire, SWIFT and ACH. You can convert your stablecoin to and from USDC and USDT on the blockchains where both are supported. See [Stables and Blockchains](/stablecoins-and-blockchains) for current availability.
Mint, Redeem and Swap all use the [Autoramp](/autoramp) primitive. One endpoint creates a standing rule, MoonPay Enterprise derives the transaction kind from the currency pair, and every deposit against the rule runs through the standard [transaction lifecycle](/transaction-status). There is no separate issuance API. If you have integrated MoonPay Enterprise ramps or swaps, you already know how to mint and redeem.
## How Minting Works
When a user deposits fiat, MoonPay Enterprise automatically:
1. Detects the incoming deposit
2. Allocates funds to your stablecoin's reserve accounts
3. Mints equivalent tokens on-chain
4. Delivers tokens to the user's specified wallet address
Every token is backed at least 1:1 by equivalent fiat value in reserves.
## How Redeeming Works
When a user redeems their stablecoins, MoonPay Enterprise automatically:
1. Detects the incoming stablecoins on-chain
2. Burns the tokens, removing them from circulation
3. Releases equivalent fiat value from reserve accounts
4. Sends fiat to the user's bank account
# PYUSDx
Source: https://dev.enterprise.moonpay.com/issuance/pyusdx
PYUSDx is a DeFi-oriented issuance platform from MoonPay and M0, built with PayPal. Launch a stablecoin backed 1:1 by PYUSD, with programmable rewards and shared liquidity.
PYUSDx is a DeFi-oriented issuance platform built by MoonPay and M0 in partnership with PayPal. Every PYUSDx token is backed 1:1 by PayPal USD (PYUSD), and rewards are generated from the PYUSD reserves. You integrate through M0's protocol to launch your own stablecoin backed by PYUSDx. Availability limited by jurisdiction.
PYUSDx tokens (and any digital tokens issued on the PYUSDx platform) are not PayPal USD issued by Paxos Trust Company, NA or PayPal, are not a PayPal product or service, and are not otherwise affiliated with PayPal or Paxos Trust Company, NA. PYUSDx is not a regulated stablecoin and its regulatory classification varies by jurisdiction. Availability varies by jurisdiction and issuance model.
The infrastructure layer, built by the original architects of USDC and DAI. The network runs 15+ stablecoins across 20+ chains, with \$3B+ in on-chain flows.
A crypto payments group with 30M+ users and global on and off-ramp coverage. The operations layer behind PYUSDx.
One of the world's largest payments networks, with 400M+ users across PayPal and Venmo. The primary distributor of PYUSD.
## Why build on PYUSDx
You own the branding, fee logic, distribution, and rewards allocation for the stablecoin you build on top of PYUSDx.
Rewards from PYUSD reserves stream on-chain to your stablecoin. You control who receives them, how they split, and on what cadence.
MoonPay handles 1:1 minting and redemption against PYUSD, giving you and your partners a proven way to mint and redeem at scale.
Convert 1:1 on-chain between your stablecoin, PYUSD, and every other token built on PYUSDx.
## How minting works
When a user deposits PYUSD to mint PYUSDx, MoonPay Enterprise automatically:
1. Detects the incoming PYUSD deposit on-chain
2. Allocates PYUSD to the PYUSDx reserve account
3. Mints equivalent PYUSDx tokens on-chain
4. Delivers PYUSDx tokens to the user's specified wallet address
Every token is backed at least 1:1 by equivalent PYUSD value in reserves.
## How redeeming works
When a user redeems PYUSDx for PYUSD, MoonPay Enterprise automatically:
1. Detects the incoming PYUSDx deposit on-chain
2. Burns the PYUSDx tokens, removing them from circulation
3. Releases equivalent PYUSD value from reserve accounts
4. Delivers PYUSD tokens to the user's specified wallet address
Unlike fiat-backed issuance, PYUSDx mints and redeems against PYUSD, so the flow is on-chain crypto-to-crypto end to end. You can also convert 1:1 on-chain between PYUSDx, PYUSD, and any other token built on PYUSDx.
# Reserve Management and Rewards
Source: https://dev.enterprise.moonpay.com/issuance/reserves-and-rewards
Learn how MoonPay manages stablecoin reserves and how to claim rewards for your stablecoin.
## Reserve Management
MoonPay stablecoin reserves are held exclusively in GENIUS-compliant assets:
* Demand deposit accounts
* US Treasuries (90 days or less to maturity)
* Treasury-collateralized reverse repos
* US Government Money Market Funds
## Reserve Partners
MoonPay works with a changing set of stablecoin reserve partners. Contact MoonPay for details on current reserve partners.
## Claiming Rewards
MoonPay uses M0 technology to track stablecoin reward accrual on a per-block basis. Accrued rewards are distributed as newly minted tokens to your designated recipient address, on a cadence you configure.
# Business
Source: https://dev.enterprise.moonpay.com/kyb
Onboard business customers using MoonPay Enterprise's hosted Link flow or the programmatic Business API.
MoonPay Enterprise supports two methods for onboarding business customers through KYB (Know Your Business) verification. Both follow the same [onboarding lifecycle](/onboarding) and collect the same underlying compliance data.
Create a `Link` identification and receive a URL. Your customer completes KYB verification in MoonPay Enterprise's hosted interface. MoonPay Enterprise handles the entire collection, review, and approval process.
Submit all KYB data directly via the API with a `Business` identification. You collect and provide company info, beneficiaries, documents, and the compliance questionnaire programmatically.
## Hosted Link Flow
The simplest way to onboard a business customer. You create a `Link` identification and MoonPay Enterprise returns a URL where your customer completes the full KYB process through MoonPay Enterprise's hosted interface.
MoonPay Enterprise is fully responsible for collecting all required information, performing verification, and deciding on approval.
`POST /api/customers`
```bash theme={null}
curl --request POST \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{"name": "acme_corp", "email": "admin@acme.com", "customer_type": "Business"}' \
--url 'https://api.sandbox.iron.xyz/api/customers'
```
The customer is created in `SigningsRequired` status under `X-API-Version: 2026-08-01` or later, and in `IdentificationRequired` on earlier versions. See [Create a new customer](/onboarding).
Fetch the terms via `GET /api/customers/{id}/required-signings` and record acceptance via `POST /api/customers/{id}/signings` before creating the identification. The terms contain the data sharing agreement, so they come before KYB data collection.
`POST /api/customers/{id}/identifications/v2`
```bash theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{ "type": "Link" }' \
--url 'https://api.sandbox.iron.xyz/api/customers//identifications/v2'
```
The response includes a `url` field. Pass this URL to your customer to begin the hosted KYB flow.
The customer opens the link and completes the flow in MoonPay Enterprise's hosted interface, which collects company information, documents, beneficial ownership details, and the business questionnaire.
Once the submission is complete, MoonPay Enterprise reviews the data, performs verification, and decides on approval. Track progress via [webhooks](/webhooks) or by polling the identification status.
Once KYB is approved, MoonPay Enterprise checks that the terms have been signed. If not, the customer status is `SigningsRequired`: retrieve required signings, present them to the customer, and mark them as signed. See the [onboarding lifecycle](/onboarding) for details.
## Programmatic Business API
For partners who collect KYB data within their own application, you can submit all business verification data directly via the API. This requires a [KYC/KYB sharing agreement](/sharing) with MoonPay Enterprise.
You collect the company information, beneficial ownership details, identity documents, and the compliance questionnaire, then submit everything in a single API call.
`POST /api/customers`
```bash theme={null}
curl --request POST \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{"name": "acme_corp", "email": "admin@acme.com", "customer_type": "Business"}' \
--url 'https://api.sandbox.iron.xyz/api/customers'
```
Fetch the terms via `GET /api/customers/{id}/required-signings` and record acceptance via `POST /api/customers/{id}/signings` before creating the identification.
`POST /api/customers/{id}/identifications/v2`
Submit the full KYB payload with `type: "Business"`. Company identity, beneficiaries, and documents are nested under `company_data` (company fields sit under `company_data.company_info`). The company `phone` and the submitting device's `ip_address` are top-level, and `questionnaire` carries a `type` discriminator (`Generic` for all company types except non-US sole proprietorships, `SoleProp` for non-US sole proprietorships). The example below shows a complete submission with one beneficial owner:
From API version `2026-10-15`, every beneficiary with the `UltimateBeneficialOwner` role also sets `ubo_subtype`. Add `"ubo_subtype": "Ownership"` to those beneficiaries in the examples on this page. Earlier versions reject the field. See [Migrating to API Version 2026-10-15](#migrating-to-api-version-2026-10-15).
```bash Shell theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/customers//identifications/v2' \
--data-raw '{
"type": "Business",
"phone": "+4930123456789",
"ip_address": "8.8.8.8",
"company_data": {
"company_info": {
"company_name": "Acme Corp",
"registration_number": "12345678",
"country": "DE",
"incorporation_date": "2020-06-15",
"legal_address": "Friedrichstraße 123, 10117 Berlin",
"postal_address": "Friedrichstraße 123, 10117 Berlin",
"address": {
"street_1": "Friedrichstraße 123",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE"
},
"email": "info@acme.com",
"tax_identification_number": "DE123456789",
"website": "https://www.acme.com",
"company_type": "LimitedLiabilityCompany"
},
"beneficiaries": [
{
"roles": ["Director", "UltimateBeneficialOwner"],
"share_percentage": 100,
"beneficiary_info": {
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1985-03-22",
"email_address": "jane@acme.com",
"phone_number": "+4915112345678",
"tax_identification_number": "12345678901",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Munich",
"state": "Bavaria",
"country": "DE"
},
"address": {
"street_1": "Unter den Linden 42",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE"
},
"identity": {
"identity_country_code": "DE",
"identity_document_type": "Passport",
"identity_document_front": "",
"selfie": "",
"selfie_collected_at": "2026-07-01T12:00:00Z",
"nationality": "DE"
},
"proof_of_address": ""
}
}
],
"documents": [
{
"type": "IncorporationCert",
"country": "DE",
"file": ""
},
{
"type": "IncorporationArticles",
"country": "DE",
"file": ""
},
{
"type": "ShareholderRegistry",
"country": "DE",
"file": ""
},
{
"type": "DirectorsRegistry",
"country": "DE",
"file": ""
}
]
},
"questionnaire": {
"type": "Generic",
"business_details": {
"industry_type": "InformationTechnologyAndSoftware",
"business_model_description": "B2B SaaS platform for supply chain management, serving mid-market and enterprise logistics teams",
"main_business_countries": ["DE", "FR", "NL"],
"has_licenses_or_registrations": false
},
"source_of_funds": {
"source_of_funds": "VcPrivateEquity",
"source_of_funds_description": "Series B venture round led by a European growth fund"
},
"planned_activities": {
"purpose_and_intended_use": "ReceivingBusinessIncome",
"estimated_monthly_volume": "FiftyToOneHundredThousand",
"estimated_monthly_transactions": "TenToTwentyFive",
"sends_funds_on_customer_behalf": false
}
}
}'
```
```python Python theme={null}
import requests
import base64
from pathlib import Path
API_KEY = ""
CUSTOMER_ID = ""
BASE_URL = "https://api.sandbox.iron.xyz/api"
def encode_document(file_path: str) -> str:
return base64.b64encode(Path(file_path).read_bytes()).decode()
response = requests.post(
f"{BASE_URL}/customers/{CUSTOMER_ID}/identifications/v2",
headers={
"accept": "application/json; charset=utf-8",
"content-type": "application/json; charset=utf-8",
"idempotency-key": "",
"x-api-key": API_KEY,
},
json={
"type": "Business",
"phone": "+4930123456789",
"ip_address": "8.8.8.8",
"company_data": {
"company_info": {
"company_name": "Acme Corp",
"registration_number": "12345678",
"country": "DE",
"incorporation_date": "2020-06-15",
"legal_address": "Friedrichstraße 123, 10117 Berlin",
"postal_address": "Friedrichstraße 123, 10117 Berlin",
"address": {
"street_1": "Friedrichstraße 123",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE",
},
"email": "info@acme.com",
"tax_identification_number": "DE123456789",
"website": "https://www.acme.com",
"company_type": "LimitedLiabilityCompany",
},
"beneficiaries": [
{
"roles": ["Director", "UltimateBeneficialOwner"],
"share_percentage": 100,
"beneficiary_info": {
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1985-03-22",
"email_address": "jane@acme.com",
"phone_number": "+4915112345678",
"tax_identification_number": "12345678901",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Munich",
"state": "Bavaria",
"country": "DE",
},
"address": {
"street_1": "Unter den Linden 42",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE",
},
"identity": {
"identity_country_code": "DE",
"identity_document_type": "Passport",
"identity_document_front": encode_document("passport-front.jpg"),
"selfie": encode_document("selfie.jpg"),
"selfie_collected_at": "2026-07-01T12:00:00Z",
"nationality": "DE",
},
"proof_of_address": encode_document("proof-of-address.pdf"),
},
}
],
"documents": [
{
"type": "IncorporationCert",
"country": "DE",
"file": encode_document("registration.pdf"),
},
{
"type": "IncorporationArticles",
"country": "DE",
"file": encode_document("articles.pdf"),
},
{
"type": "ShareholderRegistry",
"country": "DE",
"file": encode_document("shareholders.pdf"),
},
{
"type": "DirectorsRegistry",
"country": "DE",
"file": encode_document("directors.pdf"),
},
],
},
"questionnaire": {
"type": "Generic",
"business_details": {
"industry_type": "InformationTechnologyAndSoftware",
"business_model_description": "B2B SaaS platform for supply chain management, serving mid-market and enterprise logistics teams",
"main_business_countries": ["DE", "FR", "NL"],
"has_licenses_or_registrations": False,
},
"source_of_funds": {
"source_of_funds": "VcPrivateEquity",
"source_of_funds_description": "Series B venture round led by a European growth fund",
},
"planned_activities": {
"purpose_and_intended_use": "ReceivingBusinessIncome",
"estimated_monthly_volume": "FiftyToOneHundredThousand",
"estimated_monthly_transactions": "TenToTwentyFive",
"sends_funds_on_customer_behalf": False,
},
},
},
)
identification = response.json()
print(identification)
```
```typescript TypeScript theme={null}
import fs from "fs";
const API_KEY = "";
const CUSTOMER_ID = "";
const BASE_URL = "https://api.sandbox.iron.xyz/api";
function encodeDocument(filePath: string): string {
return fs.readFileSync(filePath).toString("base64");
}
const response = await fetch(
`${BASE_URL}/customers/${CUSTOMER_ID}/identifications/v2`,
{
method: "POST",
headers: {
accept: "application/json; charset=utf-8",
"content-type": "application/json; charset=utf-8",
"idempotency-key": "",
"x-api-key": API_KEY,
},
body: JSON.stringify({
type: "Business",
phone: "+4930123456789",
ip_address: "8.8.8.8",
company_data: {
company_info: {
company_name: "Acme Corp",
registration_number: "12345678",
country: "DE",
incorporation_date: "2020-06-15",
legal_address: "Friedrichstraße 123, 10117 Berlin",
postal_address: "Friedrichstraße 123, 10117 Berlin",
address: {
street_1: "Friedrichstraße 123",
city: "Berlin",
state: "Berlin",
zip_code: "10117",
country: "DE",
},
email: "info@acme.com",
tax_identification_number: "DE123456789",
website: "https://www.acme.com",
company_type: "LimitedLiabilityCompany",
},
beneficiaries: [
{
roles: ["Director", "UltimateBeneficialOwner"],
share_percentage: 100,
beneficiary_info: {
first_name: "Jane",
last_name: "Smith",
date_of_birth: "1985-03-22",
email_address: "jane@acme.com",
phone_number: "+4915112345678",
tax_identification_number: "12345678901",
tax_jurisdiction_country_code: "DE",
birth_place: {
place: "Munich",
state: "Bavaria",
country: "DE",
},
address: {
street_1: "Unter den Linden 42",
city: "Berlin",
state: "Berlin",
zip_code: "10117",
country: "DE",
},
identity: {
identity_country_code: "DE",
identity_document_type: "Passport",
identity_document_front: encodeDocument("passport-front.jpg"),
selfie: encodeDocument("selfie.jpg"),
selfie_collected_at: "2026-07-01T12:00:00Z",
nationality: "DE",
},
proof_of_address: encodeDocument("proof-of-address.pdf"),
},
},
],
documents: [
{
type: "IncorporationCert",
country: "DE",
file: encodeDocument("registration.pdf"),
},
{
type: "IncorporationArticles",
country: "DE",
file: encodeDocument("articles.pdf"),
},
{
type: "ShareholderRegistry",
country: "DE",
file: encodeDocument("shareholders.pdf"),
},
{
type: "DirectorsRegistry",
country: "DE",
file: encodeDocument("directors.pdf"),
},
],
},
questionnaire: {
type: "Generic",
business_details: {
industry_type: "InformationTechnologyAndSoftware",
business_model_description:
"B2B SaaS platform for supply chain management, serving mid-market and enterprise logistics teams",
main_business_countries: ["DE", "FR", "NL"],
has_licenses_or_registrations: false,
},
source_of_funds: {
source_of_funds: "VcPrivateEquity",
source_of_funds_description:
"Series B venture round led by a European growth fund",
},
planned_activities: {
purpose_and_intended_use: "ReceivingBusinessIncome",
estimated_monthly_volume: "FiftyToOneHundredThousand",
estimated_monthly_transactions: "TenToTwentyFive",
sends_funds_on_customer_behalf: false,
},
},
}),
}
);
const identification = await response.json();
console.log(identification);
```
MoonPay Enterprise validates the submission synchronously. A complete submission is created as `Processed` and moves into verification. An incomplete submission — any country — is rejected with `400`. See [Incomplete Submissions](#incomplete-submissions). Track progress via [webhooks](/webhooks) or by polling the identification status.
Submit once. This endpoint is not an upsert, and every call that returns `201` creates a new identification record. Do not call it again to refresh status, or to correct a submission that was accepted. A `400` is the exception: nothing was created, so fixing the payload and submitting again is correct. See [Resubmitting and Correcting a Submission](#resubmitting-and-correcting-a-submission).
Same as the hosted flow. Once KYB is approved, MoonPay Enterprise checks that the terms have been signed. If not, the customer status is `SigningsRequired`: retrieve required signings, present them, and mark them as signed to activate the customer.
### Migrating to API Version 2026-10-15
API version `2026-10-15` changes how a `Business` submission describes who the ultimate beneficial owners (UBOs) are. This section covers what changes, how to update your payload, and what you see if you haven't yet.
Only `Business` submissions sent with `X-API-Version: 2026-10-15` or later are affected. If you send an earlier version or no version header, your submissions behave exactly as before. `Person` and `Token` submissions are not affected.
#### What changes
| | Before `2026-10-15` | From `2026-10-15` |
| - | - | - |
| A UBO | Role `UltimateBeneficialOwner` with a `share_percentage` | The same, plus `ubo_subtype: "Ownership"` |
| An individual who controls the company without owning it | Not expressible | Role `UltimateBeneficialOwner` with `ubo_subtype: "Control"` and a `nature_of_control` |
| A company where nobody owns enough to qualify | A `Director` alone was accepted | Submit the individual who controls the company as a `Control` UBO |
| The fields `ubo_subtype` and `nature_of_control` | Rejected | Accepted |
#### Migrate in four steps
1. Send `X-API-Version: 2026-10-15` on `POST /api/customers/{id}/identifications/v2`. The version is read per request, so you can move one request at a time.
2. For every beneficiary with the `UltimateBeneficialOwner` role, set `ubo_subtype` using the table below.
3. If no individual owns enough to qualify, add the individual who controls the company as a `Control` UBO. A `Director` with no `UltimateBeneficialOwner` role no longer satisfies this.
4. Submit in the sandbox first and check that the submission is accepted.
#### Which subtype to use
| Situation | `ubo_subtype` | `share_percentage` | `nature_of_control` |
| - | - | - | - |
| An individual owns 25% or more (or 10% or more when nobody reaches 25%) | `Ownership` | Required | Not allowed |
| An individual controls the company without owning it, for example through voting rights, a trust, or the right to appoint the board | `Control` | Not allowed | Required, up to 255 characters |
| A foundation, where a council member controls the company | `Control` | Not allowed | Required |
| A company owned by another company, where no individual reaches the threshold | `Control`, for the individual who controls it | Not allowed | Required |
#### Before and after
An owner needs one extra field:
```jsonc theme={null}
{
"roles": ["UltimateBeneficialOwner"],
"share_percentage": 40,
"ubo_subtype": "Ownership", // new
"beneficiary_info": { /* unchanged */ }
}
```
A company where nobody owns enough to qualify submits the individual who controls it:
```jsonc theme={null}
// Before: accepted with a Director only
{
"roles": ["Director"],
"beneficiary_info": { /* ... */ }
}
// From 2026-10-15: the same person also declares how they control the company
{
"roles": ["Director", "UltimateBeneficialOwner"],
"ubo_subtype": "Control",
"nature_of_control": "Member of the foundation council with voting rights",
"beneficiary_info": { /* ... */ }
}
```
#### Rules to remember
* `Control` is accepted only on root-level person beneficiaries of a company that isn't a sole proprietorship.
* A corporate beneficiary never counts as the UBO, whatever its roles. At least one person beneficiary must have the `UltimateBeneficialOwner` role.
* Inside a nested company's own `beneficiaries`, `ubo_subtype` is optional and only `Ownership` is accepted.
* The new fields appear in every dated version of the OpenAPI spec, but versions before `2026-10-15` reject them.
* For US companies, individuals submitted as `Control` are listed in their own section of the generated certification. Your submission doesn't change.
#### If you haven't migrated yet
Every case below is a `400` that creates nothing. When the problem is on one beneficiary, the error carries its `beneficiary_index`.
| What you sent | Error | Fix |
| - | - | - |
| A UBO without `ubo_subtype` | [`ubo_subtype_invalid`](/errors#ubo_subtype_invalid) | Set `Ownership` or `Control` |
| `Control` with a `share_percentage`, or without `nature_of_control` | [`ubo_subtype_invalid`](/errors#ubo_subtype_invalid) | Remove the share and add the description |
| `ubo_subtype` or `nature_of_control` on an earlier version, or without the version header | [`ubo_subtype_invalid`](/errors#ubo_subtype_invalid) | Send `X-API-Version: 2026-10-15` |
| No person with the `UltimateBeneficialOwner` role | [`structure_invalid`](/errors#structure_invalid) | Add the controlling individual as a `Control` UBO |
| A corporate owner whose nested people include an individual with 10% or more of the company indirectly, who isn't submitted as a UBO | [`structure_invalid`](/errors#structure_invalid) | Submit that individual as an `Ownership` UBO |
| `Ownership` below the disclosure threshold | [`ubo_share_below_threshold`](/errors#ubo_share_below_threshold) | Correct the share, or use `Control` if they don't own shares |
### Incomplete Submissions
A Business submission must be complete to be accepted, in every country: when required company documents are missing, the request is rejected with `400` and must be resubmitted with the missing item — nothing is created. Complete submissions are created as `Processed` directly.
A `201` response means the submission was complete and accepted for verification. An incomplete submission never creates a record — it's rejected with `400` instead. Check `status` on the returned identification: `Processed` means MoonPay Enterprise has everything it needs.
Sandbox approves business submissions at creation regardless of completeness. The reject-on-incomplete behavior occurs in production only.
A submission is rejected with `400` when:
* A required document group is missing for the company's region — including for a related company in the ownership structure. See [Company Documents](#company-documents) and, for US companies, [Automatic Verification for US Companies](#automatic-verification-for-us-companies).
* The registration document is missing, for a non-US sole proprietorship.
* A beneficiary of a US company provides a `DriverLicense`, `ResidencePermit`, or `DigitalDriversLicense` that isn't US-issued (`Passport` and `NationalId` are accepted from any country for these beneficiaries). `DigitalDriversLicense` is Brazil-issued only, so it's never accepted for these beneficiaries.
These validations always reject the request with `400`, regardless of country:
* More than 8 company documents
* A document that is not valid base64, exceeds 10 MB, or has an invalid country code
* A document the verification provider cannot process, such as a corrupted file. The response includes the rejection detail. Fix the file before retrying. One exception: an error naming a blocked duplicate upload means the file is fine and a retry succeeds.
* No root-level beneficiary holds the `Director` role, for any company other than a non-US sole proprietorship. This is checked independently of the US attestation requirement below. See [`structure_invalid`](/errors#structure_invalid).
* Ownership is disclosed (a share of 10% or more, or an owner listed without `share_percentage`) but no root-level person can be listed as an owner at 25% or more (or 10% or more, if nobody owns 25%), for any company other than a non-US sole proprietorship. Before API version `2026-10-15`, a company where nobody owns 10% or more is accepted with a root `Director`, and a non-US `PrivateFoundation` is exempt. From `2026-10-15`, submit the individual who controls the company as a `Control` beneficiary instead. A beneficiary whose only role is `Representative` doesn't count toward this — Iron never independently verifies a Representative-only beneficiary's identity, so their disclosed `share_percentage` can't count as ownership evidence. See [`structure_invalid`](/errors#structure_invalid).
* A `share_percentage` outside 0-100, or combined shares of beneficiaries counted as qualifying owners (any `UltimateBeneficialOwner`, or any other beneficiary who isn't `Representative`-only) above 100 for any company in the structure
* A beneficiary whose only role is `UltimateBeneficialOwner` and whose `share_percentage` doesn't meet the disclosure threshold for their company (or nested company): 25% if any beneficiary at that level owns 25% or more, otherwise 10% if any beneficiary there owns 10% or more, otherwise nobody at that level qualifies. Such a beneficiary has no other role to be reported under instead. A beneficiary below the threshold that also holds another role (for example `Director`) is accepted, but submitted without the `UltimateBeneficialOwner` tag. See [`ubo_share_below_threshold`](/errors#ubo_share_below_threshold). From API version `2026-10-15` the threshold applies to `Ownership` beneficiaries only, and a person with `ubo_subtype: "Ownership"` below it is rejected even when they hold another role.
* From API version `2026-10-15`, a missing or inconsistent `ubo_subtype` or `nature_of_control`. See [`ubo_subtype_invalid`](/errors#ubo_subtype_invalid).
* From API version `2026-10-15`, no person beneficiary with the `UltimateBeneficialOwner` role at a company other than a sole proprietorship. See [`structure_invalid`](/errors#structure_invalid).
* A missing `tax_identification_number` for a `Director` or `UltimateBeneficialOwner` person beneficiary, in any country, or for the proprietor of a non-US sole proprietorship. For a beneficiary whose `tax_jurisdiction_country_code` is a jurisdiction that doesn't issue tax IDs, a missing `identity.identity_number` instead. See [Tax ID where none is issued](#tax-id-where-none-is-issued).
* A missing `company_info.tax_identification_number`, unless the company is registered in a jurisdiction that doesn't issue tax IDs. The error identifies the beneficiary by its zero-based index in the `beneficiaries` array.
* A `ResidencePermit` identity document, unless it's US-issued and the beneficiary is at a US company
* A `DigitalDriversLicense` identity document, unless it's Brazil-issued
* Invalid attestation data: more than one attesting party, an attesting party without the `Director` role or nested below the root, a missing or future `attested_at`, or beneficial-ownership data the generated CDD certification cannot render. See [Company Documents](#company-documents).
`400` responses on this endpoint return the reason as a plain string:
```json theme={null}
"Beneficiary share_percentage must be between 0 and 100"
```
### Resubmitting and Correcting a Submission
`POST /api/customers/{id}/identifications/v2` is not an upsert. Every call creates a new identification record, and the newest record drives the customer's status. Calling it a second time while the first is in flight resets the customer to `IdentificationRequired` and stops the older identification counting, even if it is approved later.
Once a submission is accepted, never re-call this endpoint to correct it, to add a missing document, or to check status. Each accepted call adds another identification to the customer, and a customer carrying several competing identifications needs manual reconciliation by MoonPay Enterprise before onboarding can finish.
Whether a second call is correct depends on what the first one returned. A `400` created nothing, so submitting again is the fix. A `201` created a record, so submitting again creates a competing one.
| Situation | What to do |
| - | - |
| Request returned `400` | Nothing was created. Fix the payload and submit again. This is the normal path for an incomplete submission. See [Incomplete Submissions](#incomplete-submissions) |
| You want the current state | `GET /api/customers/{id}/identifications`, or subscribe to the `identification_status` [webhook](/webhooks) |
| You want the specific outstanding items | `GET /api/customers/{id}/requirements`. See [Checking Outstanding Requirements](/onboarding#checking-outstanding-requirements) |
| Request timed out or returned `500` | `GET /api/customers/{id}/identifications` first. Only submit again if no record exists |
| Identification reached `Declined` or `Expired` | Create a new identification. This is the one case where a second call on an existing record is correct |
An `Idempotency-Key` does not protect you here. MoonPay Enterprise stores successful responses only, so a retry after an error runs the submission again. In production, resubmitting the same payload while the earlier identification is live returns `409` with `existing_identification_id` instead of a second record. A changed payload creates a new record. See [Idempotency](/idempotency). The rule above is the control that prevents duplicates.
### Example Scenarios
The following examples show the `company_data.beneficiaries` array for common ownership structures. All other fields (`company_data.company_info`, `company_data.documents`, `questionnaire`) remain the same as the full example above. The person objects below are abbreviated: a real submission also carries `identity`, `tax_identification_number`, and `proof_of_address` per beneficiary, as in the full example.
A company with a majority shareholder/director and a minority UBO:
```json theme={null}
"beneficiaries": [
{
"roles": ["Director"],
"beneficiary_info": {
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1985-03-22",
"email_address": "jane@acme.com",
"phone_number": "+4915112345678",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Munich",
"state": "Bavaria",
"country": "DE"
},
"address": {
"street_1": "Unter den Linden 42",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE"
}
}
},
{
"roles": ["UltimateBeneficialOwner"],
"share_percentage": 60,
"beneficiary_info": {
"first_name": "Max",
"last_name": "Müller",
"date_of_birth": "1978-11-05",
"email_address": "max@acme.com",
"phone_number": "+4915198765432",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Hamburg",
"state": "Hamburg",
"country": "DE"
},
"address": {
"street_1": "Königstraße 10",
"city": "Hamburg",
"state": "Hamburg",
"zip_code": "20095",
"country": "DE"
}
}
},
{
"roles": ["UltimateBeneficialOwner"],
"share_percentage": 40,
"beneficiary_info": {
"first_name": "Sophie",
"last_name": "Weber",
"date_of_birth": "1990-07-14",
"email_address": "sophie@acme.com",
"phone_number": "+4915155556666",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Frankfurt",
"state": "Hessen",
"country": "DE"
},
"address": {
"street_1": "Goethestraße 8",
"city": "Frankfurt",
"state": "Hessen",
"zip_code": "60313",
"country": "DE"
}
}
}
]
```
When a legal entity owns or controls the company, give it the `Shareholder` role and pass a business `beneficiary_info`: an object with a single `company_data` field. It carries the full data for that entity: its `company_info`, its own `beneficiaries`, and its own `documents`. The natural persons who are UBOs through the corporate chain appear inside the nested entity:
```json theme={null}
"beneficiaries": [
{
"roles": ["Director"],
"beneficiary_info": {
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1985-03-22",
"email_address": "jane@acme.com",
"phone_number": "+4915112345678",
"tax_jurisdiction_country_code": "DE",
"birth_place": {
"place": "Munich",
"state": "Bavaria",
"country": "DE"
},
"address": {
"street_1": "Unter den Linden 42",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE"
}
}
},
{
"roles": ["Shareholder"],
"share_percentage": 100,
"beneficiary_info": {
"company_data": {
"company_info": {
"company_name": "Acme Holdings GmbH",
"registration_number": "HRB987654",
"country": "DE",
"incorporation_date": "2015-01-10",
"legal_address": "Kurfürstendamm 50, 10707 Berlin",
"postal_address": "Kurfürstendamm 50, 10707 Berlin",
"address": {
"street_1": "Kurfürstendamm 50",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10707",
"country": "DE"
},
"email": "info@acme-holdings.com",
"tax_identification_number": "DE987654321",
"website": "https://www.acme-holdings.com",
"company_type": "LimitedLiabilityCompany"
},
"beneficiaries": [
{
"roles": ["Director", "UltimateBeneficialOwner"],
"share_percentage": 100,
"beneficiary_info": {
"first_name": "Max",
"last_name": "Müller",
"date_of_birth": "1978-11-05",
"email_address": "max@acme-holdings.com",
"phone_number": "+4915198765432",
"tax_jurisdiction_country_code": "DE",
"birth_place": { "place": "Hamburg", "state": "Hamburg", "country": "DE" },
"address": {
"street_1": "Königstraße 10",
"city": "Hamburg",
"state": "Hamburg",
"zip_code": "20095",
"country": "DE"
}
}
}
],
"documents": [
{ "type": "IncorporationCert", "country": "DE", "file": "" },
{ "type": "IncorporationArticles", "country": "DE", "file": "" },
{ "type": "ShareholderRegistry", "country": "DE", "file": "" },
{ "type": "DirectorsRegistry", "country": "DE", "file": "" }
]
}
}
}
]
```
Each corporate owner carries its own `company_data.documents` and `beneficiaries`. Each corporate owner needs Company Details and Ownership Structure documents, as described in [Company Documents](#company-documents). The natural persons who ultimately own 25%+ through the chain must appear as UBOs.
From API version `2026-10-15`, an individual who controls the company without owning it is a `Control` beneficiary. For example, a foundation council member with voting rights. They send `nature_of_control` and no `share_percentage`:
```json theme={null}
"beneficiaries": [
{
"roles": ["Director", "UltimateBeneficialOwner"],
"ubo_subtype": "Control",
"nature_of_control": "Member of the foundation council with voting rights",
"beneficiary_info": {
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1985-03-22",
"email_address": "jane@acme.com",
"phone_number": "+4915112345678",
"tax_jurisdiction_country_code": "DE",
"address": {
"street_1": "Unter den Linden 42",
"city": "Berlin",
"state": "Berlin",
"zip_code": "10117",
"country": "DE"
}
}
}
]
```
### Business API Field Reference
The request envelope has four top-level fields beyond `type`:
| Field | Type | Required | Description |
| - | - | - | - |
| `company_data` | object | Yes | Company identity, beneficiaries, and documents (`company_info`, `beneficiaries`, `documents`) |
| `phone` | string | Yes | Company phone number (international format) |
| `ip_address` | string | Yes | IP address of the device used to submit the identification. Must be a public, globally routable address: private, loopback, and documentation ranges are rejected with `400`. |
| `questionnaire` | object | Yes | KYB compliance questionnaire. Set `type` to `Generic` (all company types except non-US sole proprietorships) or `SoleProp` (non-US sole proprietorships only) |
These fields live under `company_data.company_info`.
| Field | Type | Description |
| - | - | - |
| `company_name` | string | Legal name of the company |
| `alternative_names` | string\[] | Optional list of trading or alternative names |
| `registration_number` | string | Number assigned at legal registration |
| `country` | string | ISO 3166-1 alpha-2 country code of registration |
| `incorporation_date` | string | Date of incorporation (YYYY-MM-DD) |
| `legal_address` | string | Address used during legal registration |
| `postal_address` | string | Postal address of the company |
| `address` | object | Structured address (`street_1`, `street_2`, `city`, `state`, `zip_code`, `country`) |
| `email` | string | Company email address |
| `tax_identification_number` | string | Tax ID of the company. Required unless the company is registered in a jurisdiction that doesn't issue tax IDs, where any value sent is ignored and `registration_number` is used instead. |
| `website` | string | Company website URL |
| `company_type` | enum | `LimitedLiabilityCompany`, `PubliclyListedCompany`, `SoleProprietorship`, `Partnership`, `Corporation`, `Trust`, `PrivateFoundation`, `Charity`, `NonprofitCorporation`, `NonprofitOrganization`, `PublicAgenciesOrAuthorities`, `StockCorporationOutOfStock`, `GeneralCorporation` |
The `company_data.beneficiaries` array contains one or more `CompanyBeneficiary` objects representing UBOs, directors, representatives, or corporate shareholders. Each business level holds up to 5 beneficiaries and at most one business beneficiary, and businesses nest at most two levels below the root company. The `Director` role is only valid on person beneficiaries. Requests outside these bounds are rejected with `400`.
#### `CompanyBeneficiary`
| Field | Type | Required | Description |
| - | - | - | - |
| `roles` | enum\[] | Yes | One or more of: `UltimateBeneficialOwner`, `Director`, `Representative`, `Shareholder` |
| `share_percentage` | decimal | Conditional | Ownership percentage, 0-100. Combined `UltimateBeneficialOwner` shares must not exceed 100 per company, at the root and at every nested level. Required when roles include `UltimateBeneficialOwner`, except for `Control` beneficiaries, which send none. A person below the disclosure threshold (25%, or 10% when nobody at that level owns 25%) must hold another role too (for example `Director`) — otherwise the submission is rejected; if they do hold another role, they're accepted but submitted without the `UltimateBeneficialOwner` tag. |
| `ubo_subtype` | enum | Conditional | `Ownership` or `Control`. Required when roles include `UltimateBeneficialOwner`, from API version `2026-10-15`. Rejected on earlier versions and on beneficiaries without that role. `Control` is accepted only on root-level person beneficiaries of a company that isn't a sole proprietorship. |
| `nature_of_control` | string | Conditional | How the individual controls the company without owning it, for example through voting rights or a trust. Up to 255 characters. Required when `ubo_subtype` is `Control`, and rejected otherwise. |
| `beneficiary_info` | object | Yes | Person or Business info. |
| `attesting_party` | boolean | No | Marks this beneficiary as certifying the beneficial-ownership information (31 CFR 1010.230). At most one per submission: a root-level person beneficiary with the `Director` role. Required for every US company, sole proprietorships included: see [Company Documents](#company-documents). |
| `attested_at` | string | Conditional | RFC 3339 timestamp of when the beneficiary certified the information. Must be in the past. Required when `attesting_party` is `true`. |
The `beneficiary_info` field takes one of two shapes: `PersonBeneficiaryInfo` for an individual, or `BusinessBeneficiaryInfo` for a corporate owner, which carries a nested `company_data`. Unlike `questionnaire`, this object has no `type` discriminator: MoonPay Enterprise detects the variant from the fields you send.
#### `PersonBeneficiaryInfo`
Used when the beneficial owner is a natural person.
| Field | Type | Required | Description |
| - | - | - | - |
| `first_name` | string | Yes | First name |
| `last_name` | string | Yes | Last name |
| `middle_name` | string | No | Middle name |
| `date_of_birth` | string | Yes | Date of birth in `YYYY-MM-DD` format |
| `tax_identification_number` | string | Conditional | SSN for US persons, otherwise a national TIN. Required for `Director` and `UltimateBeneficialOwner` person beneficiaries in any country, for a `Representative` when the root company is registered outside the US, and always for the proprietor of a non-US sole proprietorship. When `tax_jurisdiction_country_code` is a jurisdiction that doesn't issue tax IDs, any value sent is ignored and `identity.identity_number` is used instead. See [Tax ID where none is issued](#tax-id-where-none-is-issued). |
| `email_address` | string | Yes | Email address. Checked for format only. It doesn't have to be on the company's domain, and it isn't compared against `company_info.email`: an external director, consultant, or outside counsel can use their own firm's address. |
| `phone_number` | string | Yes | Phone number in international format (e.g. `+4915112345678`) |
| `tax_jurisdiction_country_code` | string | Yes | ISO 3166-1 alpha-2 country code of tax jurisdiction |
| `birth_place` | object | No | Place of birth (see `BirthPlace` below). Optional, but when provided it must be complete. |
| `address` | object | Yes | Residential address (see `Address` below) |
| `identity` | object | Yes | Identity document and biometric data (see `BeneficiaryIdentity` below) |
| `proof_of_address` | string | No | Base64-encoded proof of address document, 10 MB max. Not currently required for any beneficiary role or country: validated (base64, size) when provided, but a missing one never blocks the submission. |
#### `BeneficiaryIdentity`
The `identity` object on a person beneficiary.
| Field | Type | Required | Description |
| - | - | - | - |
| `identity_country_code` | string | Yes | ISO 3166-1 alpha-2 country code of the identity document |
| `identity_document_type` | enum | Yes | `Passport`, `NationalId`, `DriverLicense`, `ResidencePermit`, or `DigitalDriversLicense`. `ResidencePermit` is rejected unless it's US-issued and the beneficiary is at a US company. `DigitalDriversLicense` is rejected unless it's Brazil-issued. Beneficiaries of US companies additionally need `DriverLicense` or `ResidencePermit` to be US-issued (`Passport` and `NationalId` are accepted from any country): see [Identity Verification](#identity-verification-/-liveness-check). |
| `identity_document_front` | string | Yes | Base64-encoded image of the document front, 10 MB max |
| `identity_document_back` | string | Conditional | Base64-encoded image of the document back, 10 MB max. Required for `NationalId`, `DriverLicense`, and `ResidencePermit`. `Passport` and `DigitalDriversLicense` need the front only. |
| `identity_document_valid_until` | string | No | Expiry date of the identity document, `YYYY-MM-DD`. Send it when the document has one. |
| `identity_number` | string | Conditional | Number printed on the identity document. Required in place of `tax_identification_number` when the beneficiary needs a tax ID and `tax_jurisdiction_country_code` is a jurisdiction that doesn't issue tax IDs. |
| `selfie` | string | Conditional | Base64-encoded selfie, 10 MB max. Required for every `Director`, in any country. Required for a `Representative` when the root company is registered outside the US. Not required for `UltimateBeneficialOwner`- or `Shareholder`-only beneficiaries. |
| `selfie_collected_at` | string | Conditional | ISO 8601 timestamp of when the selfie was captured. Provide it together with `selfie`. |
| `nationality` | string | Yes | ISO 3166-1 alpha-2 country code of nationality |
#### `BusinessBeneficiaryInfo`
Used when a beneficial owner is a legal entity (corporate shareholder). It carries a full `company_data` with the same shape as the top level, so the same information is collected at every level of the corporate structure.
| Field | Type | Required | Description |
| - | - | - | - |
| `company_data` | object | Yes | `company_info`, `beneficiaries`, and `documents` for this entity. Same schema as the top-level `company_data`. Nesting continues through further business beneficiaries. |
Each corporate owner carries its own `company_data.documents` and `beneficiaries`. Each corporate owner needs Company Details and Ownership Structure documents, as described in [Company Documents](#company-documents). The natural persons who ultimately own 25%+ through the chain must appear as UBOs.
#### `BirthPlace`
| Field | Type | Required | Description |
| - | - | - | - |
| `place` | string | Yes | City or town of birth |
| `state` | string | Conditional | State or region of birth. Required for countries with administrative subdivisions, omit it for countries without any (Vatican City, for example). |
| `country` | string | Yes | ISO 3166-1 alpha-2 country code |
#### `Address`
| Field | Type | Required | Description |
| - | - | - | - |
| `street_1` | string | Yes | Street address |
| `street_2` | string | No | Additional street address |
| `city` | string | Yes | City |
| `state` | string | Conditional | State, province, or region. Required on the company address for countries with administrative subdivisions. |
| `zip_code` | string | Yes | Postal / ZIP code |
| `country` | string | Yes | ISO 3166-1 alpha-2 country code |
The `company_data.documents` array contains up to 8 `CompanyDocParams` objects. Each document is uploaded as a base64-encoded file. Structurally optional at submission, but an incomplete set for the company's region is rejected with `400`. See [Company Documents](#company-documents) for the groups required per region.
#### `CompanyDocParams`
| Field | Type | Required | Description |
| - | - | - | - |
| `type` | enum | Yes | Document type (see table below) |
| `country` | string | Yes | ISO 3166-1 alpha-2 country code of the issuing country |
| `file` | string | Yes | Base64-encoded document content **without** the `data:` URI prefix |
#### Upload requirements
* Maximum file size: **10 MB** per document (after base64 decoding)
* Maximum 8 documents per company. The limit applies separately to the root company and to each nested business beneficiary. More than 8 is rejected with `400`.
* Encoding: standard base64 (no `data:application/pdf;base64,` prefix)
* Supported formats: PDF, JPEG, PNG
#### Document types
| Value | Description |
| - | - |
| `PowerOfAttorney` | Power of attorney |
| `TransparencyRegistryExtract` | Recent excerpt from a transparency company registry |
| `DirectorsRegistry` | Directors registry |
| `GoodStandingCert` | Certificate of good standing |
| `IncorporationArticles` | Memorandum, articles of incorporation, association, or registration |
| `IncorporationCert` | Certificate of incorporation or registration |
| `IncumbencyCert` | Certificate of incumbency |
| `InformationStatement` | Statement of information |
| `PartnershipAgreement` | Partnership agreement |
| `ProofOfAddress` | Proof of address (e.g. utility bill, rent contract) |
| `ProofOfNatureOfBusiness` | Proof of nature of business |
| `RegulatoryLicense` | Regulatory or operating license |
| `SelfDeclarationForm` | Self-declaration form |
| `ShareholderRegistry` | Shareholder registry |
| `StateRegistry` | Recent excerpt from a state company registry |
| `TradeLicense` | Commercial or trading license |
| `TrustAgreement` | Trust agreement |
| `Other` | Other document not covered by the types above |
The `BusinessQuestionnaire` object is a required compliance questionnaire structured into three sections. Each section collects specific regulatory information about the business. Set `type` to `Generic` for all company types except non-US sole proprietorships, or `SoleProp` for non-US sole proprietorships.
```json theme={null}
{
"questionnaire": {
"type": "Generic",
"business_details": { ... },
"planned_activities": { ... },
"source_of_funds": { ... }
}
}
```
**Currency of the revenue and volume bands.** The numbers in `estimated_monthly_volume` (and, on the `SoleProp` questionnaire, `estimated_annual_revenue`) are band boundaries, not a currency requirement: read them in the currency the business reports in, so a company reporting 60,000 USD sends `FiftyToOneHundredThousand`. Do not convert at an FX rate, and note that there is no separate band set per currency. Neither field sets a transaction limit.
#### `business_details`
Industry classification and operational information about the company.
| Field | Type | Required | Description |
| - | - | - | - |
| `industry_type` | enum | Yes | Industry sector classification (see values below) |
| `industry` | enum | Required from `2026-08-01`, optional before | Full granular industry classification. See [`industry` values](#industry-values). |
| `business_model_description` | string | Yes | Description of the business model and target clients. Minimum 80 characters. |
| `main_business_countries` | string\[] | Yes | ISO 3166-1 alpha-2 country codes where the company primarily operates |
| `has_licenses_or_registrations` | boolean | Yes | Whether the company currently holds any licenses or registrations |
| `license_details` | string | Conditional | Required if `has_licenses_or_registrations` is `true`. Describe the licenses or registrations the company holds. |
**`industry_type` values:** `Agriculture`, `Automotive`, `BankingAndFinancialServices`, `ConstructionAndRealEstate`, `CryptocurrencyAndBlockchain`, `CryptocurrencyMining`, `DecentralisedFinance`, `NftPlatforms`, `CryptoPaymentProcessors`, `CryptoWalletProviders`, `CryptoExchanges`, `CryptoCustodyServices`, `EcommerceAndOnlineMarketplaces`, `EducationAndTraining`, `EnergyAndUtilities`, `EntertainmentAndMedia`, `GamingAndEsports`, `HealthcareAndPharmaceuticals`, `HospitalityAndTourism`, `InformationTechnologyAndSoftware`, `Insurance`, `LegalAndProfessionalServices`, `LogisticsAndTransportation`, `ManufacturingAndIndustrial`, `MetaversePlatforms`, `MiningAndNaturalResources`, `NonProfitAndCharitable`, `RetailAndWholesaleTrade`, `Telecommunications`, `VentureCapitalAndPrivateEquity`, `CasinosAndGambling`, `MoneyServiceBusinesses`, `SocialMediaAndNetworking`, `CrowdfundingPlatforms`, `RegTech`, `Other`
`industry_type` is deprecated but still accepted indefinitely. `industry` is the modern replacement, exposing the full industry taxonomy (136 values) in place of the 36 broad categories above, and is required from API version `2026-08-01` onward. Before that version it stays optional and `industry_type` alone is sufficient. If a submission includes both fields, `industry` takes precedence and `industry_type` is ignored.
MoonPay Enterprise doesn't use NAICS, SIC, or MCC codes, and there's no mapping from those schemes to these fields. Classify the company directly against the [`industry` values](#industry-values).
Don't classify a company under a catch-all value. `industry: "Miscellaneous"` is rejected with [`miscellaneous_industry_deprecated`](/errors#miscellaneous_industry_deprecated) on every submission, however detailed `business_model_description` is. The deprecated `industry_type` values `Other` and `NonProfitAndCharitable` pass validation, but both resolve to `Miscellaneous` internally and are sent for review that way, which slows the review and can produce a request for more information later. Pick the closest specific `industry` value instead.
The full list is in the appendix: [Industry Values](#industry-values).
**`estimated_annual_revenue` values** (`SoleProp` questionnaire only):
| Value | Range |
| - | - |
| `Under100K` | 0 - 99,999 |
| `From100KTo1M` | 100,000 - 999,999 |
| `From1MTo10M` | 1,000,000 - 9,999,999 |
| `From10MTo50M` | 10,000,000 - 49,999,999 |
| `From50MTo250M` | 50,000,000 - 249,999,999 |
| `Over250M` | 250,000,000+ |
#### `planned_activities`
Information about the company's intended account use and expected transaction patterns.
| Field | Type | Required | Description |
| - | - | - | - |
| `purpose_and_intended_use` | enum | Yes | Primary purpose of the business account |
| `estimated_monthly_volume` | enum | Yes | Expected monthly transaction volume bracket. See the currency note above. |
| `estimated_monthly_transactions` | enum | Yes | Expected number of transactions per month |
| `sends_funds_on_customer_behalf` | boolean | Yes | Whether the company sends funds on behalf of customers or third parties |
| `sends_funds_on_customer_behalf_description` | string | Conditional | Required if `sends_funds_on_customer_behalf` is `true`. Describe how funds are sent. |
| `flow_of_funds_diagram` | string | Conditional | Base64-encoded flow of funds diagram, 10 MB max. Required if `sends_funds_on_customer_behalf` is `true`. |
| `source_of_funds_document` | string | Conditional | Base64-encoded source of funds supporting document, 10 MB max. Required if `sends_funds_on_customer_behalf` is `true`. |
**`purpose_and_intended_use` values:** `ReceivingBusinessIncome`, `BusinessPurchasesAndExpenses`, `EmployeeAndContractorPayouts`, `EcommerceSales`, `TreasuryManagement`, `PaymentIntermediary`, `Investments`, `TaxPlanning`, `AssetProtection`, `Other`
**`estimated_monthly_volume` values:**
| Value | Range |
| - | - |
| `TenToTwentyFiveThousand` | 10,000 - 25,000 |
| `TwentyFiveToFiftyThousand` | 25,000 - 50,000 |
| `FiftyToOneHundredThousand` | 50,000 - 100,000 |
| `OneHundredToTwoHundredFiftyThousand` | 100,000 - 250,000 |
| `TwoHundredFiftyToFiveHundredThousand` | 250,000 - 500,000 |
| `MoreThanFiveHundredThousand` | 500,000+ |
**`estimated_monthly_transactions` values:**
| Value | Range |
| - | - |
| `LessThanTen` | Fewer than 10 |
| `TenToTwentyFive` | 10 - 25 |
| `TwentyFiveToFifty` | 25 - 50 |
| `FiftyToOneHundred` | 50 - 100 |
| `MoreThanOneHundred` | More than 100 |
#### `source_of_funds`
Information about where the company's funds originate.
| Field | Type | Required | Description |
| - | - | - | - |
| `source_of_funds` | enum | Yes | Primary source of the company's funds |
| `source_of_funds_description` | string | Yes | Detailed description of the source of funds |
**`source_of_funds` values:** `BusinessRevenue`, `ShareholderFunds`, `VcPrivateEquity`, `AssetSale`, `IcoProceeds`, `CryptoActivityProceeds`, `LoanProceeds`, `Other`
***
## Standard KYB Requirements
### Company Information
* Country
* Company name
* Registration number
* Company registration address
* Company operations address
* Type of entity
* Date of registration
* Website
* Company email
* Tax ID
### Company Documents
Company documents fall into four groups. Which groups the root company needs depends on where it's registered and its type. A nested business beneficiary (a `Shareholder` that's a legal entity) always needs just two groups — Company Details and Ownership Structure — regardless of where it's registered or what the root company needs.
| Company profile | Required document groups |
| - | - |
| Root, registered outside the US, all types except sole proprietorships | One document from each group: Company Details, Legal Presence, Ownership Structure, Control and Management Structure |
| Root, registered in the US, all types | Company Details only. Legal Presence and Control and Management Structure are not required. Ownership Structure is always satisfied by a mandatory generated CDD certification (see the note below) — and Company Details itself is waived when MoonPay Enterprise can verify the company automatically (see [Automatic Verification for US Companies](#automatic-verification-for-us-companies)). |
| Sole proprietorship, registered outside the US | Incorporation certificate (`IncorporationCert`) |
| Nested business beneficiary, any country | Company Details and Ownership Structure only. Legal Presence and Control and Management Structure are never required, and the CDD certification doesn't apply — a nested entity always needs a real Ownership Structure document. |
A submission missing a required group is rejected with `400`, in every country, and must be resubmitted with the missing item. See [Incomplete Submissions](#incomplete-submissions).
Every US company submission must designate one root person beneficiary holding the `Director` role as the attesting party: set `attesting_party: true` and `attested_at` on them, after presenting the collected beneficial-ownership information to them for confirmation. MoonPay Enterprise generates a CDD certification (31 CFR 1010.230) from the submitted data and files it, satisfying the Ownership Structure group without a separately uploaded document. This is mandatory, not optional — a submission missing an attesting party, or one that cannot produce the certification (missing `share_percentage` on a beneficiary the certification must list, no SSN or tax/identity number for a listed US person, or more than two levels of beneficiaries), is rejected with `400`. The certification's ownership section lists whichever beneficiaries own 25% or more; if nobody does, it falls back to listing whoever owns 10% or more instead. From API version `2026-10-15`, individuals submitted as `Control` beneficiaries are listed in a separate section of the certification, with their `nature_of_control`. See the qualifying-owner requirement below for when a company with no owner at either threshold is accepted.
### Automatic Verification for US Companies
Eligible US companies are checked automatically against official company records as part of verification. When MoonPay Enterprise can confirm the company this way, the Company Details document isn't required — the automatic match satisfies it. This happens without any extra input from you; we still recommend sending the Company Details document anyway, since it's the fastest way to speed up review if automatic verification isn't possible for a given company.
The CDD attestation above is still required either way, whether or not automatic verification succeeds.
Accepted document types:
* Incorporation certificate (`IncorporationCert`)
* Excerpt from a state company registry (`StateRegistry`)
* Certificate of good standing (`GoodStandingCert`)
Accepted document types:
* Articles and memorandum of association (`IncorporationArticles`)
Accepted document types:
* Shareholder registry (`ShareholderRegistry`)
* Statement of information (`InformationStatement`)
* Trust agreement (`TrustAgreement`)
* Certificate of incumbency (`IncumbencyCert`)
The document must list all shareholders with their ownership percentages (totaling 100%), be dated within the last 12 months, and signed by a director unless publicly available.
**KYC checks:** Each UBO with 25%+ ownership and all company directors must complete KYC (Proof of ID) and be individually approved before onboarding. If there is no UBO who owns 25%+, all UBOs with 10%+ ownership must be verified.
**KYB checks:** Apply to all legal entities in the ownership chain. For each additional entity, the same information is required unless ownership can be verified through open sources.
Accepted document types (any that contains information about appointed directors):
* Director registry (`DirectorsRegistry`)
* Trust agreement (`TrustAgreement`)
* Statement of information (`InformationStatement`)
* Certificate of incumbency (`IncumbencyCert`)
### Business Questionnaire
| Section | Question | Answers |
| - | - | - |
| Business information | Industry type | Agriculture, Automotive, Banking and Financial Services, Construction and Real Estate, Cryptocurrency and Blockchain, Cryptocurrency Mining, DeFi, NFT Platforms, Crypto Payment Processors, Crypto Wallet Providers, Crypto Exchanges, Crypto Custody Services, E-commerce and Online Marketplaces, Education and Training, Energy and Utilities, Entertainment and Media, Gaming and Esports, Healthcare and Pharmaceuticals, Hospitality and Tourism, Information Technology and Software Development, Insurance, Legal and Professional Services, Logistics and Transportation, Manufacturing and Industrial, Metaverse Platforms, Mining and Natural Resources, Non-Profit and Charitable Organizations, Retail and Wholesale Trade, Telecommunications, Venture Capital and Private Equity, Casinos and Gambling, Money Service Businesses, Social Media and Networking Platforms, Crowdfunding Platforms, RegTech, Other |
| Business information | Business description | Free text |
| Business information | Main countries of operation | Dropdown country selection |
| Business information | Licensed or registered | Yes / No |
| Business information | Licenses or registrations required in jurisdiction | Yes / No |
| Business information | Estimated annual revenue | Up to 99,999, 100,000-999,999, 1,000,000-9,999,999, 10,000,000-49,999,999, 50,000,000-249,999,999, More than 250,000,000 |
| Source of funds | Source of funds | Business revenue, Shareholder funds, VC or private equity, Sale of assets, ICO proceeds, Crypto related income, Loan proceeds, Other |
| Source of funds | Customer funds details | Free text |
| Planned activities | Purpose and intended use | Receiving income, Expenses, Payouts, E-commerce sales, Treasury management, Payment intermediary, Investments, Tax planning, Asset protection, Other |
| Planned activities | Estimated monthly volume | 10,000-25,000, 25,000-50,000, 50,000-100,000, 100,000-250,000, 250,000-500,000, More than 500,000 |
| Planned activities | Monthly transaction count | Less than 10, 10-25, 25-50, 50-100, More than 100 |
| Planned activities | Send funds on behalf of customers | Yes / No |
The revenue and volume figures are band boundaries, not a currency requirement. Read them in the currency the business reports in, and do not convert at an FX rate. Neither answer sets a transaction limit.
Upload a Source of Funds document whenever `sends_funds_on_customer_behalf` is `true`, in any currency.
### Associated Parties
| Party Type | Description |
| - | - |
| UBO (Ultimate Beneficial Owner) | Always a natural person. Direct or indirect ownership above 25% (or 10% in high-risk cases). |
| Director | Individual(s) formally appointed as director(s). |
| Representative | Authorized individual acting on behalf of the company. |
| Shareholder | Direct ownership above 25%. A legal entity, or a natural person whose only role is shareholder. |
Example: Business Customer → owned by Company A → John Smith owns Company A → Company A is Shareholder, John Smith is UBO.
### Required Information by Party Type
* First name
* Last name
* Middle name
* Date of birth
* Email
* Contact number
* Nationality
* Percentage of ownership
* Tax ID: SSN for US persons, otherwise a national TIN. Always required, in any country, including for the proprietor of a non-US sole proprietorship.
* Address details
* First name
* Last name
* Middle name
* Date of birth
* Email
* Contact number
* Nationality
* Tax ID: always required for a `Director`, in any country. Required for a `Representative` only when the root company is registered outside the US.
* Address details
* Country
* Company name
* Registration number
* Certificate of incorporation
* Shareholder Registry or Register of Members or Excerpt from state companies registry (if it contains ownership data)
### Tax ID where none is issued
Some jurisdictions don't issue tax IDs. When a party's tax residence is one of them, you don't need to send a tax ID. Any `tax_identification_number` you send is ignored, and MoonPay Enterprise uses an identifier from the submission instead:
* **Person beneficiary:** `identity.identity_number`, from any identity document type. Tax residence is `tax_jurisdiction_country_code`. If the beneficiary's role needs a tax ID, `identity.identity_number` is required and a missing one returns [`tin_required`](/errors#tin_required).
* **Company** (the root company or a nested business beneficiary): `registration_number`. Tax residence is `company_info.country`.
US persons, meaning a US address, US nationality, or a US-issued identity document, are never covered by this. Send their SSN or ITIN as usual.
Proof of address is not currently required for any person beneficiary, in any country. It's validated (base64, size) when provided, but a missing one never blocks the submission.
### Identity Verification / Liveness Check
For business customers, there are typically 2-3 types of people to identify, depending on whether an authorized representative is appointed:
| Party | Requirements |
| - | - |
| Director(s) | ID collection + liveness check, in any country |
| UBO(s) | ID collection. Liveness check only if also a director |
| Authorized Representative | ID collection + power of attorney signed by the company's director. Liveness check required outside the US only |
Accepted identity documents are `Passport`, `NationalId`, `DriverLicense`, `ResidencePermit`, and `DigitalDriversLicense`. A `ResidencePermit` is rejected unless it's US-issued and the beneficiary is at a US company. A `DigitalDriversLicense` is rejected unless it's Brazil-issued. Beneficiaries of US companies additionally need `DriverLicense` or `ResidencePermit` to be US-issued (`Passport` and `NationalId` are accepted from any country): any other combination is rejected with `400`, not held pending.
### Sole Proprietor Onboarding
This simplified onboarding path applies to **non-US sole proprietorships only**. A US sole proprietorship (`company_type: "SoleProprietorship"` with `company_data.company_info.country: "US"`) follows the standard business flow for documents, the attesting party, and the questionnaire — the standard US document set, a mandatory attesting party, and the `Generic` questionnaire, same as any other US company. See [Company Documents](#company-documents), [Required Information by Party Type](#required-information-by-party-type), and the attesting-party note above.
Unlike a US LLC or corporation, though, a US sole proprietorship is still structurally a single owner: it must submit **exactly one** root-level beneficiary, who cannot be a business. That one beneficiary holds `UltimateBeneficialOwner` at 100% share, the `Director` role, and is the attesting party, all at once — it's the same person wearing three hats, not three separate beneficiaries. Submitting more than one beneficiary, or a beneficiary who doesn't own 100%, is rejected. See [`structure_invalid`](/errors#structure_invalid).
Non-US sole proprietors use the same onboarding paths as any other business. Pass `company_type: "SoleProprietorship"` to the Hosted Link flow or the Programmatic Business API. The Hosted Link flow automatically adapts its UI to the smaller sole-proprietor data set. The Programmatic API uses the same wire schema as any business: the proprietor must be submitted as exactly one person beneficiary with the `UltimateBeneficialOwner` role at 100% share. Anything else is rejected with `400`.
For non-US sole proprietors, set `questionnaire.type` to `"SoleProp"`. This is a simplified variant of the business questionnaire with fewer required fields.
Non-US sole proprietorships need one company document: the incorporation certificate (`IncorporationCert`). Proof of address is not required for the proprietor. A non-US sole proprietorship missing the incorporation certificate is rejected with `400`. See [Incomplete Submissions](#incomplete-submissions).
The hosted sole-proprietor flow walks the customer through four verification steps:
* Country
* Company name
* Incorporated on
* Registration number
* Website
* Custom field: Doing Business As
Sole Proprietor Information (sets `questionnaire.type` to `"SoleProp"`).
* Certificate of incorporation or registration
The proprietor as a single UBO:
* First name
* Last name
* Date of birth
* Email
* Phone
* TIN
```json theme={null}
{
"type": "Business",
"phone": "+442071234567",
"ip_address": "8.8.8.8",
"company_data": {
"company_info": {
"company_name": "Jane's Consulting Ltd",
"alternative_names": null,
"registration_number": "SC123456",
"country": "GB",
"incorporation_date": "2020-01-15",
"legal_address": "10 Downing Street, London, SW1A 2AA",
"postal_address": "10 Downing Street, London, SW1A 2AA",
"address": {
"street_1": "10 Downing Street",
"street_2": null,
"city": "London",
"state": "London",
"zip_code": "SW1A 2AA",
"country": "GB"
},
"email": "jane@janesconsulting.co.uk",
"tax_identification_number": "GB123456789",
"website": "https://janesconsulting.co.uk",
"company_type": "SoleProprietorship"
},
"beneficiaries": [
{
"roles": ["UltimateBeneficialOwner"],
"share_percentage": 100,
"beneficiary_info": {
"first_name": "Jane",
"middle_name": null,
"last_name": "Doe",
"date_of_birth": "1985-06-15",
"tax_identification_number": "GB987654321",
"address": {
"street_1": "10 Downing Street",
"street_2": null,
"city": "London",
"state": null,
"zip_code": "SW1A 2AA",
"country": "GB"
},
"email_address": "jane@janesconsulting.co.uk",
"phone_number": "+442071234567",
"tax_jurisdiction_country_code": "GB",
"birth_place": { "place": "Manchester", "state": "England", "country": "GB" },
"identity": {
"identity_country_code": "GB",
"identity_number": null,
"identity_document_type": "Passport",
"identity_document_front": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mP4z8BAEmIY1TCqYfhqAACQ+f8B8u7oVwAAAABJRU5ErkJggg==",
"identity_document_back": null,
"nationality": "GB"
},
"proof_of_address": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFUlEQVR42mNgYPhPIhrVMKph2GoAAJLb/wHQPqPSAAAAAElFTkSuQmCC"
}
}
],
"documents": [
{
"type": "IncorporationCert",
"country": "GB",
"file": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mP4f+A0SYhhVMOohuGrAQDBXYofzKX3pgAAAABJRU5ErkJggg=="
}
]
},
"questionnaire": {
"type": "SoleProp",
"planned_activities": {
"purpose_and_intended_use": "ReceivingBusinessIncome",
"estimated_monthly_volume": "TenToTwentyFiveThousand",
"estimated_monthly_transactions": "LessThanTen"
},
"source_of_funds": { "source_of_funds": "BusinessRevenue" },
"business_details": {
"industry_type": "LegalAndProfessionalServices",
"business_model_description": "Independent consulting services for small businesses, focused on operations and financial strategy",
"main_business_countries": ["GB"],
"estimated_annual_revenue": "Under100K"
}
}
}
```
## Enhanced KYB Due Diligence
Business customer risk is defined through a customer risk rating process, calculating the risk score based on different factors: geography, industry, use of product, expected activities, legal form, etc.
| Requirement | Details |
| - | - |
| KYB Information | Standard KYB |
| UBO Information | Individuals holding 10%+ ownership |
| Source of Wealth | Financial document outlining the corporate entity's ability to continue business |
## Appendix
Industry Values
`business_details.industry` accepts 136 values. 109 are accepted for onboarding. The other 27 are the 26 [prohibited industries](#prohibited-industries), rejected with [`prohibited_industry`](/errors#prohibited_industry), plus `Miscellaneous`, rejected with [`miscellaneous_industry_deprecated`](/errors#miscellaneous_industry_deprecated).
These are MoonPay Enterprise's own values. NAICS, SIC, and MCC codes aren't used and don't map onto them.
**Accepted values:** `Advertising`, `Aerospace`, `AgricultureAndFarming`, `Alcohol`, `ArmoredCourierServices`, `Art`, `ArtificialIntelligence`, `ATMs`, `AuctioneersAndBrokers`, `BankingAsAService`, `Biotechnology`, `BlockchainAdTech`, `BlockchainAssetManagement`, `BlockchainDeveloperTech`, `BlockchainUtilities`, `BuildingMaterialsAndSupplies`, `BusinessServices`, `BuyNowPayLaterBNPL`, `CentralizedCryptoExchangesAndTrading`, `CleaningProductsAndServices`, `CloudComputing`, `CollectibleCards`, `CommissionAgents`, `Commodities`, `ComputerHardware`, `Consulting`, `ConsumerElectronics`, `ConsumerGoods`, `ConsumerProducts`, `Crafts`, `CryptoExchange`, `CryptoHedgeFund`, `CryptoMining`, `CryptoNews`, `CryptoPayroll`, `Culture`, `CustodialWallet`, `Cybersecurity`, `DataStorageAndManagement`, `DecentralizedCryptoExchangeAndTrading`, `DeFiDecentralizedFinance`, `Design`, `DigitalBrokerage`, `ElectricVehicles`, `EmbassiesConsulatesMissions`, `EnvironmentalServices`, `EscrowServices`, `Events`, `FabricsAndTextiles`, `Fashion`, `Fintech`, `FoodAndBeverage`, `GamblingHostingEnvironmentsWebsites`, `GamblingPaymentProcessors`, `GamblingSoftware`, `Government`, `GovernmentIDsDocumentsUniforms`, `Heavy`, `HighNetWorthIndividualExclusiveServices`, `Hospitality`, `ImmersiveExperience`, `InformationTechnology`, `LeisureAndRecreation`, `LicensedPharmaceuticals`, `LogisticsAndSupplyChain`, `LoyaltyRewardsAndUserEngagementTool`, `LuxuryGoods`, `MachineryAndHeavyEquipmentManufacturing`, `Media`, `MedicalDevicesAndSupplies`, `Music`, `NeobanksChallengerBanks`, `NFTMarketplace`, `NonCustodialWallet`, `OTCTrading`, `PawnBrokers`, `PersonalServices`, `PetCareAndSupplies`, `Photography`, `PoliticalOrganizationsPACs`, `PredictionMarkets`, `Printing`, `PrivateBankingWealthManagement`, `ProfessionalServices`, `Publishing`, `RealEstate`, `RestaurantsAndFoodServices`, `Retail`, `Robotics`, `ScrapyardDealers`, `Security`, `SkinsLootBoxTrading`, `Software`, `Space`, `SportsAndFitness`, `SustainableAndEthical`, `Technology`, `Telecom`, `ThirdPartyPaymentProcessors`, `TicketBrokersAgencies`, `Timeshare`, `ToolsAndHardware`, `ToysAndGames`, `TraditionalProducts`, `Transportation`, `TravelAndTourism`, `TrustAndCompanyServiceProvidersTCSP`, `Utilities`, `WasteManagement`
### Prohibited Industries
The following business activities are prohibited from using MoonPay Enterprise's services to ensure alignment with regulatory expectations and MoonPay Enterprise's risk appetite.
A submission whose resolved industry is one of these is rejected with [`prohibited_industry`](/errors#prohibited_industry). The first column is the literal `business_details.industry` value to check against on your side. No `industry_type` value currently resolves to a prohibited industry, so the block only fires on `industry`.
| `industry` value | Activity |
| - | - |
| `ForeignCorrespondentBanking` | Foreign Correspondent Banking |
| `Chemical` | Chemical |
| `DefenseMilitary` | Defense/Military |
| `MultiLevelMarketing` | Multi-Level Marketing |
| `TobaccoNicotineVapes` | Tobacco/Nicotine/Vapes |
| `Cannabis` | Cannabis |
| `WeaponsKnivesGuns` | Weapons/Knives/Guns |
| `BailBondsmanBailiffServices` | Bail Bondsman / Bailiff Services |
| `Nutraceuticals` | Nutraceuticals |
| `CarDealership` | Car Dealership |
| `Pharmaceutical` | Pharmaceutical |
| `DiamondIndustry` | Diamond Industry |
| `NomineeShareholderBearerShareEntities` | Nominee Shareholder / Bearer Share Entities |
| `AdultEntertainment` | Adult Entertainment |
| `DatingServices` | Dating Services |
| `GemsOrPreciousMetalsDealership` | Gems or Precious Metals Dealership |
| `SexualServices` | Sexual Services |
| `AntiquesAndCollectibles` | Antiques and Collectibles |
| `PrePaidCards` | Pre-Paid Cards |
| `ExoticAnimals` | Exotic Animals |
| `PrivateMilitaryOrSecurity` | Private Military or Security |
| `CashFundedRemittances` | Cash-Funded Remittances |
| `JewelryOrWatches` | Jewelry or Watches |
| `FXTrading` | FX Trading |
| `BinaryOptions` | Binary Options |
| `CreditServices` | Credit Services |
Don't resubmit a rejected company under a different, less accurate industry. Contact support to discuss an exception instead.
### Foundations
Foundations (`company_type: PrivateFoundation`) are subject to additional restrictions on the account purposes Iron can onboard them for. If your foundation doesn't fit within these restrictions, contact us to explore whether onboarding is possible.
### Business Ownership and Constitutional Documents
To comply with MoonPay Enterprise's KYB requirements, all businesses must provide clear, official documentation that reliably verifies their complete ownership structure, ultimate beneficial owners (UBOs), controlling individuals, and confirms the existence and ongoing operations of the legal entity.
If the entity does not have individual beneficial shareholders, we will need to confirm the individuals who significantly control the entity instead, such as its directors and officers.
**Supported entity types:**
* Sole Proprietorships
* Partnerships
* LLCs & Corporations
* Trusts
* Nonprofits
* DAOs
* Cooperatives
### Required Documentation per Entity Type
This lightweight documentation applies to **non-US sole proprietorships only**. A US sole proprietorship must instead provide the standard LLCs and Corporations-equivalent documentation for a US company — see [Company Documents](#company-documents) and [Sole Proprietor Onboarding](#sole-proprietor-onboarding).
* Official sole proprietorship license or registration document clearly naming the sole proprietor and confirming active operation
* Recent tax filings or official government-issued documents linking the proprietor to business operations
* Partnership agreements officially certified or notarized, detailing partners and ownership shares
* Officially issued partnership registration certificates clearly listing partners and their shares
* Articles of Incorporation or Articles of Organization issued by the official business registry, clearly listing shareholders/members
* Official shareholder/membership register or certified extracts from commercial registries clearly indicating owners and ownership percentages
* Share certificates officially verified or notarized
* Recent audited financial statements (for publicly traded or regulated entities), confirming the company's operations and ownership
* Official trust deed or certificate clearly identifying trustees, settlors, protectors (if applicable), and beneficiaries
* Notarized trustee affidavit clearly verifying trustee identity, roles, and authorities
* Official formation documents (e.g., Articles of Incorporation) clearly listing controlling individuals (e.g., directors or board members)
* Government-issued or officially filed annual reports or governance documents confirming ongoing operations and key individuals
* Official governance documents or independently audited membership agreements clearly outlining membership, control mechanisms, and operational status
* Official formation documents clearly identifying cooperative members
* Independently certified membership ledgers or agreements clearly indicating members and voting rights
### Document Verification Standards
All provided documents must be issued by an official governmental authority and, where applicable, certified by a notary public, lawyer, auditor, or similar independent professional, with Apostille certification where applicable.
* Must be officially issued by governmental authorities
* Must be verifiable online through an official registry or database (if not possible, must be independently certified)
* Apostille certification or equivalent legalization required where applicable
* Self-generated or internally-produced documents are NOT acceptable
* Must clearly display the full legal name and registration code of the business entity
* If ownership structure involves more than two layers, a clear organizational chart with ownership percentages is required
* Must be recent (issued within the past three months)
* Documents not in English must include certified translations
Documents must clearly confirm:
* The legal existence of the entity
* Its current operational status
* Clear identification of ultimate beneficial owners, controlling individuals, and precise ownership percentages
* Self-generated, internal, unsigned, or informal documents
* Simple printouts, screenshots, or unofficial emails
* Internally prepared capitalization tables (unless provided by reputable equity management services)
### Requirements for Regulated Entities
Entities that operate under regulatory supervision must provide additional documentation to confirm their licensing status and compliance with AML/CFT regulations.
#### Regulatory Licenses and Authorizations
A valid license or authorization issued by the relevant financial regulator or supervisory authority permitting the entity to conduct its business activities. If the license or authorization is not publicly accessible online, it must be certified by the issuing authority or a notary public.
#### AML/CFT Policy and Procedures
A copy of the entity's internal AML/CFT policy, outlining its approach to:
* Customer due diligence (CDD) and enhanced due diligence (EDD)
* Transaction monitoring processes
* Suspicious activity reporting procedures
* Sanctions screening and PEP checks
- The policy must be approved by senior management and aligned with the applicable jurisdiction's AML laws and regulations
- Licenses and authorizations must be verifiable online (if not, must be certified by the issuing authority, a notary public, or a licensed legal/audit professional)
- The submitted AML/CFT policy must be an officially approved internal document (not a draft or template)
# Individual
Source: https://dev.enterprise.moonpay.com/kyc
KYC requirements for onboarding individual customers. Covers standard, enhanced, and high-risk verification tiers with required documents.
## KYC Requirements
### Standard Onboarding (USA, EEA, UK, AUS, Low Risk RoW)
* Full name
* Residential Address
* Date of Birth
* Nationality
* Email
* Phone
* ID Document
* Liveness check/Selfie (comparison of liveness vs image on passport or DL and verification of document)
**Proof of Address (POA)** - always required at Tier 1 for customers residing outside the USA, EEA, and UK, including Australia. For USA, EEA, and UK residents it's conditional, and the triggers differ by region. Documents other than identity documents must be issued within the last 3 months. See [Proof of Address](/proof-of-address) for the region-by-region triggers and the accepted document list.
**SSN** - required for US citizens or residents only
* Use of product
* Employment status
* Source of Wealth
* Annual Income
* Monthly expected transaction count
* Monthly expected transaction value
### High Risk Jurisdiction Onboarding and EDD (Limit Increase)
All of the Standard Onboarding requirements plus Enhanced Due Diligence:
* Approximate net worth
* Industry
* Source of Wealth proof: bank statements, payslips, sale proceeds, or another document from the [accepted document list](/source-of-wealth)
* Proof of address
**Yearly Limit:** Unlimited but subject to review
Exception flows automatically trigger EDD workflow for customers residing in high-risk jurisdictions or customers residing in medium-risk jurisdictions who are aged 67 years or older. When this happens, MoonPay Enterprise sets `with_edd` to `true` on the identification response automatically. See [Tracking EDD Status](/onboarding#tracking-edd-status) for how to interpret this field.
## Appendix
### Questionnaire List
| Question | Answers | Onboarding Type |
| - | - | - |
| What will you use MoonPay Enterprise for? | Investing, Payments to friends/family/others, Purchase of digital assets, Online Purchases of Goods/Services, Trading | Standard |
| What is your employment status? | Employed or self-employed, Retired, Student, Unemployed | Standard |
| What's your yearly gross income? | Less than 20,000 to 150,000 (in increments of 10,000), then 150,001-200,000, 200,001-500,000, and More than 500,000 | Standard |
| What is your source of wealth? | Salary, Savings, Investments, Crypto trading, Other | Standard |
| How many times do you expect to transact per month? | Less than 5, 5-10, More than 10 | Standard |
| What's your approximate net worth? | Up to 25,000 / 25,001-50,000 / 50,001-100,000 / 100,001-300,000 / 300,001-500,000 / 500,001-1,000,000 / Over 1,000,001 | EDD |
| What industry do you work in? | Agriculture, Arts and entertainment, Construction, Education, Financial services, Information and technology, Manufacturing, Retail, Real Estate, Other | EDD |
| Source of Wealth verification | Bank statements or payslips showing stable income, proof of savings or investments, evidence of asset ownership or sale proceeds, documentation of crypto holdings, and other legitimate records demonstrating lawful accumulation of wealth. | EDD |
### Accepted ID Documents
* Passport
* National ID card
* Driver's License
* Residence Permit
* Digital Driver's License (Brazil-issued only)
### Verification Standards
* ID must be valid, readable, government-issued, and contain a person's photograph
* POA must be ≤ 3 months old
* SoW proof must be one of the accepted documents in [Source of Wealth](/source-of-wealth)
* Liveness must match ID
* Missing/inconsistent information may trigger additional review
# Limits and Minimums
Source: https://dev.enterprise.moonpay.com/limits-and-minimum
Transaction spending limits and minimum amounts per risk tier. Covers 52-week rolling limits per direction, verification requirements, and minimum transaction sizes.
## Spending Limits
Individual customer limits are calculated dynamically from the income and wealth information collected during KYC (the wealth questionnaire). There is no single fixed limit. The wealth questionnaire is what derives each Tier 1 customer's personalized cap, so higher declared income produces a higher limit with fewer manual reviews. The limit can be increased to Tier 2 (unlimited) by completing additional due diligence (compliance questionnaire and Source of Wealth proof).
Tier 1 limits apply **per direction**, tracked separately for fiat onramp and crypto offramp, measured on a 52-week rolling window that ends now. If compliance resets a direction, that direction's window starts at the reset instead, so the two windows can differ. Business customers (KYB) have no enforced limit.
| Risk Category | Tier (Access Level) | Limit per Direction (52-week rolling) | Required Verification |
| - | - | - | - |
| Low and medium risk | Tier 1 | Dynamic (based on declared income) | Government-issued ID + Liveness check + KYC Questionnaire + Proof of address (conditional in the USA, EEA, and UK; always required elsewhere) |
| | Tier 2 | Unlimited | Government-issued ID + Liveness check + Proof of address + KYC Questionnaire + SoW proof |
| High risk | N/A | Unlimited | Government-issued ID + Liveness check + Proof of address + KYC Questionnaire + SoW proof |
Proof of address at Tier 1 depends on where the customer resides. Customers residing outside the USA, EEA, and UK always provide one, including Australia. For USA, EEA, and UK residents, MoonPay Enterprise requests it only on specific triggers, and those triggers differ by region. At Tier 2 and for high-risk customers, proof of address is always required. See [Proof of Address](/proof-of-address) for the region-by-region triggers.
Because Tier 1 limits are personalized per customer, do not hard-code a threshold. Read the live value from [Get customer spending limit usage](/reference/customer/get-customer-spending-limit-usage) (`threshold` per direction). The per-direction limit is **not** a combined cap: a Tier 1 customer can spend up to their limit on fiat onramp **and** up to their limit on crypto offramp within the same 52-week window before either direction triggers a tier upgrade.
## Checking Remaining Spending Limit
Poll [Get customer spending limit usage](/reference/customer/get-customer-spending-limit-usage) to read a customer's exact `used` and `remaining` per direction. Use it to trigger proactive EDD before they hit a threshold, instead of waiting for a paused transaction.
To read a customer's live usage, call [Get customer spending limit usage](/reference/customer/get-customer-spending-limit-usage). The response returns `threshold`, `used`, and `remaining` per direction (fiat on-ramp and crypto off-ramp), in `currency` cents as strings, where `currency` is the customer's fiat currency. `threshold` and `remaining` are `null` for Business customers and Regulated persons (no enforced cap).
The response also includes `threshold_eur`, `used_eur`, and `remaining_eur`, EUR-converted views of the same values. These are deprecated: use `threshold`, `used`, and `remaining` instead.
## Proactively Increasing Customer Limits
Partners can proactively upgrade a customer to Tier 2 (unlimited) by initiating Enhanced Due Diligence (EDD) upfront, before the customer hits any spending threshold. This avoids transaction pauses and provides a smoother experience for high-volume customers.
### How to Trigger EDD
When creating an identification via the [Create Customer Identification V2](/reference/customer/create-customer-identification-v2) endpoint, you can trigger EDD depending on the identification type:
**Link flow.** Set `with_edd` to `true`:
```json theme={null}
{
"type": "Link",
"with_edd": true
}
```
**Token flow.** Include `edd_questionnaire` in the request body. See [SumSub Token Sharing](/reliance-kyc-token-sharing#with-kyc--edd-questionnaire) for the full example and field reference.
**Person flow (KYC/KYB Sharing).** Include `edd_questionnaire` in the Person params. See [KYC/KYB Sharing](/sharing#triggering-edd) for details.
### How It Works
* If the customer already has an approved KYC, MoonPay Enterprise will reuse it and only prompt for the additional EDD steps (no duplicate verification required).
* If the customer has not yet completed KYC, the full KYC + EDD flow will be initiated.
* The same identification URL used for regular KYC is used for KYC + EDD, so partners can copy the link from the Partner Dashboard as usual.
Once the identification is created, the `with_edd` field on the [Identification response](/onboarding#tracking-edd-status) indicates whether EDD was applied. This field is also set automatically when MoonPay Enterprise's AML checks determine EDD is required.
Proactive EDD is only available for individual customers. Business customers (KYB) are not supported.
## Handling Transactions Paused Due to Limit Thresholds
MoonPay Enterprise sets spending limits per customer to support its risk-based approach to transactions oversight. When a customer reaches their threshold, transactions are paused until further verification is completed.
### How It Works
When a customer hits their transaction or balance limit:
* The transaction is **paused**.
* The customer's status moves to `IdentificationRequired`.
* MoonPay Enterprise triggers a **tier upgrade**, requiring additional identity verification (including Source of Wealth, "SoW").
* The partner is responsible for providing the customer with the identity link for the tier upgrade.
### Partner Responsibilities
Do one of the following:
* Provide the customer with the identity link for the tier upgrade.
* Contact the end customer, collect the required document (the Source of Wealth proof), and pass it to the MoonPay Enterprise compliance team.
### Timing
* If the customer submits the documents **within 24 hours**, MoonPay Enterprise reviews, clears the transaction, and can **raise the customer's limit**.
* If the customer fails to provide the required documents in time, the transaction is **rejected and returned**.
### Bank-Triggered Blocks
In some cases, a **banking partner** may block a transaction due to compliance concerns. MoonPay Enterprise's compliance team will:
* Contact the partner and request additional customer documentation (typically the SoW proof).
* Optionally, set the customer status to `IdentificationRequired`.
* The partner should assist the customer in providing the documents promptly to avoid transaction rejection.
### Important Notes
* MoonPay Enterprise's compliance team can apply new limit thresholds after reviewing the submitted documentation.
* If the document arrives **after** the 24-hour window, compliance can still raise the limit, but the original transaction does not resume. You must retry it.
## Source of Wealth (SoW) Document Requirements
Source of Wealth proof is required to upgrade a customer to Tier 2 and for Enhanced Due Diligence. For accepted documents by source and acceptance criteria, see [Source of Wealth](/source-of-wealth).
## Minimum Transaction Amount
The minimum supported amount depends on the payout method. Any request below the applicable threshold is rejected and not executed, because processing and network costs would exceed the transferred value.
| Transaction type | Minimum amount |
| - | - |
| Standard onramp and offramp | **1** unit of the payout currency (e.g. 1 EUR, 1 USD) |
| SWIFT / wire payouts | **5** units of the payout currency |
| Stablecoin mint and redeem | **10** units of the input currency |
When a deposit arrives but the resulting payout would fall below the applicable minimum, the deposit is returned to the customer.
Incoming crypto deposits worth less than `1 EUR` (or equivalent) are treated as dust. They are not processed and are not returned.
Communicate these minimums to your end users, especially during onboarding or when guiding them through their first test transaction. This helps avoid confusion and unnecessary troubleshooting.
## Micro-Deposits (Penny Tests)
Some banks and payroll providers verify a Virtual Account by sending a small test deposit before routing real payments to it. MoonPay Enterprise supports two ways to detect these deposits, depending on the rail.
### ACH: Microdeposits
Microdeposits are supported in both the API and dashboard.
* **API:** `GET /api/customers/{id}/microdeposits` returns microdeposits for a customer. See [Get customer microdeposits](/reference/customer/get-customer-microdeposits).
* **Dashboard:** **Customers → select customer → Microdeposits**. Shows amount, currency, counterparty and date. The section is hidden when there are no records.
### All rails: below-minimum rejection webhook
SEPA and other rails carry no scheme-level micro-deposit flag, so detection runs through the transaction lifecycle:
* Send a deposit below the applicable minimum (for example 0.01 EUR) from the bank account you want to verify to your Virtual Account.
* The deposit creates a transaction that is rejected with `transaction_status: RejectedMinAmount`. This is a terminal status: the funds are not converted and no payout is executed.
* MoonPay Enterprise sends a [transaction status webhook](/webhooks) for the rejection. Watch for `transaction_status` set to `RejectedMinAmount` on your webhook endpoint.
* The webhook payload carries the transaction `id` and status, not the amount. Fetch the transaction with [Get autoramp transactions by transaction IDs](/reference/autoramp/get-autoramp-transactions-by-transaction-ids) and confirm the amount and deposit account match the test you sent.
The deposit must land on a Virtual Account with an active Autoramp. If it does not match one (for example, a payment-by-reference account receiving a deposit without the expected reference), MoonPay Enterprise returns the deposit and no transaction or webhook is created. See [When no transaction appears](/transaction-status#when-no-transaction-appears).
# Offramp
Source: https://dev.enterprise.moonpay.com/offramp
Create a liquidation address that accepts stablecoins and settles in fiat into a bank account.
Give your users a **liquidation address**: a persistent wallet address that accepts stablecoins and settles the proceeds as fiat into a registered bank account.
Examples:
* A **liquidation address** that turns incoming USDC into EUR and pays out to a Deutsche Bank account
* A **liquidation address** that turns incoming USDC into GBP and pays out to a Revolut account
## How it works
Max registers his bank account and gets a **liquidation address** owned by MoonPay Enterprise
An `autoramp` ties that address to his bank account: USDC in, EUR out
Max sends 1000 USDC to the liquidation address
As soon as funds hit the address, MoonPay Enterprise converts them to EUR and pays out to Max's bank account
Max can transfer at any time. The address is persistent.
Non-supported assets sent to the address are returned to sender
## Two ways to offramp
| | Exact-out quote | Standing autoramp |
| - | - | - |
| **You specify** | The exact fiat amount to land in the bank | The route only |
| **Rate** | Locked for the quote window | Current mid-market rate at deposit |
| **Best for** | Paying an invoice, payroll, a fixed obligation | Continuous sweeping of a balance |
| **Deposit matching** | Deposit must match the quote's `amount_in` | Any supported deposit converts |
Most offramp integrations start with an exact-out quote, because the payer usually owes a precise fiat figure. Both are shown below, and a single quote-source autoramp can keep accepting new quotes over time.
## 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.
```bash theme={null}
export API_KEY=""
export BASE_URL="https://api.sandbox.iron.xyz"
```
Your customer must have `Active` status: terms signed and [identification](/onboarding) (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, and subscribe to the `customer_fiat_abilities` [webhook](/webhooks) to track rail changes instead of polling. See [Onramp](/onramp#onboard-a-customer-from-scratch) for a full onboarding walkthrough and [Tracking ability changes](/onboarding#tracking-ability-changes) for the webhook.
Register the destination bank account via `POST /api/addresses/fiat`. See [Fiat Addresses](/fiat-addresses). The account starts in `RegistrationPending` status.
Save the returned fiat address ID. Quotes reference it as `recipient_account_id`.
In Sandbox, the fiat address stays in `RegistrationPending` until you manually advance it:
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/fiat-verification/$FIAT_ADDRESS_ID" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Registered"'
```
In production, registration is handled automatically.
Register your customer's wallet addresses via [Crypto Addresses](/crypto-addresses) for [Travel Rule](/travel-rule) compliance. Funds from unregistered wallets may be delayed for manual review.
Either request an exact-out quote and create a quote-source autoramp, or create a standing autoramp directly. Both are shown below.
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:
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/autoramp/$AUTORAMP_ID" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Approved"'
```
In production, an autoramp reaches `Approved` on its own once its deposit account is provisioned and verified.
Quote requests fail if `recipient_account_id` references a bank account that has not been registered or has not passed verification. Complete the Sandbox approval step above first.
## Offramp with an exact-out quote
Use this when your customer owes a precise fiat amount. You ask for the output, MoonPay Enterprise tells you the required stablecoin input.
`GET /api/autoramps/quote`
Set `amount_out` to the fiat figure you need to land in the bank. Set `amount_in` instead if you want to price a known stablecoin amount. Never set both.
```bash theme={null}
curl -X GET "$BASE_URL/api/autoramps/quote?customer_id=&source_currency_code=USDC&source_currency_chain=Ethereum&destination_currency_code=EUR&recipient_account_id=&amount_out=100&rate_lock_duration_minutes=10&rate_expiry_policy=Return&expiry_in_hours=1&is_third_party=false" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: $API_KEY"
```
```json theme={null}
{
"amount_in": {
"amount": "111.19863",
"currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" }
},
"amount_out": {
"amount": "100",
"currency": { "code": "EUR", "type": "Fiat" }
},
"customer_id": "",
"deposit_account_type": "Iban",
"destination_currency": { "code": "EUR", "type": "Fiat" },
"fee": {
"banking_fee": { "amount": "0.326434", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"iron_fee": { "amount": "0.326434", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"network_fee": { "amount": "1.734570", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"partner_fee": { "amount": "0", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"total_fee": { "amount": "2.387438", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } }
},
"fee_settlement": "deductedImmediately",
"is_third_party": false,
"quote_id": "e8cd73a4-54cc-4a34-916d-238c5ce5f898",
"rate": "0.919023105",
"rate_expiry_policy": "Return",
"rate_lock_valid_until": "2025-03-14T11:37:24.250798+00:00",
"recipient_account": {
"type": "Fiat",
"account_identifier": { "type": "SEPA", "iban": "DE89370400440532013000" }
},
"signature": "36665918454471c078724046bcb82698eac2fb6f5f91c58a0f1a3923c5d95a28",
"source_currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" },
"valid_until": "2025-03-14T11:37:24.250798+00:00"
}
```
`amount_in` is what your customer must send: 111.19863 USDC to receive exactly 100 EUR, with the fee breakdown itemised.
`POST /api/autoramps`
Submit the signed quote payload verbatim.
```bash theme={null}
AUTORAMP_ID=$(curl -s -X POST "$BASE_URL/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '' | jq -r '.id')
echo "$AUTORAMP_ID"
```
The quote is digitally signed. Any modification, including changing values or omitting fields, causes the request to fail.
Once the autoramp is `Approved`, `deposit_rails` contains the liquidation address. Show your customer the address and the exact `amount_in` figure from the quote.
A deposit only matches a quote when its `amount_in` value and source currency match. Deposits that match no active quote, or only an expired one, are returned to sender.
**Staged setup.** You can create the autoramp with any initial quote to surface the deposit address early (so the payer can whitelist it), then attach the real quote later via `POST /api/autoramps/{autoramp_id}/quotes` once the amount is known. The initial quote expires unused.
### Rate locks and expiry
`rate_lock_duration_minutes` is optional and clamped to the cap for the pair. Stablecoin to fiat cross-currency pairs (e.g. USDC to EUR) cap at **10 minutes**; pegged pairs (a USD-stablecoin to USD, a EUR-stablecoin to EUR) cap at **2 days**.
`rate_expiry_policy` decides what happens when funds arrive after the lock expires:
* `Return`: validity equals the rate lock. Late funds are refunded.
* `Slippage`: validity extends beyond the lock. Late funds execute if the market rate is within `slippage_tolerance_in_bips`, otherwise they are refunded or requoted.
See [Quotes](/quotes) for the full parameter reference, multi-currency input, and attaching quotes over time.
## Offramp with a standing autoramp
Use this when you want a persistent address that converts whatever arrives at the current mid-market rate, with no amount agreed up front.
`POST` `/api/autoramps`
The `Idempotency-Key` header is required. Send the same key to safely retry a request without creating a duplicate autoramp.
### Request fields
| Field | Required | Description |
| - | - | - |
| `source_currencies` | Yes | Array of input currencies (Crypto or Fiat objects) |
| `destination_currency` | Yes | Output currency (Crypto or Fiat object) |
| `recipient_account` | Yes | Destination account. For fiat: `{ "type": "Fiat", "account_identifier": { ... } }` |
| `customer_id` | Yes | The customer's UUID |
| `source_is_third_party` | No | Whether the source allows third-party payments |
| `sepa_payment_reference` | No | Reference for outgoing SEPA transfers (max 140 chars) |
| `ach_payment_reference` | No | Reference for outgoing ACH transfers (max 10 chars) |
| `batch_payout` | No | If `true`, payouts are batched daily instead of sent individually. Fiat destinations only. |
The `account_identifier` inside `recipient_account` uses the same payment rail discriminator as [Fiat Addresses](/fiat-addresses): `SEPA` (requires `iban`), `ACH` / `Wire` / `FEDNOW` (require `routing_number` + `account_number`), `SWIFT` (requires `bic` + `account_number`), `CHAPS` / `FPS` (require `sort_code` + `account_number`).
### Request
```bash Bash theme={null}
AUTORAMP_ID=$(curl -s -X POST "$BASE_URL/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"source_currencies": [{
"type": "Crypto",
"token": "USDC",
"blockchain": "Ethereum"
}],
"destination_currency": {
"type": "Fiat",
"code": "EUR"
},
"recipient_account": {
"type": "Fiat",
"account_identifier": {
"type": "SEPA",
"iban": "DE89370400440532013000"
}
},
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"source_is_third_party": false
}' | jq -r '.id')
echo "$AUTORAMP_ID"
```
Capturing the ID into `$AUTORAMP_ID` lets you paste the remaining commands on this page straight into the same shell.
The `account_identifier` you provide in `recipient_account` must match a bank account you've already registered via the [Fiat Addresses API](/fiat-addresses).
### Response
```json theme={null}
{
"id": "9a7b6c5d-e3f1-4a2b-8c7d-6e5f4a3b2c1d",
"status": "Authorized",
"deposit_rails": [],
"created_at": "2025-01-20T13:45:23Z"
}
```
`deposit_rails` is empty at `Authorized`. Poll `GET /api/autoramps/{id}` or subscribe to webhooks until `status = Approved`, then read the liquidation address from `deposit_rails`:
```json theme={null}
"deposit_rails": [
{ "type": "Crypto", "chain": "Ethereum", "address": "0xYourEthereumAddress" }
]
```
See [Autoramp Status](/autoramp-status).
The example above is trimmed to the fields most integrations need. The full `Autoramp` object also returns `kind`, `source`, `recipient`, `customer_id`, `source_currencies`, `destination_currency`, `is_third_party`, `fee_profile_id`, `batch_payout`, and `quotes`. The legacy `deposit_account` field is deprecated.
**Shared EVM deposit address.** An offramp gets one deposit address per source chain, and every EVM chain on the same autoramp shares one address, so the EVM entries in `deposit_rails` repeat the same string. Only the token and chain combinations listed in `source_currencies` are converted: a deposit arriving on an EVM chain you did not declare is not swept, even though the address matches. Declare every chain you want to accept when you create the autoramp. Non-EVM chains such as Solana still get their own distinct deposit address.
### Error response
Validation failures return the error as a plain string body, not a structured object. An `account_identifier` that does not match a bank account already registered and verified for this customer returns `400`:
```json 400 Bad Request theme={null}
"recipient fiat account not verified"
```
This includes a bank account you submitted that is still in `RegistrationPending`: the lookup only matches accounts in `Registered` status. Complete the Sandbox approval step, or wait for registration in production, before you create the autoramp.
Show the user their liquidation address along with the supported assets for it (e.g. EURC, USDC). Non-supported assets are returned to sender.
## 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.
`POST /api/sandbox/transaction` builds a transaction directly from the route, reusing its currencies, customer, and recipient.
```bash theme={null}
TRANSACTION_ID=$(curl -s -X POST "$BASE_URL/api/sandbox/transaction" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d "{\"autoramp_id\": \"$AUTORAMP_ID\", \"amount\": \"1000\"}" | jq -r '.id')
```
The response carries `amount_in`, `amount_out`, `currency_in`, `currency_out`, and `state`. See [Sandbox](/sandbox#create-a-sandbox-transaction) for the optional `fee`, `fx_rate`, and `initial_state` fields.
The simulated transaction starts in `Pending`. Advance it to trigger the rest of the status webhooks.
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/transaction/$TRANSACTION_ID/state" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "state": "Completed" }'
```
Your endpoint should have received two `transaction_status` events: `ConversionInProgress` when you created the transaction, then `Completed` when you advanced its state. The sandbox writes the state you ask for straight onto the transaction, so it does not send the `transaction` event or the intermediate `FundsReviewInProgress` and `PayoutInProgress` statuses that a live deposit produces.
Read `transaction_status`, not the deprecated `status` field, for the current state. Payload shapes and the full status table are in [Monitoring payments](/onramp#monitoring-payments); signature verification is in [Webhooks](/webhooks).
On an offramp, `Completed` means the payout was sent to the bank. Arrival depends on the rail: SEPA and ACH settle on their own schedules. See [Transaction Status](/transaction-status).
## Simulate it in the Dashboard
Register and approve the bank account first: **My Address → Linked Bank Accounts** in the **App** view, then **Developer → Sandbox → Fiat Accounts** and set the entry under **Unverified Fiat Accounts** to **Registered**. Until you do, the offramp has nowhere to pay out.
You can run this same flow without writing a single API call. The [Partner Dashboard](https://app.sandbox.iron.xyz) covers every step, from creating the route to settling a simulated deposit.
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 .
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.
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.
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.
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
| | Sandbox | Production |
| - | - | - |
| API base URL | `https://api.sandbox.iron.xyz` | `https://api.iron.xyz` |
| Fiat address registration | You call `PUT /api/sandbox/fiat-verification/{id}`, or set **Registered** in the Dashboard | Automatic |
| Autoramp approval | You call `PUT /api/sandbox/autoramp/{id}`, or set **Authorized** in the Dashboard | Automatic once the deposit account is provisioned and verified |
| Deposits | Simulated via `POST /api/sandbox/transaction` or **New Transaction** in the Dashboard | Real stablecoins arriving at the liquidation address |
| Quote rates | Simulated, but signatures work identically | Live pricing |
Remove every `/api/sandbox/*` call from your integration. Those endpoints do not exist in production.
## What to read next
Full quote reference: expiry policies, multi-currency input, attaching quotes over time
Register bank accounts per payment rail and track registration status
The reverse route: a named virtual account that settles into stablecoins
Status mapping, settlement times, and the polling API
Signature verification, payload schemas, and retry behaviour
How batched payouts and daily settlement work
# Onboarding Lifecycle
Source: https://dev.enterprise.moonpay.com/onboarding
End-to-end onboarding flow for partners and customers. Covers customer creation, terms and conditions, identification, status tracking, and activation.
MoonPay Enterprise gives you full visibility into your customer onboarding: clear status at every stage, written feedback from our compliance team when something needs fixing, and real-time insight into your customer's payment abilities.
## Steps to Onboard and Activate a Customer
`POST /api/customers`
Create a customer record. Present the terms and conditions next, before you create an identification.
The status the customer is created in depends on the API version you send:
| `X-API-Version` | Created status |
| - | - |
| `2026-08-01` or later | `SigningsRequired`. Terms are the first step |
| Earlier, or header omitted | `IdentificationRequired` |
The version is pinned to the customer when you create it, and it drives that customer's onboarding for its lifetime. Existing customers keep the behavior they were created with, so adopting `2026-08-01` only affects customers you create after you start sending it. See [API Versioning](/versioning).
```bash theme={null}
curl --request POST \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{"name": "new_amazing_customer", "email": "customer@example.com", "customer_type": "Person"}' \
--url 'https://api.sandbox.iron.xyz/api/customers'
```
`GET /api/terms-and-conditions?country={ISO3}`
Fetch 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. `country` is a required parameter, but as of the [terms update in the changelog](/changelog#2026-08-01) it no longer changes which document you get back. See [Terms and Conditions](#terms-and-conditions) for the current behavior and what changed.
For customers created on `2026-08-01` or later you can skip this endpoint entirely and call `GET /api/customers/{id}/required-signings` instead. It returns the same document, takes no `country`, and is the same call you need after KYC and for [every future document](#signing-new-terms-later), so one code path serves the whole lifecycle. Use `GET /api/terms-and-conditions?country={ISO3}` when you need the terms before a customer record exists, or for customers created on an earlier version.
```bash Get terms for a country theme={null}
curl --request GET \
--header 'accept: application/json; charset=utf-8' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/terms-and-conditions?country=DEU'
```
```bash Record the signing theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{
"content_id": "",
"signed": true
}' \
--url 'https://api.sandbox.iron.xyz/api/customers//signings'
```
**Example response from terms-and-conditions:**
```json theme={null}
[
{
"display_name": "Terms and Conditions",
"id": "019ababb-ddd6-7f02-8f5d-7469a0e8afb6",
"url": "https://example.com/terms-and-conditions"
}
]
```
Pass the `url` to your customer for review. The signing request takes `content_id` (the `id` from the terms response) and `signed: true`.
`POST /api/customers/{id}/identifications/v2`
Create an identification using one of these methods:
* Hosted MoonPay Enterprise KYC link
* SumSub token sharing
* KYC/KYB Sharing
This endpoint is not an upsert. Every call creates a new identification record, and the newest record drives the customer's status. If you create a new identification while another is in flight, the customer resets to `IdentificationRequired` and the older identification stops counting, even if it is approved later.
Create one identification, then wait for it to reach a terminal status (`Approved`, `Declined`, or `Expired`) before creating another. To track progress, subscribe to the `identification_status` webhook or poll `GET /api/customers/{id}/identifications`. Never re-call this endpoint to refresh status or to correct a submission.
An `Idempotency-Key` does not protect you here. It stops one request running twice. It does not stop two requests creating two records. See [Resubmitting and Correcting a Submission](/kyb#resubmitting-and-correcting-a-submission).
```bash theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/customers//identifications/v2' \
--data '{"type": "Link"}'
```
Once approved, MoonPay Enterprise checks whether the customer has signed the current terms. If they have, and nothing else is outstanding, the customer becomes `Active` and you can move on. Having signed the terms in step 2, that is the normal outcome.
Handle the other branch anyway. If anything is outstanding the status is `SigningsRequired` instead: call `GET /api/customers/{id}/required-signings`, present each returned document, and mark each as signed via `POST /api/customers/{id}/signings`. The customer becomes `Active` once none are left.
When nothing is outstanding, the response depends on your API version: `2026-08-01` and later return `200` with an empty list, earlier versions return `409 Conflict`.
This is not only an onboarding branch. An `Active` customer returns to `SigningsRequired` whenever a new document is published, and cannot transact until they sign it, so the flow you build here needs to stay reachable for the life of the account. See [Signing new terms later](#signing-new-terms-later).
```bash Retrieve signings theme={null}
curl --request GET \
--header 'accept: application/json; charset=utf-8' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/customers//required-signings'
```
```bash Sign a document theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'content-type: application/json; charset=utf-8' \
--header 'idempotency-key: ' \
--header 'x-api-key: ' \
--data '{
"content_id": "",
"signed": true
}' \
--url 'https://api.sandbox.iron.xyz/api/customers//signings'
```
**Example response from required-signings:**
```json theme={null}
[
{
"display_name": "Terms and Conditions",
"id": "019ababb-ddd6-7f02-8f5d-7469a0e8afb6",
"url": "https://example.com/terms-and-conditions"
}
]
```
Pass the `url` to your customer for review. The signing request takes `content_id` (the `id` from `required-signings`) and `signed: true`.
`abilities.fiat_payout` is nested by currency, then by rail. Check the specific rail you plan to use, for example `abilities.fiat_payout.usd.ach === "Active"`, before initiating payouts.
```json theme={null}
{
"fiat_payout": {
"usd": { "ach": "Active", "wire": "Active", "rtp": "Unavailable", "fednow": "Unavailable", "swift": "Active" },
"eur": { "sepa": "Active", "swift": "Unavailable" },
"gbp": { "fps": "Active", "chaps": "Active", "swift": "Unavailable" },
"brl": { "pix": "Unavailable" }
}
}
```
Each rail also has a `_thirdparty` variant (e.g. `ach_thirdparty`) for payouts to a third party. The abilities object also returns `fiat_deposit` with the same shape and a `currencies` array listing the flows (mint, redeem, onramp, offramp, swap) available per currency.
Call the endpoint once here, then subscribe to the `customer_fiat_abilities` [webhook](/webhooks) and keep your copy current from the deliveries. Rails turn on at times you cannot predict from your own requests, so the webhook replaces polling this endpoint. See [Tracking ability changes](#tracking-ability-changes).
Before an active customer can transact, register their wallet addresses for [Travel Rule](/travel-rule) compliance. Self-hosted wallets register with a signed proof-of-ownership message, or, for US and Rest of World customers, by [self-attestation](/crypto-addresses#register-a-self-attested-crypto-address). See the [Crypto Addresses guide](/crypto-addresses).
A customer's status reverts from `Active` to `SigningsRequired` or `IdentificationRequired` when new compliance actions are required (e.g. updated terms and conditions, fraud review, enhanced due diligence). Plan for this: see [Signing new terms later](#signing-new-terms-later).
## Terms and Conditions
The terms and conditions contain the data sharing agreement between MoonPay Enterprise and your customer. Your customer must accept them before MoonPay Enterprise collects KYC data, so present the terms right after creating the customer and before creating an identification.
### Fetching Terms Before KYC
For a customer created on `2026-08-01` or later, `GET /api/customers/{id}/required-signings` returns the outstanding document at any point in their lifecycle, including before identification. Prefer it: no `country` parameter, and the same call serves onboarding and every document afterwards.
The endpoint below is still the way to fetch the terms before a customer record exists, and the only way for customers created on an earlier version.
`GET /api/terms-and-conditions?country={ISO3}`
| Parameter | Required | Description |
| - | - | - |
| `country` | Yes | ISO 3166-1 alpha-3 code of the customer's country (e.g. `DEU`) |
MoonPay Enterprise serves a single global document, the same for every customer regardless of country. `country` is still a required parameter, but its value no longer changes which document you get back. Pass the customer's ISO 3166-1 alpha-3 code. A 3-letter code MoonPay Enterprise doesn't map to a region (for example `MCO`) returns `500`, so don't retry it. The response is a list with `id`, `url`, and `display_name`, the same shape as `required-signings`. Present the `url` to your customer, then record acceptance via `POST /api/customers/{id}/signings` with `content_id` and `signed: true`.
A malformed country code returns `400` with a plain string body:
```json theme={null}
"Invalid country code. Expected a 3-letter ISO 3166-1 alpha-3 code."
```
This is the current behavior, in effect since the [terms update noted in the changelog](/changelog#2026-08-01). Earlier API versions still resolve a region-specific document from `country`, and compare the signed terms against the region KYC verifies. See the changelog entry for that behavior if you're integrating against an earlier version.
### Validation After KYC
After the identification is approved, MoonPay Enterprise checks that the customer has signed the current terms document. If they have, and nothing else is outstanding, they're `Active`; otherwise `GET /api/customers/{id}/required-signings` returns the document to present and sign.
### Signing New Terms Later
Signing terms is not a one-time onboarding step. When MoonPay Enterprise publishes a new document, the previous signing no longer satisfies the requirement: affected customers move from `Active` back to `SigningsRequired`, and `GET /api/customers/{id}/required-signings` returns the new document. They cannot transact until they sign it.
This is how new capabilities reach your customers. Access to an additional currency, a new payment rail, or a new product often depends on terms your customer has not seen yet. The faster they can sign, the faster you can turn those features on for them.
So treat the signing flow you build during onboarding as a permanent, reusable path rather than a step you run once:
* **Poll or subscribe.** Subscribe to the `customer_status` [webhook](/webhooks) and treat a transition into `SigningsRequired` as a prompt to act, rather than discovering it from a failed transaction.
* **Reuse one code path.** For customers created on `2026-08-01` or later, `required-signings` returns whatever is currently outstanding at any point in the lifecycle. The same function that handles onboarding handles every later document.
* **Never cache "terms signed" as a permanent flag.** Match on the `content_id` that `required-signings` returns. A customer who signed last year's document has a signing on file but still owes the current one.
* **Keep it reachable in your UI.** Your customer should be able to sign from wherever they already are, without contacting your support team or restarting onboarding.
Signings are kept for audit, never replaced. `GET /api/customers/{id}/signings` returns the full history, so an earlier document and the current one both appear. Presence of *a* signing does not mean the current document is signed. Compare against `required-signings`.
## Handling Missing Information
If an identification is incomplete, the customer's status is `IdentificationRequired` and a `url` is returned on the Identification object. Redirect your customer to this URL. It opens a hosted step-up flow that collects only the missing data.
This occurs when:
* A data point is found to be invalid, expired, or inconsistent
* A limit triggers additional due diligence requirements
A Business submission missing required documents never reaches this state. It is rejected with `400` at creation, in every country, and no identification is created. See [Incomplete Submissions](/kyb#incomplete-submissions).
## Checking Outstanding Requirements
`GET /api/customers/{id}/requirements`
The customer `status` tells you the stage of onboarding. This endpoint tells you which verification requirements are satisfied, awaiting review, or need attention. Use `X-API-Version: 2026-10-01` or later for the response described here. See the [API Reference](/reference/customer/get-customer-requirements) for the selected version's schema.
Verification categories with outstanding requirements: `kyc`, `kyb`, or an empty array. Includes requirements awaiting review, so a non-empty array does not always mean the customer needs to act. An empty array does not confirm that terms are signed or payment rails are active.
Requirements the customer has satisfied. Each item has `code` and `description`.
Requirements awaiting review. Each item has `code` and `description`. Show a waiting state for these items.
Requirements awaiting customer input. Each item has `code` and `description`. Questionnaire items also include `missing_questions`, with each unanswered question's `id`, `title`, and nullable `section_id`.
Requirements with review issues. Each item includes `code`, `description`, nullable `review_comment` and `retryable`, `reject_reasons`, and rejected document `steps`. See [Handling Review Issues](#handling-review-issues).
The identification associated with the response. This is a top-level field, not a field on each requirement. It is `null` when no identification is associated with the response.
Feedback about the overall submission, when available for requirements awaiting review or with issues. Show it alongside the more specific requirement and document comments. See [Displaying Onboarding Comments to Your Customer](#displaying-onboarding-comments-to-your-customer).
Preliminary assessment, when available. Contains `status`, `missingInputs`, `reasons`, and `workflowErrors`. Its `status` is `likely_pass`, `fail`, `likely_fail`, or `inconclusive`. This is an early signal, not a verification approval or a replacement for the requirement buckets.
All four buckets are always present, including when empty. `pendingReview` uses that exact casing.
Earlier API versions and requests without `X-API-Version` keep the legacy format: `missing` means awaiting input, and `pending` means awaiting review. From `2026-10-01`, use `pending` for input and `pendingReview` for review, and read `reject_reasons[].error_code` instead of `reject_labels`. See [API Versions](/versioning).
### Requirement Items
Use each item's `code` to identify the requirement. Verification codes are `kyc_selfie`, `kyc_identity`, `kyc_applicant_data`, `kyc_proof_of_residence`, `kyc_questionnaire`, `kyb_company_data`, `kyb_company_beneficiaries`, `kyb_company_documents`, `kyb_questionnaire`, `identification_kyc`, and `identification_kyb`.
`description` explains what the requirement is. It is not the reviewer's feedback or a stable value to match on. Read the comment and rejection fields for what needs fixing. Only pending questionnaire items contain `missing_questions`; ordinary pending items and completed items do not carry that field.
### Handling Review Issues
Review problems are returned inside a successful `200` response. Read each `issues` item even when the HTTP request succeeds. These are separate from the validation errors returned when an identification submission fails with `400`.
Customer-facing feedback for this requirement. A `null` value does not mean there is no issue: also read `general_review_comment`, the rejection reasons, and document-step comments.
`true` means the customer can correct and resubmit to clear the issue. `false` means the issue is terminal. `null` means retryability is unknown; do not treat it as permission to retry. Follow the correction instructions for your integration, and contact [support](/support) if the next action is unclear.
Review rejection reasons for the requirement.
Stable code to use in application logic. See [Verification rejection codes](/errors#verification-rejection-codes) for the meanings.
Explanation of the code. The text can change; do not parse it or match on it.
Rejected document steps for this requirement. Several steps can have the same document type; keep each step's `id` to distinguish them.
UUID identifying the document step.
Document type, such as `PASSPORT` or `SHAREHOLDER_REGISTRY`.
Explanation of the document type.
Customer-facing instructions for this specific document.
This document's rejection reasons, each with `error_code` and `description`.
For example, `verification_document_expired` means the document is outside its accepted validity period. `verification_screenshots` means an original image is required instead of a screenshot. Check both `issues[].reject_reasons[].error_code` and `issues[].steps[].reject_reasons[].error_code`, and use the [full rejection-code list](/errors#verification-rejection-codes) for their meanings.
Handle `verification_unknown_rejection` and codes your app does not recognize with a general feedback message. An empty `reject_reasons` array does not mean approval: screening, fraud, and policy reasons are not exposed by this endpoint. Keep the issue visible and use the available comments and `retryable` value to guide the next action.
### Reading the Buckets Correctly
Read all four buckets together. For example, company data can be `complete`, beneficiaries can be in `pendingReview`, the questionnaire can be in `pending`, and company documents can be in `issues` at the same time. Show the questionnaire and document corrections alongside the review progress; do not hide actionable work behind a waiting screen.
* **`complete`:** No action is needed for that requirement.
* **`pendingReview`:** Show that the item is awaiting review. Do not ask the customer to upload it again just because review is unfinished.
* **`pending`:** Guide the customer to supply the requested information. For questionnaires, use `missing_questions` to identify unanswered questions.
* **`issues`:** Read the comments and rejection codes, then check `retryable` before offering a correction or resubmission.
When detailed requirements are not yet available, `identification_kyc` or `identification_kyb` can represent the identification as a whole. In `pending`, it means the identification flow still needs input or has not started. In `pendingReview`, it means the submission is being processed or reviewed. Use top-level `identification_id` to find the associated identification. If it is `null`, there is no associated identification to resume; start the initial identification after completing required signings.
A requirement remaining outstanding is not a reason to create another identification. Continue the existing flow or follow the correction process for your integration. See [Resubmitting and Correcting a Submission](/kyb#resubmitting-and-correcting-a-submission) for business submissions.
The following example shows all four buckets for a business customer. Beneficiaries are awaiting review, while the customer still needs to answer questionnaire questions and correct a shareholder registry.
```bash theme={null}
curl --request GET \
--header 'Accept: application/json; charset=utf-8' \
--header 'X-API-Key: ' \
--header 'X-API-Version: 2026-10-01' \
--url 'https://api.sandbox.iron.xyz/api/customers//requirements'
```
```json theme={null}
{
"identification_id": "01a08c3e-97fd-73d0-991a-3f8a3293037b",
"complete": [
{
"code": "kyb_company_data",
"description": "Company information required for business verification."
}
],
"pendingReview": [
{
"code": "kyb_company_beneficiaries",
"description": "Associated-person information required for business verification."
}
],
"pending": [
{
"code": "kyb_questionnaire",
"description": "Business information required for verification.",
"missing_questions": [
{
"id": "sourceOfFunds",
"title": "What is the source of the company's funds?",
"section_id": "businessInformation"
},
{
"id": "expectedMonthlyVolume",
"title": "What monthly volume do you expect?",
"section_id": null
}
]
}
],
"issues": [
{
"code": "kyb_company_documents",
"description": "Company documents required for business verification.",
"review_comment": "Please correct the rejected company documents.",
"retryable": true,
"reject_reasons": [
{
"error_code": "verification_document_unsuitable",
"description": "The document type or certification does not meet requirements."
}
],
"steps": [
{
"id": "10000000-0000-5000-8000-000000000001",
"name": "SHAREHOLDER_REGISTRY",
"description": "A document identifying the company's current shareholders.",
"review_comment": "Provide a signed shareholder registry within the accepted validity period.",
"reject_reasons": [
{
"error_code": "verification_document_unsuitable",
"description": "The document type or certification does not meet requirements."
},
{
"error_code": "verification_document_expired",
"description": "The document is outside the accepted validity period."
}
]
}
]
}
],
"requirements_due": [
"kyb"
],
"preflight_summary": null,
"general_review_comment": "Please address the outstanding company document issues."
}
```
A request for a customer outside your account returns `404`:
```json theme={null}
"Customer not found for the provided ID."
```
## Mapping Onboarding Statuses in Your App
Use the customer's `status` for lifecycle gates, then use the requirements response to decide what verification work to show. Apply the lifecycle gates first: a suspended or archived customer must not see a retry or ready-to-transact screen just because a requirement permits a retry.
| Customer `status` | What to show | What to check |
| - | - | - |
| `UserRequired` | Partner Area user setup | Create the required user before continuing |
| `SigningsRequired` | Terms signing | Call `GET /api/customers/{id}/required-signings`, present the returned documents, and record acceptance |
| `IdentificationRequired` | Verification tasks and review progress | Read `pending`, `issues`, and `pendingReview` as described below |
| `Active` | Customer account, with payment availability per rail | Check `GET /api/customers/{id}/abilities` for the specific rail before offering a transaction |
| `Suspended` | Account unavailable | Contact support; do not offer a verification retry as a way to remove the suspension |
| `Archived` | Account permanently deactivated | Do not offer transactions or restart onboarding for this customer |
For customers in `IdentificationRequired`, combine the buckets rather than picking one screen from a single identification status:
| Requirements state | In your UI |
| - | - |
| `pending` has items | Show the requested input or resume the existing hosted flow |
| `issues` has items with `retryable: true` | Show the feedback and the supported correction flow |
| An issue has `retryable: false` | Show the available feedback without a retry action for that issue |
| An issue has `retryable: null` | Show the available feedback; resolve the next action before offering a retry |
| `pendingReview` has items | Show review progress alongside any input or corrections still needed |
| Only `pendingReview` has outstanding items | Show a waiting state without requesting another submission |
| No outstanding items | Re-read customer status and required signings; do not infer activation from empty buckets |
The customer object's `identification_status` is deprecated and returns `null`. When you need the identification's own `status` or hosted `url`, use `GET /api/identifications/{id}` with the requirements response's `identification_id`. If that ID is `null`, no identification is associated with the response; do not interpret this as an approval or rejection.
Use identification status for lifecycle context: `Pending` means the flow is in progress, `Processed` and `PendingReview` mean the submission is being processed or reviewed, and `Approved` means verification is approved. For `Declined`, check the requirement issues and retryability before offering another attempt. For `Expired`, follow the restart flow for your integration. `Archived` is terminal. Use requirement buckets for the individual tasks within that lifecycle.
Customers can return to `SigningsRequired` when new terms are due. For customers created with an API version earlier than `2026-08-01`, present terms before collecting identification data even if their initial status is `IdentificationRequired`. See [Terms and Conditions](#terms-and-conditions).
Subscribe to the `identification_status` [webhook](/webhooks) to track identification changes and re-fetch requirements. Track rail availability with the `customer_fiat_abilities` webhook. `Active` on the customer does not guarantee that a particular payment rail is active.
### Tracking EDD Status
Each identification includes a `with_edd` field that indicates whether Enhanced Due Diligence was applied. This field is an optional boolean:
* `true`: EDD was triggered (either by the partner or automatically by MoonPay Enterprise's AML checks)
* `false`: EDD was explicitly not required
* `null`: Identification was created before this feature was available
`with_edd` can be set in two ways:
1. **Partner-initiated.** Pass `with_edd: true` (Link flow) or include `edd_questionnaire` (Token/Person flow) when creating the identification. See [Proactively Increasing Customer Limits](/limits-and-minimum#proactively-increasing-customer-limits).
2. **Automatically by MoonPay Enterprise.** If AML checks determine EDD is required (e.g. customer resides in a high-risk jurisdiction), MoonPay Enterprise sets `with_edd` to `true` server-side.
Use `status` and `with_edd` together to understand where a customer is in the verification process:
| `status` | `with_edd` | Meaning |
| - | - | - |
| `Pending` | `null` / `false` | Standard KYC in progress |
| `Pending` | `true` | EDD flow in progress |
| `PendingReview` | `null` / `false` | Standard KYC under manual review |
| `PendingReview` | `true` | EDD complete at verification provider, awaiting compliance review |
| `Approved` | `null` / `false` | Standard KYC approved, no EDD |
| `Approved` | `true` | Approved through EDD |
| `Declined` | `null` / `false` | Standard KYC rejected |
| `Declined` | `true` | Rejected after EDD review |
### Edge Cases
| `status` | Meaning |
| - | - |
| `Suspended` | Blocked. Can happen at any stage. Contact support. |
| `UserRequired` | Partner-area user must be created first. Only applies if `requires_user` is enabled. |
### Displaying Onboarding Comments to Your Customer
Use the requirements response to tell your customer what needs attention in KYC or KYB. Read feedback at all three levels:
* **Overall submission:** `general_review_comment`.
* **Requirement:** `issues[].review_comment`.
* **Document:** `issues[].steps[].review_comment`, kept with the document's `id`, `name`, and `description`.
Show non-empty comments whenever the response contains them, including while other requirements are awaiting review. Do not restrict feedback to a customer's or identification's status. A `null` comment does not clear the issue: check the rejection reasons and the other comment levels.
Keep document instructions beside the document they refer to. Deduplicate repeated text within the same context, but keep comments for different documents separate even when the wording matches. An issue comment that repeats `general_review_comment` is returned as `null`; document comments can still repeat other feedback.
The [example response above](#reading-the-buckets-correctly) contains an overall comment, a company-document comment, and instructions for the rejected shareholder registry. This helper extracts all three levels and retains the requirement and document identifiers for your UI:
```typescript theme={null}
type RequirementsFeedback = {
general_review_comment: string | null;
issues: Array<{
code: string;
review_comment: string | null;
steps: Array<{
id: string;
name: string;
description: string;
review_comment: string | null;
}>;
}>;
};
type VerificationMessage = {
message: string;
requirement_code?: string;
step_id?: string;
document_name?: string;
document_description?: string;
};
function getVerificationMessages(
requirements: RequirementsFeedback,
): VerificationMessage[] {
const messages: VerificationMessage[] = [];
const seen = new Set();
const general = requirements.general_review_comment?.trim();
function add(comment: string | null, context: Omit) {
const message = comment?.trim();
if (!message) return;
const key = JSON.stringify([context.requirement_code, context.step_id, message]);
if (seen.has(key)) return;
seen.add(key);
messages.push({ message, ...context });
}
add(requirements.general_review_comment, {});
for (const issue of requirements.issues) {
if (issue.review_comment?.trim() !== general) {
add(issue.review_comment, { requirement_code: issue.code });
}
for (const step of issue.steps) {
add(step.review_comment, {
requirement_code: issue.code,
step_id: step.id,
document_name: step.name,
document_description: step.description,
});
}
}
return messages;
}
```
For an issue without comments, show an explanation based on the recognized rejection codes, or a general message if no code is available. Read both requirement and document `reject_reasons[].error_code`; see [Verification rejection codes](/errors#verification-rejection-codes) for their meanings. Check the issue's `retryable` value separately before showing a correction action.
`step_status` on the identification is deprecated for reading customer requirements from `2026-10-01`. Use the requirements buckets and their feedback fields to build this UI. Do not infer required work from null legacy step results or display raw step verdicts as correction instructions.
***
## API Endpoints
| Endpoint | Purpose |
| - | - |
| `POST /api/customers` | Create the customer record |
| `GET /api/customers/{id}` | Customer lifecycle `status` and identification IDs |
| `POST /api/customers/{id}/identifications/v2` | Create an identification (`Link`, `Token`, `Person`, or `Business`) |
| `GET /api/customers/{id}/identifications` | Identifications with their own `status` and hosted `url`, when available |
| `GET /api/identifications/{id}` | One identification, using the top-level `identification_id` from requirements |
| `GET /api/customers/{id}/required-signings` | Documents the customer currently owes, at any point in their lifecycle. Empty list when nothing is outstanding (`409` before `2026-08-01`) |
| `POST /api/customers/{id}/signings` | Record acceptance of one document |
| `GET /api/customers/{id}/signings` | Full signing history, kept for audit. Not a substitute for `required-signings` |
| `GET /api/customers/{id}/requirements` | Verification work in `complete`, `pendingReview`, `pending`, and `issues` (`2026-10-01` or later) |
| `GET /api/customers/{id}/abilities` | Customer capabilities: `fiat_deposit` and `fiat_payout` (nested by currency then rail) plus a `currencies` array. Read once, then track changes with the `customer_fiat_abilities` webhook |
| `GET /api/terms-and-conditions?country={ISO3}` | The current terms document. Use before an identification exists |
`Idempotency-Key` is required on every `POST` above (`POST /api/customers`, `POST /api/customers/{id}/identifications/v2`, `POST /api/customers/{id}/signings`). Generate a fresh UUID for each new operation, and reuse that same key when retrying that exact request. A retry sent with a new UUID is treated as a new operation and creates a second record. MoonPay Enterprise stores successful responses only, so retrying after an error runs the request again. See [Idempotency](/idempotency).
## Status Reference
### Customer Status
Returned on the customer object.
| Status | Description |
| - | - |
| `IdentificationRequired` | Must complete KYC/KYB |
| `SigningsRequired` | Must sign required documents. Reached before identification on `2026-08-01`, and also after approval if terms are outstanding |
| `Active` | Customer account active. Check payment abilities for the rail you need |
| `Suspended` | Blocked for compliance or fraud reasons |
| `UserRequired` | Partner-area user must be created first |
| `Archived` | Terminal. Set by `PUT /api/customers/{id}/archive`. The customer and all their identifications are permanently blocked from every service |
### Identification Status
Returned on each identification object.
| Status | Description |
| - | - |
| `Pending` | Customer has not started, or a business submission is still being processed by the verification provider |
| `Processed` | Documents submitted, awaiting review |
| `PendingReview` | Under compliance review |
| `Approved` | Approved |
| `Declined` | Rejected |
| `Expired` | Expired due to inactivity |
| `Archived` | Terminal. Set when the customer is archived. The record no longer drives customer status |
**Typical Flow:** `Pending` → `Processed` → `PendingReview` → `Approved` / `Declined`
The identification object also includes `with_edd` (boolean, nullable) to indicate whether EDD was applied. See [Tracking EDD Status](#tracking-edd-status) for the full interpretation table.
### Ability Status
Returned on each rail leaf under `abilities.fiat_payout` and `abilities.fiat_deposit` (e.g. `abilities.fiat_payout.usd.ach`).
| Status | Description |
| - | - |
| `Active` | Available for use |
| `Pending` | Activation in progress |
| `Unavailable` | Not offered for this customer |
| `Blocked` | Blocked for this customer |
| `Maintenance` | Temporarily unavailable |
Rails available per currency: `usd` carries `ach`, `wire`, `rtp`, `fednow`, and `swift`. `eur` carries `sepa` and `swift`. `gbp` carries `fps`, `chaps`, and `swift`. `brl` carries `pix`. Each has a `_thirdparty` variant.
### Tracking Ability Changes
Abilities move without any request from you. An identification gets approved, your customer signs a new document, compliance changes their review outcome, or a banking provider finishes registering a rail: MoonPay Enterprise recomputes the customer's abilities and delivers the new snapshot to the `customer_fiat_abilities` [webhook](/webhooks#example-payloads).
Subscribe to it and treat the deliveries as the source of truth for which rails are open. `GET /api/customers/{id}/abilities` is for the first read and for the `currencies` array, which the webhook does not carry. Polling it on a timer spends rate limit on unchanged responses and still leaves a window where you reject a transaction on a rail that already went `Active`.
Each delivery is a full snapshot of `fiat_deposit` and `fiat_payout`, not a diff, and MoonPay Enterprise does not suppress a snapshot identical to the last one it sent. Diff against your stored copy so a repeat delivery doesn't re-trigger whatever you do when a rail opens.
# Onramp
Source: https://dev.enterprise.moonpay.com/onramp
Create a named virtual fiat account that settles into stablecoins in your wallet.
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**
| Field | Value |
| - | - |
| Account holder | Jane Doe |
| Account number | xxxxxxx |
| Routing number | xxxxxxx |
| Account type | Checking |
| Bank name | Banking Circle |
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
Max sets up a vIBAN with an `autoramp` which turns EUR into USDC on Solana address 3DkN...BV4Lm
Max triggers a EUR 1000 transfer from their banking app
MoonPay Enterprise monitors the vIBAN. As soon as funds arrive, USDC is delivered to 3DkN...BV4Lm
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.
```bash theme={null}
export API_KEY=""
export BASE_URL="https://api.sandbox.iron.xyz"
```
Your customer must have `Active` status: terms signed and [identification](/onboarding) (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. Subscribe to the `customer_fiat_abilities` [webhook](/webhooks) to track rail changes instead of polling. See [Tracking ability changes](/onboarding#tracking-ability-changes).
Expand the walkthrough below if you have not onboarded a customer yet.
Register the destination wallet via [Crypto Addresses](/crypto-addresses) for [Travel Rule](/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.
If you need a locked rate (and lower-bps fee profiles), request a quote via `GET /api/autoramps/quote`. See [Quotes](#lock-a-rate-with-a-quote) below. Otherwise, the onramp executes at the current mid-market rate.
Create the autoramp via `POST /api/autoramps` using the registered wallet address as the recipient. See the implementation example below.
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:
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/autoramp/$AUTORAMP_ID" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Approved"'
```
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](/onboarding) for the full lifecycle.
**Prerequisites:**
* A Sandbox API key from the [Partner Dashboard](https://app.sandbox.iron.xyz/dashboard/keys)
* An Ethereum wallet with signing capability (e.g. [ethers.js](https://docs.ethers.org), [viem](https://viem.sh), or a similar library), required to sign a proof-of-ownership message
`POST /api/customers`
Create 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](/versioning).
```bash theme={null}
curl -X POST "$BASE_URL/api/customers" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-H "X-API-Version: 2026-08-01" \
-d '{
"customer_type": "Person",
"name": "jane_doe",
"email": "jane@example.com"
}'
```
**Example response:**
```json theme={null}
{
"id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"customer_type": "Person",
"status": "SigningsRequired",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
```
Save the `id`. You need it in every subsequent step.
`GET /api/customers/{id}/required-signings`
Fetch 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](/onboarding#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.
```bash Get the terms theme={null}
curl -X GET "$BASE_URL/api/customers//required-signings" \
-H "X-API-Key: $API_KEY"
```
```bash Record the signing theme={null}
curl -X POST "$BASE_URL/api/customers//signings" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"content_id": "",
"signed": true
}'
```
```bash Before a customer exists theme={null}
curl -X GET "$BASE_URL/api/terms-and-conditions?country=DEU" \
-H "X-API-Key: $API_KEY"
```
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`.
`POST /api/customers/{id}/identifications/v2`
Create an identification with `type: "Link"`. MoonPay Enterprise returns a URL where your customer completes KYC (individuals) or KYB (businesses) through the hosted verification interface.
```bash theme={null}
curl -X POST "$BASE_URL/api/customers//identifications/v2" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{"type": "Link"}'
```
**Example response:**
```json theme={null}
{
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "Pending",
"url": "https://api.sandbox.iron.xyz/verify?"
}
```
In production, pass the `url` to your customer to complete verification. Save the identification `id`.
`POST /api/sandbox/identification/{id}`
In Sandbox, you control whether an identification is approved or rejected.
```bash theme={null}
curl -X POST "$BASE_URL/api/sandbox/identification/" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{"approved": true}'
```
In production, the MoonPay Enterprise compliance team handles this review (typically 24-48 hours).
`GET /api/customers/{id}/required-signings`
Check 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`.
```bash Retrieve signings theme={null}
curl -X GET "$BASE_URL/api/customers//required-signings" \
-H "X-API-Key: $API_KEY"
```
```bash Sign a document theme={null}
curl -X POST "$BASE_URL/api/customers//signings" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"content_id": "",
"signed": true
}'
```
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](/onboarding#signing-new-terms-later).
`POST /api/addresses/crypto/selfhosted`
Register the wallet where stablecoins will be delivered. Required for [Travel Rule](/travel-rule) compliance before creating any autoramp.
```bash theme={null}
curl -X POST "$BASE_URL/api/addresses/crypto/selfhosted" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"customer_id": "",
"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"message": "I am verifying ownership of the wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e as customer . This message was signed on 15/01/2026 to confirm my control over this wallet.",
"signature": "",
"blockchain": "Ethereum"
}'
```
The proof message must include today's date in DD/MM/YYYY format (UTC). You can also register a [hosted wallet](/crypto-addresses#register-a-hosted-wallet) if the wallet is custodied by another VASP.
## Implementation
Create an autoramp that converts EUR to USDC on Ethereum.
`POST /api/autoramps` requires an `Idempotency-Key` header. Send the same key to safely retry a request without creating a duplicate autoramp.
### Request
```bash Shell expandable theme={null}
AUTORAMP_ID=$(curl -s -X POST "$BASE_URL/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"source_currencies": [{
"type": "Fiat",
"code": "EUR"
}],
"destination_currency": {
"type": "Crypto",
"token": "USDC",
"blockchain": "Ethereum"
},
"recipient_account": {
"type": "Crypto",
"chain": "Ethereum",
"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
},
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"source_is_third_party": false
}' | jq -r '.id')
echo "$AUTORAMP_ID"
```
Capturing the ID into `$AUTORAMP_ID` lets you paste the remaining commands on this page straight into the same shell.
### Response
```json Shell theme={null}
{
"id": "f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50",
"status": "Authorized",
"deposit_rails": [],
"created_at": "2025-01-20T12:34:56Z"
}
```
`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](/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.
```json theme={null}
{
"id": "d3c2b1a4-e5f6-78a9-0123-fedcba654321",
"status": "Approved",
"deposit_rails": [
{
"type": "SEPA",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"beneficiary_name": "Jane Doe",
"name": "Iron Bank",
"address": "10 Downing Street, London, SW1A 2AA, GB",
"phone": "+44-20-1234-5678"
}
],
"created_at": "2026-01-15T10:05:00Z"
}
```
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:
| Field | Description |
| - | - |
| `iban` | IBAN number. The only required field |
| `bic` | Bank identifier code |
| `beneficiary_name` | Account holder name, matching the customer's verified identity |
| `name` | Name of the issuing bank |
| `address` | Address of the issuing bank |
| `phone` | Contact phone for the issuing bank |
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](/crypto-addresses) returns `400`:
```json 400 Bad Request theme={null}
"Recipient wallet not verified for customer"
```
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](/onboarding#ability-status)).
## 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**.
```bash theme={null}
curl -X GET "$BASE_URL/api/autoramps/quote?customer_id=&source_currency_code=EUR&destination_currency_code=USDC&destination_currency_chain=Ethereum&recipient_account_id=&amount_in=1000&rate_expiry_policy=Return&expiry_in_hours=1&is_third_party=false" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: $API_KEY"
```
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](/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.
`POST /api/sandbox/transaction` builds a transaction directly from the route, reusing its currencies, customer, and recipient.
```bash theme={null}
TRANSACTION_ID=$(curl -s -X POST "$BASE_URL/api/sandbox/transaction" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d "{\"autoramp_id\": \"$AUTORAMP_ID\", \"amount\": \"1000\"}" | jq -r '.id')
```
The response carries `amount_in`, `amount_out`, `currency_in`, `currency_out`, and `state`. See [Sandbox](/sandbox#create-a-sandbox-transaction) for the optional `fee`, `fx_rate`, and `initial_state` fields.
The simulated transaction starts in `Pending`. Advance it to trigger the rest of the status webhooks.
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/transaction/$TRANSACTION_ID/state" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "state": "Completed" }'
```
Your endpoint should have received two `transaction_status` events: `ConversionInProgress` when you created the transaction, then `Completed` when you advanced its state. The sandbox writes the state you ask for straight onto the transaction, so it does not send the `transaction` event or the intermediate `FundsReviewInProgress` and `PayoutInProgress` statuses that a live deposit produces.
Read `transaction_status`, not the deprecated `status` field, for the current state. Payload shapes and the full status table are in [Monitoring payments](/onramp#monitoring-payments); signature verification is in [Webhooks](/webhooks).
## Simulate it in the Dashboard
You can run this same flow without writing a single API call. The [Partner Dashboard](https://app.sandbox.iron.xyz) covers every step, from creating the route to settling a simulated deposit.
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 .
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.
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.
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.
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
| | Sandbox | Production |
| - | - | - |
| API base URL | `https://api.sandbox.iron.xyz` | `https://api.iron.xyz` |
| Identification | You call `POST /api/sandbox/identification/{id}`, or **Approve** in the Dashboard | Compliance review, typically 24-48 hours |
| Autoramp approval | You call `PUT /api/sandbox/autoramp/{id}`, or set **Authorized** in the Dashboard | Automatic once the deposit account is provisioned and verified |
| Deposits | Simulated via `POST /api/sandbox/transaction` or **New Transaction** in the Dashboard | Real fiat arriving on the virtual account |
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](https://app.sandbox.iron.xyz) under **Developer → Webhooks**, or use the [Webhooks API](/webhooks). 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:
| Event | Type | When it fires |
| - | - | - |
| New transaction | `transaction` | Fiat payment is received at the vIBAN |
| Status update | `transaction_status` | Each time the transaction progresses (`FundsReviewInProgress`, `ConversionInProgress`, `PayoutInProgress`, `Completed`) |
| Autoramp status | `register_autoramp_status` | Autoramp state changes (e.g. `Authorized`, `Approved`) |
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:
```json theme={null}
{
"type": "transaction",
"timestamp": "2026-01-15T10:30:00+00:00",
"data": {
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"message": {
"Event": {
"id": "7d834f68-cea8-496a-8eae-bb0772365028",
"kind": "Transaction"
}
}
}
}
```
### 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:
```json theme={null}
{
"type": "transaction_status",
"timestamp": "2026-01-15T10:30:05+00:00",
"data": {
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"message": {
"TransactionStatus": {
"id": "7d834f68-cea8-496a-8eae-bb0772365028",
"status": "Pending",
"transaction_status": "ConversionInProgress",
"transaction_hash": null
}
}
}
}
```
Once the stablecoin is delivered to the wallet, `transaction_status` becomes `Completed` and `transaction_hash` contains the on-chain transaction hash.
### Transaction status reference
| `transaction_status` | Description |
| - | - |
| `FundsReviewInProgress` | Deposit received, undergoing AML/compliance check |
| `ConversionInProgress` | Fiat is being converted to the destination stablecoin |
| `PayoutInProgress` | Stablecoin sent to blockchain, awaiting confirmation |
| `Completed` | Funds delivered to the destination wallet |
| `Failed` | Processing error |
| `RejectedAml` | Rejected by compliance checks |
| `RejectedFraud` | Rejected by fraud review |
| `RejectedMinAmount` | Amount below the minimum threshold |
Your webhook endpoint must return `200 OK` to acknowledge receipt. Non-2xx responses trigger retries with exponential backoff. See [Webhooks](/webhooks) for signature verification and full implementation details.
## What to read next
The reverse route: stablecoins to a bank account
Every status an autoramp moves through, and what unblocks each one
Register self-hosted or hosted wallets for Travel Rule compliance
Signature verification, payload schemas, and retry behaviour
Lock a rate instead of executing at the current mid-market rate
Per-customer and per-transaction thresholds
# Customer operations
Source: https://dev.enterprise.moonpay.com/ops-and-customer-service
Find a blocked customer in Compliance and fix it from the dashboard: resume a stalled onboarding, resolve rejected documents, or guide them through enhanced due diligence.
The dashboard lets your support and operations team see exactly where a customer is blocked and resolve it directly: share a fresh verification link, upload a replacement document, or start enhanced due diligence.
Everything below is also available programmatically. See [API parity](/api-parity) for the endpoint behind each action.
## Where to work: the Compliance page
[Compliance](/partner-dashboard#compliance) is the queue: its tabs sort customers by problem type, and clicking a row opens the **Compliance Review** panel where the work happens.
The panel carries the whole picture for one customer:
| Panel section | What you get |
| - | - |
| **Help your customer** | On the **Incomplete** and **Change Requested** tabs: one generic ready-to-send message with a copy button. It does not name the missing item, so check **Requirements** for that. On the **EDD** tab: two messages, one for proof of address and one for source of wealth, each with its own copy button. |
| **Requirements** | Every outstanding item, grouped, with the reject reason on the ones that failed and whether the item is retryable. |
| **Compliance questionnaire** | What the customer submitted: occupation, employment status, source of funds, monthly income, expected monthly volume, intended use. Absent until they submit it. |
| **Identification Details** | Status, residence country, an **Enhanced due diligence** row, and the customer and identification IDs. |
Sections only render when there is something to show, so a customer who has not submitted a questionnaire has no questionnaire card.
The action buttons at the bottom of the panel follow the customer, not the tab, which is why the workflow barely changes between cases. A few appear only when they apply: **Trigger EDD** is hidden for a customer already in EDD, and **Extend KYC** shows only for an expired identification. See [Resume a stalled onboarding](/finish-onboarding), [Resolve rejected documents](/rejected-documents), and [Customer in EDD](/enhanced-due-diligence) for each one.
# Introduction
Source: https://dev.enterprise.moonpay.com/overview
MoonPay Enterprise developer portal: stablecoin APIs for onramp, offramp, swap, virtual accounts, issuance, and embedded wallets. A MoonPay company.
## Welcome to MoonPay Enterprise
Welcome to the MoonPay Enterprise developer portal! This is your go-to resource for learning about our products and integrating them into your offerings. This documentation is specifically designed for developers building onchain experiences using the MoonPay Enterprise APIs.
## What is MoonPay Enterprise?
MoonPay Enterprise is a set of Stablecoin APIs serving as the foundational infrastructure for on-chain banking, payments, money transfers, and treasury management. We enable fiat-to-stablecoin and stablecoin-to-fiat integration for businesses.
MoonPay Enterprise is a [MoonPay](https://www.moonpay.com/) Company.
See [our capabilities](/rails-and-capabilities) for everything we can do together.
## Get Started
### 1. Contact MoonPay Enterprise Team
To begin using the MoonPay Enterprise Partner Dashboard:
* **Request Access:** Reach out to the MoonPay Enterprise team at [developers@iron.xyz](mailto:developers@iron.xyz) to request access to your partner dashboard. You'll receive login credentials or an invitation link.
* **Sign In:** Go to [https://app.enterprise.moonpay.com/](https://app.enterprise.moonpay.com/) or [https://app.sandbox.iron.xyz/](https://app.sandbox.iron.xyz/) and enter your credentials and complete the authentication process.
* **Set Up API Key:** Log in to the partner dashboard and set up your first API key. See [Get API Access](/api-access) for more information.
### 2. Slack/TG Channel
We will set up a joint Slack/TG channel to coordinate integration work and answer any questions you may have.
## Support
If you have any questions or encounter any issues:
* **Email us:** [developers@iron.xyz](mailto:developers@iron.xyz)
* **Slack Channel:** Once we’re working together, we can set up a shared Slack channel for direct communication during integration.
# Dashboard Overview
Source: https://dev.enterprise.moonpay.com/partner-dashboard
Web UI for managing partners, customers, API keys, webhooks, routes, and compliance workflows at app.enterprise.moonpay.com.
## Overview
The MoonPay Enterprise Partner Dashboard is where you manage customers, compliance, fee profiles, API keys, webhooks, routes, and linked wallets and bank accounts. This guide explains how to access the dashboard and what each screen does.
* **Production:** [https://app.enterprise.moonpay.com/](https://app.enterprise.moonpay.com/)
* **Sandbox:** [https://app.sandbox.iron.xyz/](https://app.sandbox.iron.xyz/)
The dashboard is not only for your engineers. It is built so your support and operations team can unblock a customer's onboarding themselves, without filing a ticket. If that is your job, start at [Ops and Customer Service](/ops-and-customer-service). Every action there has an API equivalent.
## Accessing the dashboard
* Reach out to the MoonPay Enterprise team at [developers@iron.xyz](mailto:developers@iron.xyz) to request access to your partner dashboard.
* You'll receive login credentials or an invitation link.
* Go to [https://app.enterprise.moonpay.com/](https://app.enterprise.moonpay.com/) or [https://app.sandbox.iron.xyz/](https://app.sandbox.iron.xyz/).
* Enter your credentials and complete the authentication process.
## Two views: Dashboard and App
The sidebar switches between two views. The switcher sits at the top of the sidebar and shows the view you're in. Open it to change view.
| View | What it covers |
| - | - |
| **Dashboard** | Your partner account: Home, Customers, Compliance, Fee Profiles, the Developer menu (API Keys, Webhooks, Sandbox), Onboarding, and Team. |
| **App** | Transacting as a customer: Routes, Quotes, Transactions, Linked Wallets, and Linked Bank Accounts. |
Every navigation path in this guide starts with the view it lives in. A screen in the App view is not reachable from the Dashboard sidebar, and the reverse is also true.
The **View As** selector directly below the switcher sets which customer the App view acts as. Picking a customer there is how you create a route or request a quote on their behalf.
## Dashboard view
### Home
The landing screen after sign-in. It carries an overview of your account plus your most recent activity.
* **Read monthly volume** on the Overview chart, and step between years
* **Check the General stats** for total volume, completed transactions, average transaction size, and unique active customers, each against the previous month
* **Open the Payout Summary tab** for the same month's payout breakdown, downloadable as a PDF
* **Scan recent transactions** with status, type, send and receive amounts, fee, recipient, and timestamp
* **Export transactions to CSV** from the same table
### Customers
View and manage customer information.
* **View customer profiles** with real-time status, KYC or KYB progress and account details
* **Monitor onboarding milestones** including terms signing, KYC review and activation
* **Generate and share signup links** to onboard new customers via the hosted UI
* **Review banking capabilities** for deposits and payouts by currency and rail
* **Inspect transaction summaries** and currencies used
* **View microdeposits** in the customer detail sheet, with amount, currency, counterparty and date. See [Micro-Deposits (Penny Tests)](/limits-and-minimum#micro-deposits-penny-tests) for API access
* **Upload and manage required compliance documents** such as source of wealth (individuals) or source of funds (businesses)
For the API equivalent, see [Onboarding](/onboarding).
### Compliance
A work queue of customers who need attention, split into tabs by what is blocking them. Open a row to get the detail sheet with that customer's requirements, questionnaire, and status timeline.
| Tab | What it means |
| - | - |
| **Incomplete** | The customer started but has not submitted everything. Nothing to review yet. Nudge them to finish. |
| **Change Requested** | MoonPay Enterprise reviewed and rejected one or more items. Share the reason so your customer can resubmit. |
| **EDD** | The customer needs enhanced checks before approval. Highest priority: nudge them and guide them through it. |
| **Pending review** | Everything is submitted and with the compliance team. No action needed from you. |
| **Suspended** | The customer is blocked and cannot transact. |
| **RFIs** | MoonPay Enterprise asked this customer for more information. Respond here or over the API. See [RFIs](/rfi). |
Each tab shows the same guidance in the page itself, so the dashboard and this table stay in step. Filter by customer type, or search by name, email, ID, or external ID.
Clicking a row opens the **Compliance Review** panel, which is where a support team actually unblocks a customer. See [Ops and Customer Service](/ops-and-customer-service) for the walkthroughs.
You get a webhook when a customer's status changes, so you don't have to poll this page. See [Webhooks](/webhooks).
### Fee Profiles
Fee profiles set what you charge on top of a transaction, and what banking fees apply per rail.
* **Create a fee profile** with a partner fee in basis points
* **Add banking fees** per rail and direction, for example SEPA PayIn and PayOut
* **Review a profile's breakdown** before you attach it
* **Attach a profile to a route** when you create it, or by editing an existing route
Changing a route's fee profile is a sensitive edit. It parks in `EditPending` and needs email confirmation before it takes effect, rather than saving instantly.
### Developer
Under **Developer** in the Dashboard sidebar.
#### API Keys
* **Create a new API key** with a label and a permission level of Read Only or Read / Write
* **Reveal or copy** a key's value from its row
* **Revoke a key** to immediately end its access. The row then shows `Revoked`.
In production, only Admins can manage API keys, and the API Keys item is hidden from the sidebar for everyone else. An Admin can create a key and give it to a teammate with the Editor role. On the sandbox, Editors can manage keys too. See [Team](#team).
For how keys authenticate requests, see [Authentication](/authentication).
#### Webhooks
Register and manage the endpoints MoonPay Enterprise calls when events happen on your account. The page opens on **Webhook Logs**; registered endpoints live on the **Manage Webhooks** tab.
* **Register a webhook** with a label, an `https://` endpoint URL, and the events you want to receive
* **Copy the webhook secret** shown once after creation, used to verify signatures
* **Enable or disable** deliveries for an endpoint without deleting it
* **Send a test ping** to confirm your endpoint receives and acknowledges events
* **Review every delivery attempt** in the Webhook Logs tab, with the signed headers, the exact payload, and a retry for the ones that failed
* **Read the Integration Guide tab** for signature verification steps and sample code in PHP, JavaScript, Ruby, and Python
* **Delete an endpoint**, which also removes its delivery logs
Your endpoint must be publicly reachable over HTTPS and return a 2xx within 5 seconds. Slower responses count as failed deliveries.
For payload schemas, signature verification, and sample code, see the [Webhooks](/webhooks) reference.
#### Sandbox
On production, this screen provisions your sandbox account and hands you the link to it. It reports whether your sandbox account exists, whether it has a user, and gives you the invitation link once it's ready.
The testing tools themselves live on the sandbox app at [https://app.sandbox.iron.xyz/](https://app.sandbox.iron.xyz/): create customers, walk them through onboarding, and simulate transactions without moving real money.
### Onboarding
Your own onboarding as a partner: start KYB and sign terms. This is your partner record, not your customers'. Customer onboarding lives under [Customers](#customers).
### Team
Invite teammates to your organization and control what they can do. Find this under **Others > Team** in the Dashboard view.
* **Invite a member** by email, either emailing the invitation or sharing the invite link and code yourself
* **Assign a role** that sets their access
* **Change a member's role** at any time
* **Remove a member** to revoke their access
| Role | Access |
| - | - |
| **Viewer** | Can only see data in the dashboard and app: customers, transactions, routes, bank accounts and wallets. |
| **Editor** | Can view and edit customers, transactions, routes, bank accounts and wallets. |
| **Admin** | Everything above, plus inviting team members, editing their roles, removing them, and managing API keys. |
## App view
Set the customer you're acting as in the **View As** selector before you use these screens.
### Routes
One page for every standing route, whatever its direction. Routes are persistent, always on, and accept any amount.
Three cards at the top create the three kinds of route:
| Card | What it creates |
| - | - |
| **Onramp** | A named virtual fiat account that settles into stablecoins in your wallet. |
| **Offramp** | A liquidation address that accepts stablecoins and settles in fiat into a bank account. |
| **Stable to Stable** | A swap channel. Send one stablecoin, receive another, across chains. |
* **View all routes** in **My Routes** with name, status, type, from and to currencies, and deposit address
* **Filter by type** to narrow the table to on-ramps, off-ramps, or swaps
* **Edit a route** to change its recipient, reference, or fee profile
* **Attach a fee profile** under Advanced options when you create or edit a route
For the API equivalent, see [Autoramps](/autoramp).
### Quotes
Price a conversion before you commit to it. The composer returns a signed quote with a rate, an expiry countdown, and a fee breakdown, which you then confirm.
* **Request a quote** for an amount, in either direction
* **Review the rate, fees, and expiry** before confirming
* **Track active and expired quotes** in their own tables
### Transactions
* **View detailed transaction information** including amounts, currencies, chains, and timestamps
* **Track transaction lifecycle status** from fund detection through exchange and completion
* **Inspect fee breakdowns** by component and settlement method
* **Review source and destination details** including wallets, bank accounts, and rails
* **Access recipient information** such as holder name, institution, network, and address
For the API equivalent, see [Transaction status](/transaction-status).
### Linked Wallets and Linked Bank Accounts
Under **My Address** in the App view. These are the crypto and fiat addresses your routes send to and receive from.
* **Link a crypto wallet** for use as a transaction source or destination
* **Link a bank account** by rail, for example wire or SEPA
* **Attach ownership and compliance information** to each address
* **Review an address's status**
* **Disable or remove addresses** that are no longer required
* **Support Travel Rule compliance** by associating required beneficiary and originator data
For the API equivalent, see [Crypto addresses](/crypto-addresses) and [Fiat addresses](/fiat-addresses).
# Proof of Address
Source: https://dev.enterprise.moonpay.com/proof-of-address
When proof of address (POA) is required, accepted documents, and criteria.
## When it's required
At Tier 1, the region the customer resides in decides whether they are asked for a document.
| Region | When it's required |
| - | - |
| USA | Only when the ID document is a passport or a residence permit, was not issued by the US, its state does not match the address the customer gave, or the document or device IP country is a high-risk or prohibited jurisdiction. A US driver's license or state ID, from a US IP, with a matching state, needs nothing. |
| EEA | Only when the ID document country is outside the EU/EEA, or the device IP country is a high-risk or prohibited jurisdiction. |
| UK | Only when the ID document country differs from the residence country, a residence permit lacks or mismatches address data, or the IP country is a high-risk jurisdiction. |
| Rest of world | Always, including Australia. Not required in Brazil, Mexico and Colombia when public data confirms the address from the customer's national number: CPF (Brazil), CURP or RFC (Mexico), Cédula (Colombia). Requested only when no match is found. |
Proof of address is always required at Tier 2, for high-risk customers, and as part of the Enhanced Due Diligence set when a customer hits a spending limit. See [Limits and minimums](/limits-and-minimum) for the EDD triggers and the 24-hour window to submit documents.
## Accepted documents
Identity documents are accepted as proof of address: passport, national ID card, residence permit, or driver's license. The 3-month rule does not apply to them.
All other documents must be issued within the **last 3 months**:
* Utility bill (electricity, gas, internet, landline, water)
* Bank or credit card statement
* Tax invoice
* Mortgage statement
* Certificate of voter registration
* Signed, current lease agreement
* Employer's certificate (payslips not accepted)
* Other government-issued residential document
## Criteria
* Shows the customer's full name and complete residential address (no PO boxes)
* Shows the issuing entity's name and issue date
* Entire document visible, as an original photo or PDF
Not accepted: screenshots, spreadsheets, medical bills, insurance statements. A payslip is not accepted for POA, but it is accepted for [source of wealth](/source-of-wealth).
# Quotes
Source: https://dev.enterprise.moonpay.com/quotes
Lock an exchange rate before creating an Autoramp, and attach additional quotes to keep an Autoramp running at fixed rates.
Request a signed quote to lock a conversion rate for a defined window. An Autoramp can hold multiple active quotes at once, so partners can keep a single ramp running over time without re-creating it.
## How It Works
A quote binds a conversion rate between two currencies for a limited window. You request a quote with `GET /api/autoramps/quote`, then pass the signed result to `POST /api/autoramps` to create an Autoramp that executes at the locked rate.
Once the Autoramp exists, you can keep attaching new quotes to it via `POST /api/autoramps/{autoramp_id}/quotes`. Each attached quote stays active until its own `valid_until` timestamp passes.
When a deposit arrives, MoonPay Enterprise picks the **newest active quote** whose `amount_in` value and source currency match the deposit. Deposits that don't match any quote, or only match an expired quote, are returned to sender. When a deposit matches an active quote whose rate-lock has expired (but the quote itself is still within `valid_until`), behavior depends on the quote's `rate_expiry_policy` (see [Expiry policies](#expiry-policies)).
Without a quote, conversions execute at the current mid-market rate. With one or more active quotes, the deposit executes at the locked rate of the matching quote.
### Key parameters
**Rate lock duration**: time window where the quoted rate and fee are fully guaranteed. Deposits that match within this window execute at the quoted price.
**Validity period**: total time the quote remains usable. Deposits arriving after `valid_until` will not match this quote.
### Rate lock duration caps
The `rate_lock_duration_minutes` parameter is optional. If omitted, the quote uses the maximum allowed for the pair. Larger values are silently clamped to the cap.
| Currency pair | Max rate lock |
| - | - |
| Pegged pair (EUR ↔ EUR-stablecoin, USD ↔ USD-stablecoin) | 2 days |
| Other fiat ↔ stablecoin (e.g. EUR ↔ USDC) | 10 minutes |
| Stablecoin ↔ stablecoin (e.g. USDC ↔ USDT) | 10 minutes |
| Any other pair (e.g. EUR ↔ ETH, BTC ↔ USDC) | 1 minute |
### Expiry policies
What happens when funds arrive after the rate lock expires depends on the `rate_expiry_policy` you set.
Validity period equals rate lock duration. Funds arriving after the lock expires are refunded.
**Example:**
* Quote: 100 EUR for 108 USDC
* Rate lock: 10 minutes, validity: 10 minutes
* Funds arrive at 12 minutes
* **Result**: refunded (both windows expired)
Validity period extends beyond the rate lock. Funds arriving after the lock execute if the market rate is within `slippage_tolerance_in_bips`. Otherwise they are refunded or requoted.
**Example (within tolerance):**
* Quote: 100 EUR for 108 USDC, 5 bips slippage
* Rate lock: 10 minutes, validity: 1 hour
* Funds arrive at 20 minutes, market rate within 5 bips
* **Result**: executes at market rate
**Example (outside tolerance):**
* Same setup, but market rate exceeds 5 bips from quote
* **Result**: refunded or requoted
## Prerequisites
Complete these before requesting a quote:
Register a customer via `POST /customers` and complete identity verification. The customer must reach `Active` status before they can transact.
See [Onboarding Lifecycle](/onboarding) for the full flow.
The `recipient_account_id` in your quote request must reference a registered and verified address.
* **Fiat offramps**: register a bank account (SEPA, ACH, Wire, or SWIFT) via `POST /addresses/fiat`. The address must be verified before use. See [Fiat Addresses](/fiat-addresses).
* **Crypto swaps**: register the destination wallet via `POST /addresses/crypto`. Self-hosted wallets require a signed proof-of-ownership message; hosted wallets require the custodian's DID. See [Crypto Addresses](/crypto-addresses).
Include your API key in the `X-API-Key` header on every request. See [Authentication](/authentication).
Quote requests fail if the `recipient_account_id` references an address that has not been registered or has not passed verification.
## Create an Autoramp with a quote
`GET /api/autoramps/quote`
Request a signed quote that defines the conversion rate, lock duration, validity period, and fees. The quote must be passed unchanged when creating an Autoramp.
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/autoramps/quote?customer_id=&source_currency_code=USDC&source_currency_chain=Ethereum&destination_currency_code=EUR&recipient_account_id=&amount_out=100&rate_lock_duration_minutes=10&rate_expiry_policy=Return&expiry_in_hours=1&is_third_party=false" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: "
```
**Example response:**
```json theme={null}
{
"amount_in": {
"amount": "111.19863",
"currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" }
},
"amount_out": {
"amount": "100",
"currency": { "code": "EUR", "type": "Fiat" }
},
"customer_id": "",
"deposit_account_type": "Iban",
"destination_currency": { "code": "EUR", "type": "Fiat" },
"fee": {
"banking_fee": { "amount": "0.326434", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"iron_fee": { "amount": "0.326434", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"network_fee": { "amount": "1.734570", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"partner_fee": { "amount": "0", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } },
"total_fee": { "amount": "2.387438", "currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" } }
},
"fee_settlement": "deductedImmediately",
"is_third_party": false,
"quote_id": "e8cd73a4-54cc-4a34-916d-238c5ce5f898",
"rate": "0.919023105",
"rate_expiry_policy": "Return",
"rate_lock_valid_until": "2025-03-14T11:37:24.250798+00:00",
"recipient_account": {
"type": "Fiat",
"account_identifier": { "type": "SEPA", "iban": "DE89370400440532013000" }
},
"signature": "36665918454471c078724046bcb82698eac2fb6f5f91c58a0f1a3923c5d95a28",
"slippage_tolerance_in_bips": null,
"source_currency": { "blockchain": "Ethereum", "token": "USDC", "type": "Crypto" },
"valid_until": "2025-03-14T11:37:24.250798+00:00",
"additional_in_currencies": null,
"pay_by_reference": null
}
```
`POST /api/autoramps`
Submit the signed quote payload to create an Autoramp. The Autoramp matches inbound deposits against the active quotes attached to it.
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "IDEMPOTENCY-KEY: " \
-H "X-API-Key: " \
-d ''
```
**Example response:**
```json theme={null}
{
"id": "f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50",
"status": "Created",
"deposit_rails": [
{
"type": "Crypto",
"chain": "Ethereum",
"address": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
],
"created_at": "2025-01-20T12:34:56Z",
"source": "Quote",
"quotes": [ { "quote_id": "e8cd73a4-54cc-4a34-916d-238c5ce5f898", "...": "..." } ]
}
```
The `quotes` array lists every currently-active quote on the Autoramp. The legacy `quote` field is deprecated; read `quotes` instead.
The quote is digitally signed. Pass it verbatim into the `POST /api/autoramps` call. Any modification (changing values, omitting fields) causes the request to fail.
## Attach a new quote to an existing Autoramp
After the Autoramp exists, you can attach further quotes to keep it executing at locked rates over time. This works only on quote-source Autoramps (`source = "Quote"`).
Use the right quote object. This endpoint takes the `SignedAutorampRateQuote` returned by `GET /api/autoramps/{autoramp_id}/quote`, not the `SignedAutorampQuote` returned by `GET /api/autoramps/quote`. The creation quote has no `autoramp_id` field, so submitting it here fails with `Expected input type "string_uuid", found null.`
`GET /api/autoramps/{autoramp_id}/quote`
Returns a `SignedAutorampRateQuote`. The Autoramp's recipient, fee profile, and destination currency are reused automatically, so this endpoint has a smaller parameter surface than the initial quote endpoint.
The `source_currency_code` must be one of the Autoramp's allowed input currencies (the original `source_currency` plus any entries you registered in `additional_in_currencies` at creation time).
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/autoramps//quote?source_currency_code=USDC&source_currency_chain=Ethereum&amount_out=100&rate_lock_duration_minutes=10&rate_expiry_policy=Return&expiry_in_hours=1" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: "
```
`POST /api/autoramps/{autoramp_id}/quotes`
Submit the signed rate quote returned in the previous step. Requires an `IDEMPOTENCY-KEY` header. Returns `201` with the updated Autoramp object (its `quotes` array now includes the newly attached quote).
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/autoramps//quotes" \
-H "Content-Type: application/json; charset=utf-8" \
-H "IDEMPOTENCY-KEY: " \
-H "X-API-Key: " \
-d ''
```
Attaching a new quote that shares `amount_in` value and source currency with an existing active quote supersedes the older one for incoming deposits. The older quote stays in `quotes` until its own `valid_until` passes; nothing is deleted retroactively.
**Staged setup pattern.** You can create the Autoramp with any initial quote to surface the deposit wallet (so the payer can whitelist it or finish their onboarding), then attach the production quote later via `POST /api/autoramps/{autoramp_id}/quotes` once the real amount is known. The initial quote simply expires unused if no matching deposit arrives.
## Multi-currency input
By default, an Autoramp only accepts deposits in its original `source_currency`. To let it accept additional input currencies (and request quotes for them later), pass `additional_in_currency_codes` and `additional_in_currency_chains` when you call `GET /api/autoramps/quote`.
The two parameters are index-aligned arrays of up to 16 entries each. The chain at position `i` describes the code at position `i`. The list must not include the `source_currency` itself and must not contain duplicates.
After Autoramp creation, request a quote for any of the additional currencies via `GET /api/autoramps/{autoramp_id}/quote` and attach it via `POST /api/autoramps/{autoramp_id}/quotes`.
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/autoramps/quote?customer_id=&source_currency_code=USDC&source_currency_chain=Ethereum&destination_currency_code=EUR&recipient_account_id=&amount_out=100&rate_expiry_policy=Return&expiry_in_hours=1&is_third_party=false&additional_in_currency_codes=USDT,USDC&additional_in_currency_chains=Ethereum,Polygon" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: "
```
### Query parameters: `GET /api/autoramps/quote`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `customer_id` | string (UUID) | Yes | Unique identifier for the customer. |
| `source_currency_code` | string | Yes | Currency or token being sent (e.g. `USDC`, `USD`). |
| `source_currency_chain` | string | Conditional | Blockchain for the source currency. Required for crypto source currencies. |
| `destination_currency_code` | string | Yes | Currency or token being received (e.g. `EUR`, `GBP`). |
| `destination_currency_chain` | string | Conditional | Blockchain for the destination currency. Required for crypto destination currencies. |
| `recipient_account_id` | string (UUID) | Yes | UUID of a registered recipient account (`fiat_address` or `verified_crypto_address`). |
| `recipient_account` | string | No | **Deprecated.** Use `recipient_account_id` instead. Ignored when `recipient_account_id` is provided. |
| `amount_out` | string | Conditional | Desired output amount in the destination currency. Set either `amount_out` or `amount_in`, not both. |
| `amount_in` | string | Conditional | Desired input amount in the source currency. Set either `amount_in` or `amount_out`, not both. |
| `rate_lock_duration_minutes` | integer | No | Duration the rate is locked. Defaults to the pair's maximum; values above the cap are clamped. See the rate-lock cap table above. |
| `rate_expiry_policy` | string | Yes | What happens when the rate lock expires. One of `Return`, `Slippage`. |
| `slippage_tolerance_in_bips` | string | Conditional | Allowable deviation from quoted rate (1 bip = 0.01%). Required when `rate_expiry_policy` is `Slippage`. |
| `is_third_party` | boolean | Yes | Whether this is a third-party payment Autoramp. Set to `false` for standard customer-owned accounts. |
| `expiry_in_hours` | integer | Yes | Duration the quote is valid. Max 24. Capped at `rate_lock_duration_minutes` when `rate_expiry_policy` is `Return`. |
| `additional_in_currency_codes` | string array | No | Additional input currencies to allow on the Autoramp beyond `source_currency`. Up to 16, must not duplicate or include `source_currency`. Index-aligned with `additional_in_currency_chains`. Pass as a single comma-separated value (e.g. `USDT,USDC`), not repeated query keys. |
| `additional_in_currency_chains` | string array | No | Chains for each entry in `additional_in_currency_codes`. Same length, index-aligned. Pass as a single comma-separated value (e.g. `Ethereum,Polygon`), not repeated query keys. |
| `pay_by_reference` | boolean | No | If `true`, the Autoramp uses pay-by-reference. See [Autoramp → pay by reference](/autoramp#api-optional-pay-by-reference) for eligibility rules and the `inbound_payment_reference` response field. Defaults to `false`. |
### Query parameters: `GET /api/autoramps/{autoramp_id}/quote`
The Autoramp's recipient, destination currency, and fee profile are reused, so only pricing-related parameters are needed.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `source_currency_code` | string | Yes | Currency or token being sent. Must be one of the Autoramp's allowed input currencies. |
| `source_currency_chain` | string | Conditional | Blockchain for the source currency. Required for crypto source currencies. |
| `amount_out` | string | Conditional | Desired output amount. Set either `amount_out` or `amount_in`, not both. |
| `amount_in` | string | Conditional | Desired input amount. Set either `amount_in` or `amount_out`, not both. |
| `rate_lock_duration_minutes` | integer | No | Optional. Defaults and clamps as on the initial endpoint. |
| `rate_expiry_policy` | string | Yes | One of `Return`, `Slippage`. |
| `slippage_tolerance_in_bips` | string | Conditional | Required when `rate_expiry_policy` is `Slippage`. |
| `expiry_in_hours` | integer | Yes | Max 24. |
| `payment_reference` | string | No | Payout reference applied to transactions matched to this quote. Overrides the Autoramp's payment reference. |
This endpoint returns `400 Bad Request` if called on a standalone (non-quote-source) Autoramp, and `400` if `source_currency_code` is not one of the Autoramp's allowed input currencies.
* Check `valid_until` before submitting. Expired quotes return `400 Bad Request`.
* Use idempotency keys for `POST /api/autoramps` and `POST /api/autoramps/{id}/quotes`.
* Sandbox rates are simulated but signatures work identically to Production.
## Error Handling
| Status | Cause |
| - | - |
| `400 Bad Request` | Quote expired (`valid_until` passed), signed payload modified, source currency not allowed on the Autoramp, `recipient_account_id` does not reference a registered address, or `GET /api/autoramps/{id}/quote` called on a standalone Autoramp. Also returned by `POST /api/autoramps/{autoramp_id}/quotes` when the request body fails to parse as a `SignedAutorampRateQuote`, most commonly because the `SignedAutorampQuote` from `GET /api/autoramps/quote` (no `autoramp_id` field) was submitted instead; fails with `Expected input type "string_uuid", found null.` |
| `401 Unauthorized` | Missing or invalid API key. |
| `404 Not Found` | `autoramp_id` not found. |
| `409 Conflict` | Quote already attached (only on `POST /api/autoramps/{autoramp_id}/quotes`). |
| `422 Unprocessable Entity` | Required parameter missing or invalid value. |
Create and manage Autoramps.
Interpret Autoramp statuses.
# Rails & Capabilities
Source: https://dev.enterprise.moonpay.com/rails-and-capabilities
Virtual Accounts. Stablecoin settlement. Collections, payouts, wallets, swaps, and more.
MoonPay Enterprise connects local payment rails to stablecoins through named Virtual Accounts, with collections, payouts, wallets, and swaps.
## Virtual Accounts
Create named accounts for each customer with third-party payments enabled. Incoming fiat converts automatically to stablecoins; stablecoins convert to fiat and settle to a bank account. Customers can access local payment rails without local incorporation.
### Rails and coverage
Coverage snapshot: September 2026. October 2026 launches are planned.
| Currency | Rails | Settlement | Status |
| - | - | - | - |
| 🇺🇸 USD | ACH, Wire, FedNow, SWIFT | T+0 / T+1 (SWIFT) | Live |
| 🇪🇺 EUR | SEPA Instant, SEPA Credit | Real-time / T+1 (SEPA Credit) | Live |
| 🇬🇧 GBP | Faster Payments, CHAPS | Real-time | Live |
| 🇧🇷 BRL | PIX | Real-time | Live |
| 🇲🇽 MXN | SPEI | Real-time | Planned: Oct 2026 |
| 🇦🇷 ARS | Local bank transfer | Real-time | Planned: Oct 2026 |
| 🇨🇴 COP | Local bank transfer | Real-time | Planned: Oct 2026 |
See [currencies and rails](/currencies-and-rails) for rail-specific processing windows and [country coverage](/country) for customer eligibility.
### Multi-bank redundancy
Lead, Erebor, LHV, and Banking Circle. If one bank cannot support a customer, payments route through another without a second integration.
### Global regulatory coverage
* 47 US state MTLs
* NYDFS BitLicense
* MiCA CASP
* Dutch EMI
* FCA
* Central Bank of Ireland
* AUSTRAC
* FINTRAC
See [MoonPay licenses](https://www.moonpay.com/legal/licenses) for details.
### Three onboarding models
* [Hosted KYC/B](/hosted): MoonPay Enterprise runs onboarding.
* [Shared KYC/B](/sharing): You submit the data.
* [KYC/B reliance](/reliance-kyc-token-sharing): Rely on your own programme.
### Customer-specific limits
Limits reflect declared income and wealth and can be reviewed before a payment is blocked. No platform-level cap applies to businesses. See [limits and minimums](/limits-and-minimum).
### API-first platform
Embed onboarding, documents, and banking rails in your product through one API. The platform supports white-label integrations so customers never leave your product.
## Other services
Available through the same MoonPay Enterprise relationship.
Wallets use a separate SDK and credentials.
Convert stablecoins to fiat and pay any registered bank account. Set an exact amount or enable continuous sweeps.
Launch with MoonPay as US issuer of record, or issue offshore using PYUSDx. Retain brand, economics, and rewards. \$100 million+ issued.
Embed self-custodial wallets across Ethereum, EVM networks, Solana, and Tron. No seed phrases or custody infrastructure.
Move between stablecoins and across chains at a locked or prevailing market rate.
Track each transaction through the API and signed webhooks, from funds review and conversion through payout, completion, or rejection.
Set transaction, banking, and partner fees by customer, currency pair, or payment rail. Pass fees on, absorb them, or settle through monthly invoicing.
Versioned APIs, idempotent requests, signed webhooks with automatic retries, delivery logs, and a full Sandbox.
Pay out per transaction, or consolidate high volumes into one daily transfer with the banking fee charged once per batch. Set per route.
## Stablecoins and blockchains
**11 chains. 8 stablecoins, plus 3 MoonPay-issued.** Ethereum, Solana, Base, Arbitrum, Polygon, Tron, Avalanche, Stellar, and more. USDC, USDT, EURC, PYUSD, and others.
See [stablecoins and blockchains](/stablecoins-and-blockchains) for the full list of supported assets and networks.
## Get started
Set up a named account that converts incoming fiat into stablecoins.
Create your API keys and start integrating in the Sandbox.
# Delete a fiat address
Source: https://dev.enterprise.moonpay.com/reference/addresses/delete-a-fiat-address
https://api.sandbox.iron.xyz/spec/2026-10-15 delete /addresses/fiat/{customer_id}/{fiat_address_id}
Soft-delete a fiat address for a customer
# Delete a PIX destination.
Source: https://dev.enterprise.moonpay.com/reference/addresses/delete-a-pix-destination
https://api.sandbox.iron.xyz/spec/2026-10-15 delete /addresses/pix/{customer_id}/{pix_address_id}
Soft-deletes a PIX destination for a customer.
# Get a customer's PIX destinations.
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-a-customers-pix-destinations
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/pix/{customer_id}
# Get a specific fiat address
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-a-specific-fiat-address
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/fiat/{customer_id}/{fiat_address_id}
Get a specific fiat address by customer ID and address ID
# Get a specific PIX destination by ID.
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-a-specific-pix-destination-by-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/pix/{customer_id}/{pix_address_id}
# Get all fiat addresses / bank accounts
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-all-fiat-addresses-bank-accounts
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/fiat/{customer_id}
Get all fiat addresses registered for a customer
Bank account structure can vary per country. Therefore, the `bank_account_identifier` is always the identifier
that matches how a user would identify it. For SEPA it would be the IBAN.
You can search by providing a `search_term` parameter. This will search the following fields:
- `bank_account_identifier`
- `label`
- `bank_name`
# Get all fiat addresses with status
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-all-fiat-addresses-with-status
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/fiat
Get all fiat addresses with a specific status for the current partner
# Get all registered wallet addresses
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-all-registered-wallet-addresses
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/crypto/{customer_id}
Returns every wallet address a customer has registered (linked), including both hosted and self-hosted wallets. Use the `filter` query parameter to narrow by type.
# Get country subdivisions
Source: https://dev.enterprise.moonpay.com/reference/addresses/get-country-subdivisions
https://api.sandbox.iron.xyz/spec/2026-10-15 get /country_subdivisions/{country_code}
Utility endpoint to get a list of all accepted subdivisions (states, provinces, regions, etc.) for a given country
# Look up a PIX key.
Source: https://dev.enterprise.moonpay.com/reference/addresses/look-up-a-pix-key
https://api.sandbox.iron.xyz/spec/2026-10-15 get /pix-key-info
Utility endpoint to resolve the holder/bank info a PIX key points to,
without registering anything. Useful to preview a destination before
`POST /addresses/pix`.
# Register a fiat bank account for a customer.
Source: https://dev.enterprise.moonpay.com/reference/addresses/register-a-fiat-bank-account-for-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/fiat
Register a bank account address for a customer.
# Register a hosted wallet address
Source: https://dev.enterprise.moonpay.com/reference/addresses/register-a-hosted-wallet-address
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/crypto/hosted
Register a wallet that is custodied by another VASP (e.g. Coinbase, Kraken) on behalf of the customer. This is required by the **Travel Rule** before the wallet can be used in **any** flow — onramp, offramp, or swap.
Provide the VASP's DID (Decentralized Identifier) so Iron can exchange travel-rule data with the custodian. Use the [Search VASPs](#tag/Addresses/operation/searchHostedVasps) endpoint to look up the correct DID.
# Register a PIX destination for a customer.
Source: https://dev.enterprise.moonpay.com/reference/addresses/register-a-pix-destination-for-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/pix
Registers a PIX payout destination (Brazil / BRL). Check the returned
`status` to determine whether the destination can be used for payouts.
# Register a self-attested wallet address
Source: https://dev.enterprise.moonpay.com/reference/addresses/register-a-self-attested-wallet-address
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/crypto/attested
Register a self-hosted wallet that the customer **declares** they control, without
providing a cryptographic signature. Intended for direct customers who cannot sign a
proof-of-ownership message from their custody UI (e.g. Fireblocks).
Like the signed self-hosted flow, the wallet must be registered before it can be used
in any flow — onramp, offramp, or swap — under the **Travel Rule**. This endpoint is
currently limited to customers in the **US** and **Rest of World** jurisdictions.
# Register a self-hosted wallet address
Source: https://dev.enterprise.moonpay.com/reference/addresses/register-a-self-hosted-wallet-address
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/crypto/selfhosted
Register a wallet that the customer controls directly (i.e. they hold the private key). This is required by the **Travel Rule** before the wallet can be used in **any** flow — onramp, offramp, or swap.
Submit a proof of address ownership. **The message must be in the format**:
> I am verifying ownership of the wallet address [Wallet Address] as customer [CustomerId]. This message was signed on [DD/MM/YYYY] to confirm my control over this wallet.
The signature must be generated using the private key of the wallet address and is just the standard string format of standard ethereum / solana signatures:
## Example message
```ignore
I am verifying ownership of the wallet address GEhVdAGrGFvbAyj9ieZQt43mRAZZu4hRQ5MaKGgvpJMq as customer 7feaf33d-8551-4144-bfcb-01c86bcff857. This message was signed on 20/01/2024 to confirm my control over this wallet.
```
## Example signature
solana signature: 3Z3TWYMhq9vrA6K9ajQMQmVi1aXTmyASGLEvMpzNdS3BdWhNKeQGHtvF6cFNzR96tsnPPocJPjxfVsjtay9iWgTG
evm signature: 0x1234567890abcdef1234567890abcdef12345678900xa5f1751b75a28c12694f02590d29b8cdd68b4f5c783273a75823fc6cfeaa702f1a65fa7c1838ae799fe755c92443cb51631d000922329cdd2ce799eee75f42531babcdef1234567890abcdef
## Smart contract wallets (e.g. Safe, Privy)
EVM smart contract wallets are supported using the same `message` and `signature` fields, no separate endpoint or parameter is needed. Have your wallet SDK sign the same ownership message and submit whatever signature it produces:
- **Deployed wallets** sign per [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271).
- **Not-yet-deployed (counterfactual) wallets** sign per [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492), which wraps the ERC-1271 signature so it can be verified before the wallet's first on-chain deployment.
This endpoint automatically detects and validates both formats. Solana, Stellar, and Tron only support standard wallet signatures.
# Retry Authentication
Source: https://dev.enterprise.moonpay.com/reference/addresses/retry-authentication
https://api.sandbox.iron.xyz/spec/2026-10-15 post /addresses/fiat/{id}/retry-auth
Retry the authentication process on a failed fiat address
NOTE: This endpoint is not yet implemented
# Search hosted wallet providers (VASPs)
Source: https://dev.enterprise.moonpay.com/reference/addresses/search-hosted-wallet-providers-vasps
https://api.sandbox.iron.xyz/spec/2026-10-15 get /addresses/crypto/hosted/vasps
Look up a VASP by name to find its DID (Decentralized Identifier). You need the DID when registering a hosted wallet via [Register Hosted Wallet](#tag/Addresses/operation/registerHostedWallet).
# Update the disabled status of a verified crypto address
Source: https://dev.enterprise.moonpay.com/reference/addresses/update-the-disabled-status-of-a-verified-crypto-address
https://api.sandbox.iron.xyz/spec/2026-10-15 put /addresses/crypto/{id}/disabled
Enable or disable a verified crypto address. Disabled addresses cannot be used for transactions.
Only addresses belonging to the customer of the authenticated partner can be modified.
# List autoramp transactions
Source: https://dev.enterprise.moonpay.com/reference/autoramp/list-autoramp-transactions
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramp-transactions
Returns a list of transactions, optionally filtered by autoramp ID or customer ID
An optional `search_term` parameter can be provided that will search the following fields:
- Bank Account Name
- Bank Account Provider Name
- Wallet Address
- Transaction ID
# Archive a customer
Source: https://dev.enterprise.moonpay.com/reference/customer/archive-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 put /customers/{id}/archive
Archive a customer and all of their identifications. This is terminal and
cannot be undone: the customer is completely and permanently blocked from
using any services.
# Archive an identification
Source: https://dev.enterprise.moonpay.com/reference/customer/archive-an-identification
https://api.sandbox.iron.xyz/spec/2026-10-15 put /identifications/{id}/archive
Archive an identification. This is terminal and cannot be undone: the
identification is permanently invalidated and can no longer be used to
verify the customer.
# Create a new customer
Source: https://dev.enterprise.moonpay.com/reference/customer/create-a-new-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 post /customers
Create a new **business** or **legal person** customer.
# Create a new Customer signing
Source: https://dev.enterprise.moonpay.com/reference/customer/create-a-new-customer-signing
https://api.sandbox.iron.xyz/spec/2026-10-15 post /customers/{id}/signings
This will create a signing for the customer, representing the legal consent to a specific Document.
See The KYC/Compliance section of our documentation for more info.
# Create Customer Identification V2
Source: https://dev.enterprise.moonpay.com/reference/customer/create-customer-identification-v2
https://api.sandbox.iron.xyz/spec/2026-10-15 post /customers/{id}/identifications/v2
This will create a new identification for the customer, dependent on the customer type a KYC or KYB will be created for the customer to complete.
we support three types of identification:
- Link: where a link is provided to the customer for access to the identification process.
- Token: where a token is provided in the request from which verification data can be extracted and where the token is generated by the third party provider
- Person: where the identification details of the person for which the identification process is being created are provided in the request
# Fetch identification compliance questionnaire
Source: https://dev.enterprise.moonpay.com/reference/customer/fetch-identification-compliance-questionnaire
https://api.sandbox.iron.xyz/spec/2026-10-15 get /identifications/{id}/compliance-questionnaire
Fetch the compliance questionnaire for an identification
# Get a customer by external ID
Source: https://dev.enterprise.moonpay.com/reference/customer/get-a-customer-by-external-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{external_id}/external
Retrieve a specific customer by external ID
# Get a customer by ID
Source: https://dev.enterprise.moonpay.com/reference/customer/get-a-customer-by-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}
Retrieve a specific customer by ID
# Get all customer Identifications
Source: https://dev.enterprise.moonpay.com/reference/customer/get-all-customer-identifications
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/identifications
Retrieve all identifications for this customer
# Get all customer Signings
Source: https://dev.enterprise.moonpay.com/reference/customer/get-all-customer-signings
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/signings
Retrieve all signings for a customer, representing the various legal agreements and consents gathered from the customer.
See The KYC/Compliance section of our documentation for more info.
# Get all Customers
Source: https://dev.enterprise.moonpay.com/reference/customer/get-all-customers
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers
Get all customers for a particular partner. The partner ID does not need to be provided as it
is provided by the API Key.
An optional `search_term` can be provided to filter by name, id, email, or external id.
# Get an Identification by ID
Source: https://dev.enterprise.moonpay.com/reference/customer/get-an-identification-by-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /identifications/{id}
Retrieve a specific Identification by ID
# Get an RFI
Source: https://dev.enterprise.moonpay.com/reference/customer/get-an-rfi
https://api.sandbox.iron.xyz/spec/2026-10-15 get /rfis/{rfi_id}
# Get customer abilities
Source: https://dev.enterprise.moonpay.com/reference/customer/get-customer-abilities
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/abilities
Retrieve the abilities/capabilities for a specific customer by ID
# Get customer microdeposits
Source: https://dev.enterprise.moonpay.com/reference/customer/get-customer-microdeposits
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/microdeposits
Retrieve all microdeposits for a specific customer by ID
# Get customer required signings
Source: https://dev.enterprise.moonpay.com/reference/customer/get-customer-required-signings
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/required-signings
Get all required signings for a customer
# Get customer requirements
Source: https://dev.enterprise.moonpay.com/reference/customer/get-customer-requirements
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/requirements
API 2026-10-01 returns catalog descriptions and separate review feedback,
validation occurrences, and rejected document steps. Earlier versions retain
the legacy shape. The default when X-API-Version is omitted remains unchanged.
Customers without stored requirements show the identification-level requirement.
Only applicant-facing feedback is included on this route.
# Get customer spending limit usage
Source: https://dev.enterprise.moonpay.com/reference/customer/get-customer-spending-limit-usage
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{id}/spending-limit
Retrieve the current spending limit usage for a specific customer,
split by direction (fiat on-ramp and crypto off-ramp).
Amounts are returned in **EUR cents** as strings.
`threshold_eur` (and therefore `remaining_eur`) is `null` for Business
customers and persons with `ThresholdKind::Regulated`. `null` means
"no enforced cap"
# Get the pending authentication code for an entity
Source: https://dev.enterprise.moonpay.com/reference/customer/get-the-pending-authentication-code-for-an-entity
https://api.sandbox.iron.xyz/spec/2026-10-15 get /authentication-codes/entity/{id}
Get the pending authentication code for an entity by ID
e.g. ID of Autoramp or FiatAddress (entity in AuhorizationRequired state)
if no pending code is found, retry flow will have to be triggered
# List RFIs
Source: https://dev.enterprise.moonpay.com/reference/customer/list-rfis
https://api.sandbox.iron.xyz/spec/2026-10-15 get /rfis
# List RFIs for a customer
Source: https://dev.enterprise.moonpay.com/reference/customer/list-rfis-for-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 get /customers/{customer_id}/rfis
# Record that an RFI document link was opened
Source: https://dev.enterprise.moonpay.com/reference/customer/record-that-an-rfi-document-link-was-opened
https://api.sandbox.iron.xyz/spec/2026-10-15 post /rfis/{rfi_id}/rounds/{round_id}/document-link-opened
# Submit an authentication code
Source: https://dev.enterprise.moonpay.com/reference/customer/submit-an-authentication-code
https://api.sandbox.iron.xyz/spec/2026-10-15 put /authentication-codes/{id}
Submit the code for a given authentication
# Submit an RFI response
Source: https://dev.enterprise.moonpay.com/reference/customer/submit-an-rfi-response
https://api.sandbox.iron.xyz/spec/2026-10-15 post /rfis/{rfi_id}/rounds/{round_id}/response
# Trigger Customer TIN Step-Up
Source: https://dev.enterprise.moonpay.com/reference/customer/trigger-customer-tin-step-up
https://api.sandbox.iron.xyz/spec/2026-10-15 post /customers/{id}/identifications/tin-step-up
For an already-approved customer, moves their existing KYC
onto a TIN-only step-up level and returns a fresh verification
link. Hand the returned `url` to the customer to collect their tax
identification number status. No-ops with a 409 if the customer already
has a TIN on file.
# Update a customer
Source: https://dev.enterprise.moonpay.com/reference/customer/update-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 put /customers/{id}
Update a customer by ID
# Update Customer Identification Data
Source: https://dev.enterprise.moonpay.com/reference/customer/update-customer-identification-data
https://api.sandbox.iron.xyz/spec/2026-10-15 patch /customers/{id}/identification-data
Partial update for a person customer's existing KYC record. Only
non-null fields are applied. Patches the customer's existing KYC
record directly — does not trigger re-verification, so an
already-approved customer keeps their status. Useful for backfilling a
tax identification number (DAC8/CARF) on customers who onboarded before
TIN collection existed.
# Upload an RFI document
Source: https://dev.enterprise.moonpay.com/reference/customer/upload-an-rfi-document
https://api.sandbox.iron.xyz/spec/2026-10-15 post /rfis/{rfi_id}/rounds/{round_id}/documents
# Resolve rejected documents
Source: https://dev.enterprise.moonpay.com/rejected-documents
Find customers whose documents were rejected, see exactly why, and fix it: resend a link, upload a replacement, or start a new KYC if the item can't be retried.
### 1. Find your customer
Open **Compliance** > **Change Requested**: customers with one or more rejected items.
### 2. Read the reason
Click the row > **Requirements**: the rejected item carries its reject reason, for example that screenshots are not accepted and the original photo or scan is needed, along with whether it's marked **Retryable**. Pass the reason on to the customer only if it's retryable.
### 3. Fix it, one of two ways
* **Copy KYC & Terms link**, if the item is retryable: send the customer the link again so they can resubmit it themselves.
* **Upload documents**, top right of the Requirements section: submit a replacement yourself. It lists only the rejected documents, each with its review comment and issuing country, and accepts JPEG, PNG, or PDF up to 10 MiB.
### 4. Start over if it is terminal
If the item is not retryable, or there's no clear indication that it can be, **Create new KYC** starts a fresh verification instead. The new identification becomes the customer's active KYC and they start again from the beginning; earlier identifications stay in the history.
**Upload documents** appears when the outstanding item is a retryable document requirement, or when a business customer is missing company documents and has none rejected. In that second case the dialog takes a new file instead of listing rejected documents. Either way, only team members whose role carries the `UploadIdentificationDocument` permission see it. If you cannot see the button, either the blocking item is not a document or your role does not allow it.
# SumSub Token Sharing
Source: https://dev.enterprise.moonpay.com/reliance-kyc-token-sharing
Share existing SumSub KYC data with MoonPay to onboard verified applicants in seconds.
## Introduction
If your organization already verifies customers through SumSub, you can share that verification data with MoonPay using a **share token**. Your customers do not need to go through KYC again.
This requires a tri-party agreement between MoonPay, your organization, and SumSub. Once signed, the integration is straightforward:
The profile must include contact info, address, and (for **US citizens or residents**) an **SSN or ITIN** in `fixedInfo.tin`. For IP, follow [IP address](#ip-address): supply it (or use SumSub IP capture) when you need **EUR**-related capabilities. Otherwise you can omit `ip_address` on the MoonPay request.
Call SumSub's API to create a one-time share token for your applicant.
Send the token to MoonPay's identification endpoint with the required parameters.
If the data is sufficient, onboarding completes in seconds. If not, redirect the customer to complete the missing steps.
Present and sign the terms and conditions before submitting the share token. Fetch the document via `GET /api/customers/{id}/required-signings` and record acceptance via `POST /api/customers/{id}/signings`. Keep this flow reachable afterwards: customers must sign again when a new document is published. See [Terms and Conditions](/onboarding#terms-and-conditions).
## What Data Does MoonPay Expect?
At minimum, MoonPay expects the applicant data described in the [standard onboarding requirements](/kyc#standard-onboarding-usa-eea-uk-aus-low-risk-row). The share token must carry enough verified data to satisfy these requirements. **US citizens and residents** must also have an **SSN or ITIN** in `fixedInfo.tin` before sharing. See [US citizens and residents: SSN and TIN](#us-citizens-and-residents-ssn-and-tin).
**To keep onboarding smooth and avoid extra steps for your customer:**
* **Include the [KYC questionnaire](#kyc-questionnaire-fields)** in your request. This covers employment status, income, source of wealth, and transaction expectations, which SumSub does not typically collect.
* **Include the [EDD questionnaire](#edd-questionnaire-fields) as well** if you already know the customer will transact large amounts or resides in a higher-risk jurisdiction. This prevents a step-up flow later.
If any required data is missing, MoonPay returns a `Pending` status with a `url`. This URL opens a hosted SumSub flow that **only asks for the missing information**, not a full KYC redo. You should redirect your customer to this URL to complete onboarding. See [handling missing information](/onboarding#handling-missing-information) for details.
## Preparing Your SumSub Applicant
Before generating a share token, the applicant's SumSub profile must contain all the data MoonPay requires. Missing fields are the most common cause of failed or incomplete token shares.
### IP Address
MoonPay uses the applicant's **IP address** for fraud and geo signals (including where **EUR** / euro-denominated or SEPA-related capabilities apply). You can provide it in one of three ways:
1. **SumSub IP tracking**: Enable [SumSub's IP address tracking](https://docs.sumsub.com/docs/advanced-ip-check) so the IP is captured automatically during verification.
2. **Metadata on the applicant**: Store the IP as metadata on the SumSub applicant so it is included in the shared data.
3. **Via the MoonPay API**: Pass the `ip_address` field directly in your [identification request](#2-submit-the-token-to-moonpay) to MoonPay.
If you use SumSub's IP tracking, you do not need to pass `ip_address` in the MoonPay API call. MoonPay reads it from the shared applicant data.
If your integration **does not require EUR** (for example, no euro virtual accounts, SEPA, or other euro-specific MoonPay flows), you can **omit `ip_address`** on the MoonPay identification request. If you **do** need EUR-related capabilities, supply the IP using one of the options above so checks can run reliably.
### Required Applicant Data
The following fields must be present on the SumSub applicant's `fixedInfo` before sharing. If your KYC flow does not collect them, use SumSub's [Change Provided Info](https://docs.sumsub.com/reference/change-provided-info-fixedinfo) endpoint to PATCH them onto the applicant.
| Field | Location | Description |
| - | - | - |
| `phone` | Top-level | Applicant's phone number (e.g. `+12345678`) |
| `nationality` | `fixedInfo` | ISO 3166-1 alpha-3 country code (e.g. `DEU`) |
| `tin` | `fixedInfo` | SSN or ITIN. **Required for US citizens and residents.** A value starting with `9` is read as an ITIN, otherwise an SSN. Dashes are allowed. |
| `street` | `fixedInfo.addresses[]` | Street address |
| `town` | `fixedInfo.addresses[]` | City |
| `postCode` | `fixedInfo.addresses[]` | Postal code |
| `country` | `fixedInfo.addresses[]` | ISO 3166-1 alpha-3 country code |
### US citizens and residents: SSN and TIN
For **US citizens or US residents**, MoonPay requires a **Social Security Number (SSN)** or **Individual Taxpayer Identification Number (ITIN)**. See [Individual KYC standard onboarding](/kyc#standard-onboarding-usa-eea-uk-aus-low-risk-row) for the full requirement.
Set this value on the applicant's `fixedInfo.tin` field before you generate the share token. If your KYC flow does not collect it, PATCH it onto the applicant with SumSub's [Change Provided Info](https://docs.sumsub.com/reference/change-provided-info-fixedinfo) endpoint. A value starting with `9` is treated as an ITIN, otherwise as an SSN. Dashes are stripped, so `123-45-6789` and `123456789` are equivalent.
### Sample Applicant
Below is a representative SumSub applicant object showing the fields MoonPay needs. Fields extracted automatically by SumSub (document data, review results, questionnaires) are omitted for brevity.
```json theme={null}
{
"clientId": "iron.xyz",
"info": {
"firstName": "John",
"firstNameEn": "John",
"lastName": "Doe",
"lastNameEn": "Doe",
"dob": "1990-01-11",
"country": "DEU"
},
"fixedInfo": {
"firstName": "John",
"firstNameEn": "John",
"lastName": "Doe",
"lastNameEn": "Doe",
"dob": "1990-01-11",
"country": "DEU",
"nationality": "DEU",
"addresses": [
{
"street": "5 some street.",
"streetEn": "5 some street.",
"stateCode": "DE-BE",
"town": "Berlin",
"townEn": "Berlin",
"postCode": "12345",
"country": "DEU",
"formattedAddress": "5 some street., Berlin, Germany, 10247"
}
]
},
"email": "john.doe@gmail.com",
"phone": "+12345678",
"lang": "en",
"type": "individual"
}
```
The `fixedInfo` block is the most important section: it contains the data that was provided by the customer and must be collected during your flow or PATCHed via the SumSub API. The `info` block is typically populated by SumSub's document extraction and does not need manual intervention.
## Integration
### 1. Generate a SumSub Share Token
Use [SumSub's share token API](https://docs.sumsub.com/reference/generate-share-token) to issue a one-time token. This call is made **server-side** using your SumSub credentials.
```bash theme={null}
curl --request POST \
--url https://api.sumsub.com/resources/accessTokens/shareToken \
--header 'Content-Type: application/json' \
--header 'X-App-Token: ' \
--data '{
"applicantId": "YOUR_APPLICANT_ID",
"forClientId": "iron.xyz",
"ttlInSecs": 600
}'
```
Share tokens are **single-use**: SumSub invalidates a token after it has been consumed. Generate a new token for each submission attempt.
### 2. Submit the Token to MoonPay
```
POST /api/customers//identifications/v2
```
**Headers**
| Header | Required | Description |
| - | - | - |
| `X-API-Key` | Yes | Your MoonPay API key |
| `Idempotency-Key` | Yes | A unique UUID. Prevents duplicate identifications if retried. |
| `X-Sub-Partner-ID` | No | Sub-partner UUID, if scoped to a sub-partner |
**Body Parameters**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `type` | `"Token"` | Yes | Must be `"Token"` |
| `token` | string | Yes | The SumSub share token |
| `intended_use` | string | Yes | One of: `Investing`, `PaymentToFriendsAndFamily`, `PaymentToThirdParties`, `PurchaseDigitalAssets`, `OnlinePurchasesOfGoodsOrServices`, `Trading`. `PaymentToFriendsFamilyorOthers` is deprecated and rejected from `X-API-Version: 2026-10-15` with [`intended_use_deprecated`](/errors#intended_use_deprecated). |
| `ip_address` | string | No | Applicant IP for fraud/geo signals. Optional if captured via SumSub. See [IP address](#ip-address). You can omit it when your integration **does not require EUR**; otherwise supply it (API, SumSub tracking, or applicant metadata). |
| `kyc_questionnaire` | object | Recommended | See [KYC questionnaire fields](#kyc-questionnaire-fields). Omitting this field causes a `Pending` response requiring a step-up flow for the customer. |
| `edd_questionnaire` | object | No | See [EDD questionnaire fields](#edd-questionnaire-fields). EDD is only triggered when you include this object. Omitting it means EDD is not applied. |
`with_edd` is a **response field only**. It reflects whether EDD was applied to the identification. Do not include it in your request body. MoonPay Enterprise ignores it.
#### Basic Request
At minimum, provide the token and intended use:
```bash theme={null}
curl --request POST \
--url https://api.sandbox.iron.xyz/api/customers//identifications/v2 \
--header 'X-API-Key: ' \
--header 'Idempotency-Key: ' \
--header 'content-type: application/json; charset=utf-8' \
--data '{
"type": "Token",
"token": "sumsub_token",
"intended_use": "Investing",
"ip_address": "203.0.113.42"
}'
```
#### With KYC Questionnaire
Include the questionnaire to avoid the customer being redirected to a step-up flow:
```bash theme={null}
curl --request POST \
--url https://api.sandbox.iron.xyz/api/customers//identifications/v2 \
--header 'X-API-Key: ' \
--header 'Idempotency-Key: ' \
--header 'content-type: application/json; charset=utf-8' \
--data '{
"type": "Token",
"token": "sumsub_token",
"intended_use": "Investing",
"ip_address": "203.0.113.42",
"kyc_questionnaire": {
"employment_status": "Employed",
"yearly_gross_income": "From50001To60000",
"source_of_wealth": "Salary",
"expected_monthly_transaction_count": "Between5And10",
"expected_monthly_transaction_volume": "MoreThan500LessThan2000"
}
}'
```
#### With KYC + EDD Questionnaire
If the customer resides in a higher-risk jurisdiction or will transact large amounts, include both to complete onboarding in one step:
```bash theme={null}
curl --request POST \
--url https://api.sandbox.iron.xyz/api/customers//identifications/v2 \
--header 'X-API-Key: ' \
--header 'Idempotency-Key: ' \
--header 'content-type: application/json; charset=utf-8' \
--data '{
"type": "Token",
"token": "sumsub_token",
"intended_use": "Investing",
"ip_address": "203.0.113.42",
"kyc_questionnaire": {
"employment_status": "Employed",
"yearly_gross_income": "From50001To60000",
"source_of_wealth": "Salary",
"expected_monthly_transaction_count": "Between5And10",
"expected_monthly_transaction_volume": "MoreThan500LessThan2000"
},
"edd_questionnaire": {
"occupation": "FinancialServices",
"approximate_net_worth": "Between100001And300000",
"source_of_funds_proof": ""
}
}'
```
#### KYC Questionnaire Fields
| Field | Values |
| - | - |
| `employment_status` | `Employed`, `SelfEmployed`, `Unemployed`, `Retired`, `Student` |
| `yearly_gross_income` | `LessThan20000`, `From20001To30000`, `From30001To40000`, `From40001To50000`, `From50001To60000`, `From60001To70000`, `From70001To80000`, `From80001To90000`, `From90001To100000`, `From100001To110000`, `From110001To120000`, `From120001To130000`, `From130001To140000`, `From140001To150000`, `From150001To200000`, `From200001To500000`, `MoreThan500000`. `MoreThan150000` is deprecated: submissions using it are coerced to `From150001To200000`. Use one of the three bands above `150000` instead. |
| `source_of_wealth` | `Salary`, `Savings`, `Investments`, `CryptoTrading`, `SaleOfAssetsOrSettlement`, `Pension`, `CompanyProfits`, `SoleProprietorIncome`, `RentalIncome`, `Inheritance`, `Other` |
| `expected_monthly_transaction_count` | `LessThan5`, `Between5And10`, `MoreThan10` |
| `expected_monthly_transaction_volume` | `LessThan500`, `MoreThan500LessThan2000`, `MoreThan2000` |
#### EDD Questionnaire Fields
| Field | Values |
| - | - |
| `occupation` | `Agriculture`, `ArtsAndEntertainment`, `Construction`, `Education`, `FinancialServices`, `InformationAndTechnology`, `Retail`, `RealEstate`, `Other`, `BusinessOwner`, `Healthcare`, `Industrial`, `LegalServices`, `PublicSector`, `SeniorManagement` |
| `approximate_net_worth` | `UpTo25000`, `Between25001And50000`, `Between50001And100000`, `Between100001And300000`, `Between300001And500000`, `Between500001And1000000`, `Over1000001` |
| `source_of_funds_proof` | Base64-encoded Source of Wealth proof (jpg, png, pdf, doc, docx, xls, xlsx, odt, ods, txt). Max 10 MB. |
### 3. Handle the Response
The endpoint returns an `Identification` object:
| Field | Type | Description |
| - | - | - |
| `id` | string (UUID) | Unique identification ID |
| `customer_id` | string (UUID) | The customer this identification belongs to |
| `status` | string | Current status. See [Identification Lifecycle](#4-identification-lifecycle). |
| `with_edd` | boolean or null | Whether EDD was applied: `true`, `false`, or `null` for pre-feature identifications. See [Tracking EDD Status](/onboarding#tracking-edd-status). |
| `url` | string or null | If `Pending`, redirect the customer to this URL to complete missing steps |
| `created_at` | string (ISO 8601) | Creation timestamp |
| `updated_at` | string (ISO 8601) | Last update timestamp |
**Sufficient data**: onboarding proceeds immediately. `with_edd` reflects whether EDD was applied. It is set by MoonPay Enterprise based on the risk profile, not by your request.
```json theme={null}
{
"id": "9a667227-6992-4c6b-951e-63765d5d8f33",
"customer_id": "c5f2e8a1-3b4d-4e6f-8a9b-1c2d3e4f5a6b",
"status": "Processed",
"with_edd": true,
"url": null,
"created_at": "2025-01-06T16:54:40.162807+00:00",
"updated_at": "2025-01-06T16:54:40.162807+00:00"
}
```
**Insufficient data**: redirect the customer to complete the missing information:
```json theme={null}
{
"id": "9a667227-6992-4c6b-951e-63765d5d8f33",
"customer_id": "c5f2e8a1-3b4d-4e6f-8a9b-1c2d3e4f5a6b",
"status": "Pending",
"with_edd": true,
"url": "https://in.sumsub.com/idensic/l/#/...",
"created_at": "2025-01-06T16:54:40.162807+00:00",
"updated_at": "2025-01-06T16:54:40.162807+00:00"
}
```
**Error Responses**
| Status | Cause |
| - | - |
| `400` | Invalid body, customer is not a person, invalid base64 `source_of_funds_proof`, or `source_of_funds_proof` exceeds 10 MB |
| `401` | Invalid API key or missing `CreateIdentification` permission |
| `403` | Authenticated, but not authorized for this customer or sub-partner scope |
| `404` | Customer not found |
| `409` | The `Idempotency-Key` was already used for a request with a different body. See [Idempotency](/idempotency) |
| `429` | Identification creation rate limit exceeded |
| `500` | Internal error while creating the identification. Retry with the same `Idempotency-Key`. |
A `409` on the token flow always means the `Idempotency-Key` was reused with a different body. Send a fresh UUID for each new submission, and reuse a key only when retrying that exact request. Archiving the customer's existing identification does not clear it.
The token flow does not reject duplicate payloads, so a `409` here never points at an existing identification. That check applies to the `Person` and `Business` flows only. See [KYC/KYB Sharing](/sharing#duplicate-submissions).
This endpoint uses three error body shapes. `401`, `403`, `404`, and `429` return a plain JSON string. `409` carries no explanatory body, just the status. `500` returns an object with `message` and `trace_id`. Quote the `trace_id` when reporting a `500` to MoonPay.
`400` depends on your `X-API-Version`: unversioned or earlier callers get a plain JSON string, same as `401`/`403`/`404`. Callers on `2026-08-01` or later get a structured array of error objects instead. See [Identification Errors](/errors).
A client error (`401`, `403`, `404`, or `429`) is a JSON string:
```json theme={null}
"Source of funds proof exceeds 10MB limit"
```
A server error (`500`) is an object:
```json theme={null}
{
"message": "Create customer identification creation error. Please try again.",
"trace_id": "0f9c7a1e-3b4d-4e6f-8a9b-1c2d3e4f5a6b"
}
```
### Proof of Address
There is no separate API field for proof of address (POA). POA is expected to come from the SumSub applicant's verified documents: it is read from the shared token, not uploaded directly to MoonPay Enterprise.
**If you collect POA during your KYC flow**, ensure the document is on the SumSub applicant before generating the share token. MoonPay Enterprise reads it automatically.
**If POA is missing when MoonPay Enterprise processes the token**, two options:
1. **Collect POA in SumSub, then re-submit**: Add the document to the SumSub applicant (via your own flow or SumSub's API), generate a new share token, and create a new identification. See [Re-submitting After Missing Document Data](#re-submitting-after-missing-document-data).
2. **Redirect to the hosted step-up flow**: Use the `url` returned on the `Pending` identification. MoonPay Enterprise's hosted flow collects only the missing documents. The customer does not redo full KYC.
The MoonPay Enterprise dashboard showing "Proof of Residence: Not Started" means MoonPay Enterprise determined POA is required but was not present in the shared token. It does not mean the identification has failed.
### Re-submitting After Missing Document Data
If a `Pending` response is caused by missing documents on the SumSub applicant (for example, a missing proof of address that was collected later), you cannot update an existing identification. Instead:
1. Ensure the SumSub applicant has the missing document or data. Use SumSub's [Change Provided Info](https://docs.sumsub.com/reference/change-provided-info-fixedinfo) endpoint to PATCH it if needed.
2. Generate a new share token for the applicant.
3. Create a new identification using `POST /api/customers//identifications/v2` with the new token.
Each identification is immutable once created. A new token submission creates a new identification for the same customer.
### 4. Identification Lifecycle
| Status | Description |
| - | - |
| `Pending` | Waiting for the end-user to complete missing steps via the returned URL |
| `Processed` | Data submitted and verification is underway |
| `PendingReview` | Under manual review by the compliance team |
| `Approved` | Identification approved. Customer is verified. |
| `Declined` | Identification declined |
| `Expired` | Identification expired due to inactivity |
In the best case, an applicant with sufficient data moves from `Processed` to `Approved` within seconds.
Subscribe to the `identification_status` [webhook](/webhooks) to track status changes in real time rather than polling.
# Information Requests (RFIs)
Source: https://dev.enterprise.moonpay.com/rfi
Read open information requests for your customers, answer the questions in the latest round, and upload the documents MoonPay asked for.
## Overview
An information request (RFI) is MoonPay Enterprise asking for more detail about one of your customers. An RFI carries questions, a request for documents, or both. You collect the answers from your customer, submit them through the API, and MoonPay reviews and closes the RFI.
There are three types:
* `probing_questions`: questions about the customer or their activity
* `source_of_funds`: questions about specific transactions. Always tied to a transaction
* `liveness`: a new selfie check. The RFI carries a `liveness_url` to send your customer to. It takes no answers and no follow-up rounds, so a response to it returns `400`
Some RFIs also move the customer back to `IdentificationRequired`. That pauses the customer, and funds sent to an active Autoramp are held until the RFI is resolved. Treat an RFI on a live customer as blocking, not informational.
Held funds can be returned to the sender, but only manually. [Contact the MoonPay team](/support) to arrange it.
Not every RFI does this. A customer can keep transacting with an open RFI, so read the customer's status rather than assuming the RFI paused them.
RFIs do not emit webhooks. Poll `GET /api/rfis` to pick up new requests.
## Rounds
Every RFI has at least one round. Round 1 is created with the RFI. If your answers don't settle the question, MoonPay sends a follow-up round with new questions on the same RFI.
Only the latest round accepts a response. The write endpoints take a `round_id` and return `409 Conflict` if that round is not the latest one, so read the RFI first and use the round with the highest `round_number`. Rounds come back sorted by `round_number` ascending.
## Statuses
| Status | Description |
| - | - |
| `waiting_on_response` | The latest round is open. You owe a response. |
| `needs_review` | Your response is in. MoonPay is reviewing it. |
| `closed` | MoonPay resolved the RFI. |
Submitting a response moves an RFI from `waiting_on_response` to `needs_review`. A follow-up round moves it back to `waiting_on_response`.
The API does not return how an RFI was resolved. A `closed` RFI tells you MoonPay is done with it, not whether the response passed.
## How it works
`GET /api/rfis`
Returns open RFIs grouped by customer, oldest first.
Take the round with the highest `round_number`. Its `questions` are what you need to answer, and `documents_requested` tells you whether documents are also needed.
`POST /api/rfis/{rfi_id}/rounds/{round_id}/documents`
Collect the files from your customer and upload each one. Upload at least one before you submit the response.
`POST /api/rfis/{rfi_id}/rounds/{round_id}/response`
Answer every required question in one call. The RFI moves to `needs_review`.
MoonPay either closes the RFI or sends a follow-up round. Keep polling to find out which.
## Answer an RFI in the dashboard
Every RFI also shows up in the Partner Dashboard, so your team can answer one without an integration. The dashboard calls the same endpoints documented below, so an RFI answered there reaches `needs_review` exactly as an API response does.
### 1. Open the RFI queue
**Compliance** > **RFIs**: every customer with an open request, how many requests each one has, and the age of the oldest. The status column separates what you owe a response on from what is already back with MoonPay.
### 2. Read the request
Click the customer's name. **Questions · Round 1** lists what was asked, with a **Needs action** badge while a response is outstanding. The copy button copies the questions to your clipboard to forward to your customer.
Required questions carry an asterisk, and each answer box matches the question type: free text, or Yes/No with an optional details field.
### 3. Submit the response
**Submit response** stays disabled until every required question has an answer. When documents were requested, the card also shows a file upload, and submitting is blocked until at least one file is uploaded.
After submitting, the card turns read-only: a **Submitted** badge, your answers echoed under each question, a Responded timestamp, and **Copy Q\&A** for the whole exchange. The customer's row drops its needs-action badge.
A follow-up round arrives as a new **Questions · Round 2** card on the same RFI, and the customer's row goes back to needs action. Answer it the same way.
## List open RFIs
`GET /api/rfis`
By default, returns RFIs in `waiting_on_response` or `needs_review`, grouped by customer. Customers with at least one RFI still waiting on a response come first, then the customer whose oldest RFI was sent earliest. Closed RFIs are not included unless you pass `state=completed`, which returns only closed RFIs, most recently closed first.
### Query parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `state` | string | No | `open` (default) or `completed` |
| `search_term` | string | No | Matches customer name, customer email, or an exact customer ID. Max 256 characters |
| `cursor` | string (UUID) | No | Customer ID from the previous response's `cursor`. Fetches the next page |
| `prev_cursor` | string (UUID) | No | Customer ID from the previous response's `prev_cursor`. Fetches the previous page |
| `page_size` | integer | No | Customers per page. Default `10`, max `100` |
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/rfis?page_size=25" \
-H "X-API-Key: "
```
```typescript theme={null}
const response = await fetch(
"https://api.sandbox.iron.xyz/api/rfis?page_size=25",
{ headers: { "X-API-Key": "" } },
);
```
```python theme={null}
response = requests.get(
"https://api.sandbox.iron.xyz/api/rfis",
params={"page_size": 25},
headers={"X-API-Key": ""},
)
```
### Response
```json theme={null}
{
"items": [
{
"customer_id": "9c1f0a3e-5d4b-4f20-9a6e-3b71c8f2d014",
"customer_name": "Alina Vogt",
"customer_email": "alina.vogt@example.com",
"customer_type": "Person",
"oldest_sent_at": "2026-09-12T09:14:02Z",
"rfis": [
{
"id": "7f3a2b18-64c9-4f8d-8c31-b0d5e9a12f77",
"customer_id": "9c1f0a3e-5d4b-4f20-9a6e-3b71c8f2d014",
"transaction_id": null,
"rfi_type": "probing_questions",
"status": "waiting_on_response",
"rounds": [
{
"id": "c58e1d47-2a90-4b6f-9f03-7e2c4a8b1d55",
"round_number": 1,
"questions": [
{
"id": "employment_status",
"prompt": "What is the customer's current employment status?",
"answer_type": "text",
"required": true
},
{
"id": "trades_for_third_party",
"prompt": "Is the customer transacting on behalf of anyone else?",
"answer_type": "yes_no",
"required": true
}
],
"answers": null,
"documents_requested": true,
"document_instructions": "Upload the last three months of bank statements.",
"document_upload_url": null,
"document_link_opened_at": null,
"documents": [],
"sent_at": "2026-09-12T09:14:02Z",
"response_received_at": null
}
],
"partner_note": null,
"sent_at": "2026-09-12T09:14:02Z",
"response_received_at": null,
"closed_at": null,
"created_at": "2026-09-12T09:14:02Z",
"updated_at": "2026-09-12T09:14:02Z"
}
]
}
],
"cursor": null
}
```
`cursor` is the customer ID to pass as `cursor` for the next page. It is `null` on the last page.
## List a customer's RFIs
`GET /api/customers/{customer_id}/rfis`
Returns the open RFIs for one customer as a flat array, newest first. Closed RFIs are not included here. Fetch a closed RFI by ID instead.
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/customers/9c1f0a3e-5d4b-4f20-9a6e-3b71c8f2d014/rfis" \
-H "X-API-Key: "
```
## Get an RFI
`GET /api/rfis/{rfi_id}`
Returns one RFI in any status, including closed.
```bash theme={null}
curl -X GET "https://api.sandbox.iron.xyz/api/rfis/7f3a2b18-64c9-4f8d-8c31-b0d5e9a12f77" \
-H "X-API-Key: "
```
### Response
The RFI ID.
The customer the RFI is about.
The transaction under review. Always set for `source_of_funds`, `null` otherwise.
`probing_questions`, `source_of_funds`, or `liveness`.
`waiting_on_response`, `needs_review`, or `closed`.
The verification link to send your customer on a `liveness` RFI. Omitted on every other RFI type, and on a `liveness` RFI until the link is ready.
One entry per round, sorted by `round_number` ascending.
The round ID. Pass it as `round_id` when you respond.
Starts at 1 and increments per follow-up.
Question ID. Use it as `question_id` in your answer. Unique within the round.
The question to put to your customer.
`text` or `yes_no`. Determines which answer field to send.
When `true`, the response is rejected unless this question is answered.
The transaction this question asks about. Present on `source_of_funds` questions, omitted otherwise.
The answers you submitted for this round. `null` until you respond.
When `true`, the round needs documents as well as answers.
What the documents need to show.
External upload link on legacy rounds created before documents were uploaded through the API. `null` on new rounds: upload files with [Upload a document](#upload-a-document) instead.
When the legacy upload link was first recorded as opened. `null` until then.
Files uploaded for this round. Each entry has `id`, `file_name`, `content_type`, `size_bytes`, and `uploaded_at`. Empty until you upload one.
When the round was sent.
When your response for this round arrived. `null` until then.
When the latest round was sent. A follow-up round resets it.
When your response to the latest round arrived. `null` until you respond, and reset to `null` when a follow-up round is sent.
When MoonPay closed the RFI. `null` while it is open.
A note from MoonPay about a closed RFI. Set only on some closed RFIs, `null` otherwise.
When the RFI was created.
When the RFI last changed.
## Upload a document
`POST /api/rfis/{rfi_id}/rounds/{round_id}/documents`
Uploads one file for a round that has `documents_requested: true`. Send it as `multipart/form-data` in a `file` field. Each file must be a PDF, PNG, or JPEG of at most 10 MB, and a round holds at most 20 files. Call it once per file.
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/rfis/7f3a2b18-64c9-4f8d-8c31-b0d5e9a12f77/rounds/c58e1d47-2a90-4b6f-9f03-7e2c4a8b1d55/documents" \
-H "X-API-Key: " \
-H "Idempotency-Key: " \
-F "file=@bank-statement.pdf;type=application/pdf"
```
Returns `200` with the uploaded document:
```json theme={null}
{
"id": "0192a6f4-1c2e-7b3d-9a41-5e8f0c7d2b13",
"file_name": "bank-statement.pdf",
"content_type": "application/pdf",
"size_bytes": 284113,
"uploaded_at": "2026-09-13T10:02:45Z"
}
```
A file of the wrong type or over 10 MB returns `400` with a plain-text body:
```bash theme={null}
< HTTP/2 400
< content-type: text/plain; charset=utf-8
<
upload a PDF, PNG or JPEG file of at most 10 MB
```
This `POST` requires an `Idempotency-Key` header. See [Idempotency](/idempotency) for retry behavior.
## Record that the upload link was opened
`POST /api/rfis/{rfi_id}/rounds/{round_id}/document-link-opened`
Applies only to legacy rounds that carry a `document_upload_url`. Records that the link was opened, and stamps `document_link_opened_at`. Call it once, when you hand the link to your customer.
On a legacy round, an API key can submit a response with `documents_uploaded: true` without this call. A signed-in dashboard user has to open the link first, otherwise the response is rejected with `400`.
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/rfis/7f3a2b18-64c9-4f8d-8c31-b0d5e9a12f77/rounds/c58e1d47-2a90-4b6f-9f03-7e2c4a8b1d55/document-link-opened" \
-H "X-API-Key: " \
-H "Idempotency-Key: "
```
Returns `200` with an empty body.
This `POST` requires an `Idempotency-Key` header. See [Idempotency](/idempotency) for retry behavior.
Calling it twice for the same round returns `409`. See [Common errors](#common-errors).
## Submit a response
`POST /api/rfis/{rfi_id}/rounds/{round_id}/response`
Submits answers for one round. One call per round: there is no partial save, so send every answer you have at once.
### Body
| Field | Type | Required | Description |
| - | - | - | - |
| `answers` | array | Yes | One entry per answered question. Send `[]` for a documents-only round |
| `answers[].question_id` | string | Yes | Must match a question `id` in this round. No duplicates |
| `answers[].text_value` | string | Conditional | Required for `text` questions, and must be non-empty. Omit for `yes_no` |
| `answers[].boolean_value` | boolean | Conditional | Required for `yes_no` questions. Omit for `text` |
| `answers[].explanation` | string | No | Free-text context for the answer. Allowed on either answer type |
| `documents_uploaded` | boolean | Yes | When the round has `documents_requested: true`, upload at least one file first. `true` without an uploaded file is accepted only on a legacy round with a `document_upload_url` |
Send exactly one value per answer. A `text` question with `boolean_value` set, or a `yes_no` question with `text_value` set, returns `400`. Every question marked `required: true` has to be answered in the same call.
```bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/rfis/7f3a2b18-64c9-4f8d-8c31-b0d5e9a12f77/rounds/c58e1d47-2a90-4b6f-9f03-7e2c4a8b1d55/response" \
-H "Content-Type: application/json" \
-H "X-API-Key: " \
-H "Idempotency-Key: " \
-d '{
"answers": [
{
"question_id": "employment_status",
"text_value": "Employed full time as a nurse",
"explanation": "Confirmed by the customer on a recorded call."
},
{
"question_id": "trades_for_third_party",
"boolean_value": false
}
],
"documents_uploaded": true
}'
```
Returns `200` with an empty body. The RFI moves to `needs_review` and `response_received_at` is set on both the round and the RFI.
This `POST` requires an `Idempotency-Key` header. See [Idempotency](/idempotency) for retry behavior.
### Error response
A rejected response returns `400` with a plain-text body, not JSON:
```bash theme={null}
< HTTP/2 400
< content-type: text/plain; charset=utf-8
<
RFI answers are malformed
```
Unlike most API errors, these `400` bodies are plain text. Don't parse them as JSON.
## Common errors
| Error | Cause |
| - | - |
| `400 Bad Request` | `RFI answers are malformed`: unknown or duplicated `question_id`, the wrong value field for the question's `answer_type`, an empty `text_value`, or a missing required answer |
| `400 Bad Request` | `upload the requested documents before submitting`: the round requested documents and none has been uploaded |
| `400 Bad Request` | `upload a PDF, PNG or JPEG file of at most 10 MB`: the uploaded file has the wrong type or is too large |
| `400 Bad Request` | `a liveness RFI opens only through a liveness request and takes no answers or follow-up rounds`: you submitted a response to a `liveness` RFI |
| `401 Unauthorized` | Missing or invalid `X-API-Key` |
| `403 Forbidden` | The API key lacks `View` for the read endpoints, or `UpdateCustomer` for the write endpoints |
| `404 Not Found` | Unknown customer, RFI, or round, or it belongs to another partner |
| `409 Conflict` | The RFI is not in `waiting_on_response`, the round is not the latest one, the round did not request documents or already holds 20 files, or the upload link was already recorded as opened |
| `500 Internal Error` | The request failed server-side. Safe to retry |
Among errors, `400` is the only status that carries a body. Unlike most of the API, these endpoints return no `message` or `trace_id` on `500`.
## Related guides
# Roles & Entities
Source: https://dev.enterprise.moonpay.com/roles-entities
Organizational roles and entity types within MoonPay Enterprise. Covers partners, customers, users, and their relationships and permissions.
## Overarching
* **Partners**: These are companies integrating our API to serve stablecoin payment functionality to their end customers. *MoonPay Enterprise > Partner > Customer*.
* **Customers**: These are the ultimate end customers (Business or individual) using the MoonPay Enterprise-powered services. We also have a B2B frontend to serve customers directly, but this is not our main product. *MoonPay Enterprise > Partner > Customer* or *MoonPay Enterprise > BusinessCustomer*
* **User**: Individual associated with a Partner organization (e.g. developer, compliance officer, operations lead) who is granted access to the MoonPay Enterprise Dashboard (app.enterprise.moonpay.com). Users can view transactions, manage API configurations, monitor customer activity, and handle compliance workflows. Users are managed at the Partner level and must have assigned roles and permissions within the dashboard.
## Partner Overview
The Partner is a business entity that integrates MoonPay Enterprise’s services into its own product or platform under a B2B2B or B2B2C model. The Partner has a direct commercial relationship with MoonPay Enterprise and may help its own end customers access those services.
### Partner Key Points:
* The Partner contracts directly with MoonPay Enterprise to integrate and use MoonPay Enterprise’s services, either for its own operational purposes or to serve its customers.
* The Partner may act as a customer of MoonPay Enterprise and use MoonPay Enterprise’s autoramp services while simultaneously onboarding end customers who also become direct customers of MoonPay Enterprise.
* The Partner embeds MoonPay Enterprise’s infrastructure into its platform and helps with onboarding and access for its end customers, based on one of the approved onboarding frameworks (KYC reliance, KYC/B Sharing, or direct onboarding).
### Partner Access to MoonPay Enterprise Dashboard and MoonPay Enterprise API keys
Once approved, Partners gain access to the **MoonPay Enterprise Dashboard**, a centralized hub for managing and monitoring end customers, and accessing API keys and analytics.
[Partner Dashboard](/partner-dashboard)
### How to Become an MoonPay Enterprise Partner
If you are interested in becoming an MoonPay Enterprise Partner:
For more information or to start your journey as an MoonPay Enterprise Partner, please contact us [here](https://iron.xyz/form). We reply fast!
Share information about your B2B2B or B2B2C arrangements and customer base.
Our team will guide you through the process of setting up your account and gaining access to the MoonPay Enterprise Dashboard.
## Customer Overview
To begin using MoonPay Enterprise's services, every customer must complete an onboarding process. This checks compliance with regulatory requirements, validates the Customer's identity, and establishes their eligibility to trade. The onboarding process is run by the Partner, who acts as the liaison between the Customer and MoonPay Enterprise.
### Customer onboarded via a Partner (B2B2X)
The **Customer** is the end entity that indirectly engages with MoonPay Enterprise's services through a Partner in a **B2B2B** or **B2B2C** model. The Customer is the entity using MoonPay Enterprise's services through the Partner.
The Customer is the end-customer who uses MoonPay Enterprise’s services, delivered through a Partner, under a B2B2B or B2B2C setup. Regardless of the onboarding model applied (KYC reliance, KYC outsourcing, or direct onboarding), the Customer is always considered a direct customer of MoonPay Enterprise.
#### Key Points:
* Although the Customer accesses MoonPay Enterprise’s services through a Partner, the legal and compliance relationship is established directly between the Customer and MoonPay Enterprise.
* The Partner helps the Customer access MoonPay Enterprise’s services but does not act as the service provider.
* MoonPay Enterprise remains responsible for the Customer's compliance oversight, including onboarding, ongoing monitoring, and transaction review. MoonPay Enterprise carries this out either directly or through reliance/outsourcing arrangements.
### Direct Business Customer (B2B)
In a traditional B2B relationship, MoonPay Enterprise provides services directly to a business client, who acts as the end customer. The client uses MoonPay Enterprise’s infrastructure, such as stablecoin payments, on/off-ramps, or wallet services, for its own operational needs.
This model is designed for companies using crypto services for internal treasury, payroll, settlement, or back-office functions. A company that also wants to serve its own customers can do so as a Partner under a B2B2B or B2B2C model. See [Partner + Direct Customer Relationship](#partner--direct-customer-relationship-b2b2x--b2b) below.
#### Key Points:
* A direct commercial relationship exists between MoonPay Enterprise and another business.
* The business (the client) is the end customer of MoonPay Enterprise’s services.
* In a pure B2B relationship, the client uses MoonPay Enterprise for itself. To serve its own customers, it acts as a Partner under a B2B2B or B2B2C model (see below).
* Use Case Example: A fintech company integrates MoonPay Enterprise to access stablecoin rails for its internal treasury operations. It does not offer MoonPay Enterprise's services to its own clients.
### Partner + Direct Customer Relationship (B2B2x + B2B)
A Partner can also use MoonPay Enterprise for its own purposes, trading under its own name in a B2B relationship with MoonPay Enterprise. In this setup, the Partner enters a direct contractual agreement with MoonPay Enterprise and may use MoonPay Enterprise’s services for its own operations. The Partner’s end customers keep being treated as direct customers of MoonPay Enterprise, even if they access the services via the Partner’s platform.
# Sandbox
Source: https://dev.enterprise.moonpay.com/sandbox
Test your MoonPay Enterprise integration against api.sandbox.iron.xyz. Covers sandbox access, differences from production, and simulating transactions.
## Sandbox Location
* **Partner Dashboard:** [https://app.sandbox.iron.xyz](https://app.sandbox.iron.xyz)
* Please reach out to request access to your sandbox environment.
## Differences to Production
The Sandbox is optimized to test and integrate the MoonPay Enterprise APIs. Therefore, it has multiple differences compared to the production system in order to make it easy to work with.
* In production, only an administrator can create an API Key (and then give it to an engineer with an Editor role). In the sandbox, an Editor can also create API Keys to make it easy to test.
* In the sandbox, fiat and crypto flows are simulated. Neither real IBANs nor testnet crypto is used. Instead, we offer a [Sandbox API](/reference/sandbox/mock-a-transaction) to create mock incoming fiat or crypto transactions.
* Any accept/reject flow can be controlled by the user. Instead of going through an email flow, there's a convenient control center where the engineer can decide whether to accept or reject customer fiat accounts, Autoramps, etc.
* The sandbox offers a Reset function to remove all data and start from a pristine state.
* The sandbox has additional `/sandbox` APIs to fully control all the above features. See [Sandbox API](#sandbox-api) below.
## Managing the Sandbox
You can manage the Sandbox in the **Sandbox** area of the [Partner Dashboard](/partner-dashboard).
Log in to the Partner Dashboard.
Navigate to **Developer → Sandbox**.
## Sandbox Operations
The Sandbox page is organised into **Fiat Accounts**, **Autoramps**, **Customers**, and **Transactions** tabs, with **New Transaction** and **Reset Sandbox** in the top right.
Each tab lists one status, so a row leaves its tab as soon as you act on it. An empty tab means nothing is waiting on you.
| Tab | Lists | What you can do |
| - | - | - |
| Fiat Accounts | Fiat accounts in `RegistrationPending` | Register the account |
| Autoramps | Autoramps in `Created` | Set `Created`, `Authorized`, or `Rejected` |
| Customers | Customers in `IdentificationRequired` or `SigningsRequired` | Create an identification, set its status, sign terms |
| Transactions | Transactions in `FundsReviewInProgress` | Accept, reject, or set a specific state |
### Manage Pending Fiat Accounts
When a sandbox customer registers a new fiat account, it appears under **Unverified Fiat Accounts** on the **Fiat Accounts** tab. Open the status dropdown on the row and pick **Registered**. The account then leaves the tab, because the tab only lists accounts in `RegistrationPending`.
The dashboard sends `Registered` for every option in this dropdown. To move an account to a failure status, call [`PUT /api/sandbox/fiat-verification/{id}`](#update-fiat-address-registration-status) with the status you want.
### Manage Pending Autoramps
When a sandbox customer creates a new Autoramp, it appears under **Unverified Autoramps** on the **Autoramps** tab. The status dropdown offers **Created**, **Authorized**, and **Rejected**.
Choose **Authorized** to provision the mock deposit account, which is what a simulated deposit needs. Sending `"Approved"` to `PUT /api/sandbox/autoramp/{id}` provisions the same account and also moves the autoramp to `Approved`, so use the API call if you want to see the `Approved` status your integration waits for.
### Drive a Customer Through Onboarding
The **Customers** tab lists every customer with a `status` of `IdentificationRequired` or `SigningsRequired`, alongside the `identification_status` of their latest identification. Only customers of the partner selected at the top of the page are listed, since the sandbox actions run against one partner at a time.
Each row offers the action its current state needs:
| Row state | Action |
| - | - |
| No identification yet | **Create Identification** starts KYC with a link-type identification, the same call as [Create Customer Identification V2](/reference/customer/create-customer-identification-v2) |
| Identification exists | The **Update Status** dropdown sets the identification to any status, the UI equivalent of [`POST /api/sandbox/identification/{id}/status`](#set-identification-status) |
| `SigningsRequired` | **Sign terms** signs every outstanding terms document for that customer |
The dropdown reaches `Pending`, `Processed`, `PendingReview`, `Approved`, `Declined`, `Expired`, and `Archived`. The sandbox ignores the normal transition rules, so you can jump straight to the status you want to test instead of walking the whole flow. In production, the MoonPay Enterprise compliance team drives these transitions.
**Status guide** in the tab header opens the status combinations and what to show for each one. It is the same mapping as [Mapping Onboarding Statuses in Your App](/onboarding#mapping-onboarding-statuses-in-your-app).
### Create a Transaction
**New Transaction** opens the **Create New Sandbox Transaction** screen, the UI equivalent of [`POST /api/sandbox/transaction`](#create-a-sandbox-transaction). Select a **Customer**, pick a **Ramp** from that customer's autoramps, enter an **Amount** in the ramp's input currency, then click **Execute Transaction**.
The **Amount** field stays disabled until you pick a ramp, and its label then shows the ramp's input currency.
This bypasses the normal deposit flow and builds the transaction directly from the autoramp configuration. It does not use testnet funds.
Executing returns you to the Sandbox page on the **Fiat Accounts** tab. The new transaction starts in `PendingPayin`, so it does not appear on the **Transactions** tab, which lists only `FundsReviewInProgress`. To settle it, call [`PUT /api/sandbox/transaction/{id}/state`](#update-sandbox-transaction-state).
### Settle a Transaction
Pending transactions are listed under **Pending Transactions** on the **Transactions** tab, with **Accept** and **Reject** buttons and a state dropdown for moving a transaction to a specific state. **Accept** sets `Completed` and **Reject** sets `Failed`. All three controls call [`PUT /api/sandbox/transaction/{id}/state`](#update-sandbox-transaction-state).
### Reset Sandbox
**Reset Sandbox** removes all customers, wallets, fiat accounts, transactions, and Autoramps. It asks you to confirm first, and it cannot be undone.
The tables do not clear themselves after a reset. Reload the page to see the empty tabs.
## Sandbox API Key
The Sandbox APIs require an API Key just like the rest.
You can create a sandbox API key in your dashboard: [https://app.sandbox.iron.xyz/dashboard/keys](https://app.sandbox.iron.xyz/dashboard/keys)
```bash theme={null}
curl -X POST https://api.sandbox.iron.xyz/api/sandbox/reset \
-H "X-API-Key: YOUR-API-KEY" \
-H "IDEMPOTENCY-KEY: $(uuidgen)"
```
Replace `YOUR-API-KEY` with the actual API key you generated. The `X-API-Key` header is required for every request. API Base URL: [https://api.sandbox.iron.xyz](https://api.sandbox.iron.xyz)
## Sandbox API
The sandbox offers additional APIs that do not exist in production, grouped under **Sandbox** in the [API reference](/reference/sandbox/mock-a-transaction). They let you create mock transactions, control approval flows, and reset your environment.
All paths below are relative to the API base URL: `https://api.sandbox.iron.xyz`
### Create a Sandbox Transaction
`POST` `/api/sandbox/transaction`
Creates a transaction directly from an autoramp configuration. This bypasses the normal deposit flow and creates a ramp transaction with the specified amount using the autoramp's currencies, customer, and recipient.
| Field | Required | Description |
| - | - | - |
| `autoramp_id` | Yes | The autoramp UUID to create the transaction against |
| `amount` | Yes | Deposit amount in the autoramp's input currency (decimal string) |
| `amount_out` | No | Output amount. If omitted, calculated from `amount`, `fee`, and `fx_rate` |
| `fee` | No | Fee amount (defaults to 0.25% of `amount`) |
| `fx_rate` | No | Exchange rate. If omitted, the live mid-market rate for the currency pair is used |
| `initial_state` | No | Starting state: `Pending`, `Completed`, or `Failed` |
| `input_currency` | No | Override the input currency (Fiat or Crypto object) |
| `source` | No | Override the transaction source (bank account or wallet) |
| `transaction_id` | No | Deterministic transaction UUID instead of auto-generated |
| `deposit_id` | No | Deterministic deposit UUID instead of auto-generated |
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/sandbox/transaction" \
-H "Content-Type: application/json" \
-H "IDEMPOTENCY-KEY: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"autoramp_id": "d3c2b1a4-e5f6-78a9-0123-fedcba654321",
"amount": "100"
}'
```
**Example response:**
The response `state` is the internal transaction state, for example `PendingPayin`, not the `Pending`, `Completed`, or `Failed` value you send in `initial_state`.
```json JSON theme={null}
{
"id": "7d834f68-cea8-496a-8eae-bb0772365028",
"autoramp_id": "d3c2b1a4-e5f6-78a9-0123-fedcba654321",
"amount_in": "100",
"currency_in": "EUR",
"amount_out": "99.75",
"currency_out": "EURC",
"customer_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"state": "PendingPayin",
"created_at": "2026-01-15T10:30:00Z"
}
```
### Update Sandbox Transaction State
`PUT` `/api/sandbox/transaction/{id}/state`
Transition a sandbox transaction to a different state for testing purposes.
| Field | Required | Description |
| - | - | - |
| `state` | Yes | Target state: `Pending`, `Completed`, or `Failed` |
```bash Bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/sandbox/transaction//state" \
-H "Content-Type: application/json" \
-H "IDEMPOTENCY-KEY: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "state": "Completed" }'
```
### Set Identification Status
`POST` `/api/sandbox/identification/{id}/status`
Set a customer's identification to any status, bypassing the normal transition rules, so you can rehearse every branch of your onboarding UI. In production, MoonPay Enterprise's compliance team drives these transitions.
| Field | Required | Description |
| - | - | - |
| `status` | Yes | Target status: `Pending`, `Processed`, `PendingReview`, `Approved`, `Declined`, `Expired`, or `Archived` |
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/sandbox/identification//status" \
-H "Content-Type: application/json" \
-H "IDEMPOTENCY-KEY: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "status": "Approved" }'
```
The identification must belong to a customer of the partner the API key resolves to. Otherwise the call returns 404:
```json JSON theme={null}
"Identification belongs to another partner's customer. Select that partner and retry."
```
### Update Identification Status (deprecated)
`POST` `/api/sandbox/identification/{id}`
Approve or reject a pending customer identification. Use [Set Identification Status](#set-identification-status) instead: it reaches every status, not just approved and rejected.
| Field | Required | Description |
| - | - | - |
| `approved` | Yes | `true` to approve, `false` to reject |
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/sandbox/identification/" \
-H "Content-Type: application/json" \
-H "IDEMPOTENCY-KEY: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "approved": true }'
```
### Update Fiat Address Registration Status
`PUT` `/api/sandbox/fiat-verification/{id}`
Set the registration status of a fiat address. The request body is a plain JSON string (one of the enum values below).
| Status | Description |
| - | - |
| `RegistrationPending` | Registration is being processed |
| `Registered` | Account is active and ready for payouts |
| `RegistrationFailed` | Registration failed |
| `AuthorizationRequired` | Additional authorization needed |
| `AuthorizationFailed` | Authorization was rejected |
```bash Bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/sandbox/fiat-verification/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Registered"'
```
### Update Autoramp Status
`PUT` `/api/sandbox/autoramp/{id}`
Set the status of an autoramp. The request body is a plain JSON string (one of the enum values below).
| Status | Description |
| - | - |
| `Created` | Initial state |
| `EditPending` | Edit is being processed |
| `Authorized` | Authorized by the system |
| `DepositAccountAdded` | Deposit account has been provisioned |
| `Approved` | Fully approved and ready to process transactions |
| `Rejected` | Rejected |
| `Cancelled` | Cancelled |
```bash Bash theme={null}
curl -X PUT "https://api.sandbox.iron.xyz/api/sandbox/autoramp/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Approved"'
```
### Reset the Sandbox
`POST` `/api/sandbox/reset`
Deletes all customers, wallets, fiat accounts, transactions, and autoramps. Requires an `IDEMPOTENCY-KEY` header.
```bash Bash theme={null}
curl -X POST "https://api.sandbox.iron.xyz/api/sandbox/reset" \
-H "IDEMPOTENCY-KEY: $(uuidgen)" \
-H "X-API-Key: $API_KEY"
```
# KYC/KYB Sharing
Source: https://dev.enterprise.moonpay.com/sharing
Provide identification data on your customers directly to MoonPay Enterprise's API instead of using our verification providers.
## KYC/KYB Sharing Agreement
To use the KYC/KYB Sharing API Endpoint, a signed **KYC/KYB Data Sharing Agreement** must be in place between your organization and MoonPay Enterprise. This agreement is a prerequisite for submitting customer identification data directly via the API.
### Why is the agreement required?
MoonPay Enterprise operates under strict anti-money laundering (AML) and counter-terrorist financing (CTF) regulations across multiple jurisdictions. The KYC/KYB Sharing Agreement establishes a formal legal framework that:
* **Defines data collection standards**: Specifies the identity documents, customer information, and verification methods your organization must use when sharing KYC/KYB data with MoonPay Enterprise.
* **Requires regulatory compliance**: Confirms that both parties maintain the necessary licenses, authorizations, and internal controls required by applicable laws.
* **Sets data quality and security requirements**: Establishes obligations around accuracy, completeness, authenticity, and secure transmission of customer data via the API Endpoint.
* **Governs data protection**: Requires customer data to be handled in compliance with applicable data protection laws, with each party acting as an independent data controller.
* **Enables independent verification**: Clarifies that MoonPay Enterprise conducts its own verification and risk assessment on all received data. The agreement does not exempt MoonPay Enterprise from its own compliance obligations.
### Key considerations
* **Timing matters:** Only customer data collected after the agreement is executed is fully compliant for MoonPay Enterprise's onboarding process. Data collected before the agreement requires supplementary verification. See the [Data Collection Timeline](#data-collection-timeline) below for details.
* **Approved identity verification providers:** Your identity verification provider(s) must be proposed to and approved by MoonPay Enterprise before data sharing begins.
* **Data Completion Loop:** a KYB submission missing required documents is rejected with `400`, in every country, and must be resubmitted with the missing item (see [Incomplete Submissions](/kyb#incomplete-submissions)). If MoonPay Enterprise finds gaps later during review, you will be notified to collect and resubmit the data via the API.
To initiate the KYC/KYB Sharing Agreement process, contact the MoonPay Enterprise team at [developers@iron.xyz](mailto:developers@iron.xyz).
## Onboarding Lifecycle
### Optimizing Customer Identification
Use existing customer identity data instead of collecting it again during onboarding. In optimal scenarios, this eliminates the need for redirects or third-party verification flows.
### Regulatory Framework and Data Usage
The KYC/KYB sharing agreement establishes a framework for sharing customer identification data with MoonPay Enterprise for KYC/KYB purposes. However, data collected prior to the agreement falls outside its scope. Regulatory requirements mandate that we independently verify critical identification elements through our authorized KYC providers.
### Data Collection Timeline
Example: KYC/KYB sharing agreement executed at 13:00 (UTC) on January 1, 2026:
| Data Type | Collection Time | Status |
| - | - | - |
| **Post-Agreement** | After 13:00, Jan 1, 2026 | Fully compliant for MoonPay Enterprise's approval process |
| **Pre-Agreement** | Before 13:00, Jan 1, 2026 | Requires supplementary verification |
## Technical Details
### Onboarding via API
The process mirrors the [standard onboarding flow](/onboarding), with one exception: when creating an Identification, a body must be provided.
Present and sign the terms and conditions before submitting identification data. Fetch the document via `GET /api/customers/{id}/required-signings` and record acceptance via `POST /api/customers/{id}/signings`. The terms contain the data sharing agreement, so they must be signed before MoonPay Enterprise collects KYC data. Keep this flow reachable afterwards. Customers must sign again when a new document is published. See [Terms and Conditions](/onboarding#terms-and-conditions).
The request body uses a `type` discriminator at the top level: set it to `"Person"` for KYC or `"Business"` for KYB. For Person, all other fields are siblings of `type`. For Business, company fields nest under `company_data`, with `phone`, `ip_address`, and `questionnaire` alongside it.
#### KYC
```bash theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'Idempotency-Key: ' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/customers//identifications/v2' \
--data-raw '{
"type": "Person",
"first_name": "John",
"last_name": "Smith",
"full_name": "John Smith",
"date_of_birth": "1990-01-01",
"email_address": "[email protected]",
"phone_number": "+12125551234",
"sharing_consent": true,
"consent_collected_at": "2025-04-01T15:38:21.775210Z",
"intended_use": "PurchaseDigitalAssets",
"address": {
"street_1": "123 Main St",
"street_2": "Apt 1",
"city": "Anytown",
"state": "CA",
"zip_code": "12345",
"country": "US"
},
"identity": {
"identity_country_code": "US",
"identity_number": "123456789",
"identity_document_type": "Passport",
"identity_document_front": "",
"identity_document_back": "",
"selfie": "",
"selfie_collected_at": "2025-04-03T13:11:02.263587Z",
"ip_address": "203.0.113.42",
"nationality": "US"
},
"kyc_questionnaire": {
"employment_status": "Employed",
"yearly_gross_income": "From40001To50000",
"source_of_wealth": "Salary",
"expected_monthly_transaction_count": "Between5And10",
"expected_monthly_transaction_volume": "MoreThan500LessThan2000"
},
"tax_identification_number": "123-45-6789",
"documents": [
{
"name": "proof_of_address.pdf",
"file": "",
"type": "ProofOfAddress"
}
]
}'
```
**US citizens and residents must include `tax_identification_number`.** Provide the customer's Social Security Number (SSN) or Individual Taxpayer Identification Number (ITIN). A US person submitted without it fails with `400` and the message `Tax identification number is required for US residents`.
`tax_identification_number` is a separate top-level field from `identity.identity_number`. Put the number from the ID document (passport or license) in `identity.identity_number`, and the SSN or ITIN in `tax_identification_number`. Set `identity.identity_country_code` to the customer's country (`US` for US persons) so the requirement is applied. Dashes are allowed: `123-45-6789` and `123456789` are equivalent.
#### KYB
```bash theme={null}
curl --request POST \
--header 'accept: application/json; charset=utf-8' \
--header 'Idempotency-Key: ' \
--header 'x-api-key: ' \
--url 'https://api.sandbox.iron.xyz/api/customers//identifications/v2' \
--data-raw '{
"type": "Business",
"phone": "+12125559876",
"ip_address": "203.0.113.42",
"company_data": {
"company_info": {
"company_name": "Acme Corp",
"registration_number": "7086953",
"country": "US",
"incorporation_date": "2021-01-15",
"legal_address": "123 Commerce St, Suite 100, Anytown, CA 12345",
"postal_address": "PO Box 456, Anytown, CA 12345",
"address": {
"street_1": "123 Commerce St",
"street_2": "Suite 100",
"city": "Anytown",
"state": "CA",
"zip_code": "12345",
"country": "US"
},
"email": "info@acmecorp.com",
"tax_identification_number": "12-3456789",
"website": "https://www.acmecorp.com",
"company_type": "LimitedLiabilityCompany"
},
"beneficiaries": [
{
"roles": ["UltimateBeneficialOwner", "Director"],
"share_percentage": "50.00",
"attesting_party": true,
"attested_at": "2026-07-01T12:00:00Z",
"beneficiary_info": {
"type": "Person",
"first_name": "John",
"last_name": "Smith",
"date_of_birth": "1990-01-01",
"email_address": "john@acmecorp.com",
"phone_number": "+12125551234",
"tax_identification_number": "123-45-6789",
"tax_jurisdiction_country_code": "US",
"birth_place": {
"place": "Anytown",
"state": "CA",
"country": "US"
},
"address": {
"street_1": "456 Elm St",
"city": "Anytown",
"state": "CA",
"zip_code": "12345",
"country": "US"
},
"identity": {
"identity_country_code": "US",
"identity_document_type": "DriverLicense",
"identity_document_front": "",
"identity_document_back": "",
"selfie": "",
"selfie_collected_at": "2026-07-01T12:00:00Z",
"nationality": "US"
}
}
}
],
"documents": [
{
"type": "IncorporationCert",
"country": "US",
"file": ""
},
{
"type": "IncorporationArticles",
"country": "US",
"file": ""
}
]
},
"questionnaire": {
"type": "Generic",
"planned_activities": {
"purpose_and_intended_use": "ReceivingBusinessIncome",
"estimated_monthly_volume": "FiftyToOneHundredThousand",
"estimated_monthly_transactions": "TenToTwentyFive",
"sends_funds_on_customer_behalf": false
},
"source_of_funds": {
"source_of_funds": "BusinessRevenue",
"source_of_funds_description": "Revenue from enterprise analytics subscriptions"
},
"business_details": {
"industry_type": "InformationTechnologyAndSoftware",
"business_model_description": "SaaS platform providing analytics tools to enterprise clients across finance and operations teams",
"main_business_countries": ["US", "GB"],
"has_licenses_or_registrations": true,
"license_details": "Registered as a data controller with the UK Information Commissioner's Office"
}
}
}'
```
##### Sole proprietor variants
This simplified variant applies to **non-US sole proprietorships only**. For a non-US sole proprietorship, set `company_type` to `"SoleProprietorship"` and `questionnaire.type` to `"SoleProp"`; the proprietor is submitted as a single beneficiary with the `UltimateBeneficialOwner` role at 100% share. See [Sole Proprietor Onboarding](/kyb#sole-proprietor-onboarding) for the full data breakdown. A US sole proprietorship follows the standard business flow shown above for documents, the attesting party, and the `Generic` questionnaire — but is still structurally a single owner: its one root beneficiary holds `UltimateBeneficialOwner` (100%), `Director`, and the attesting party all at once, not three separate people. See [Sole Proprietor Onboarding](/kyb#sole-proprietor-onboarding).
```json theme={null}
{
"type": "Business",
"phone": "+442071234567",
"ip_address": "203.0.113.42",
"company_data": {
"company_info": {
"company_name": "Jane's Consulting Ltd",
"alternative_names": null,
"registration_number": "SC123456",
"country": "GB",
"incorporation_date": "2020-01-15",
"legal_address": "10 Downing Street, London, SW1A 2AA",
"postal_address": "10 Downing Street, London, SW1A 2AA",
"address": {
"street_1": "10 Downing Street",
"street_2": null,
"city": "London",
"state": "London",
"zip_code": "SW1A 2AA",
"country": "GB"
},
"email": "jane@janesconsulting.co.uk",
"tax_identification_number": "GB123456789",
"website": "https://janesconsulting.co.uk",
"company_type": "SoleProprietorship"
},
"beneficiaries": [
{
"roles": ["UltimateBeneficialOwner"],
"share_percentage": 100,
"beneficiary_info": {
"type": "Person",
"first_name": "Jane",
"middle_name": null,
"last_name": "Doe",
"date_of_birth": "1985-06-15",
"tax_identification_number": "GB987654321",
"address": {
"street_1": "10 Downing Street",
"street_2": null,
"city": "London",
"state": null,
"zip_code": "SW1A 2AA",
"country": "GB"
},
"email_address": "jane@janesconsulting.co.uk",
"phone_number": "+442071234567",
"tax_jurisdiction_country_code": "GB",
"birth_place": { "place": "Manchester", "state": "England", "country": "GB" },
"identity": {
"identity_country_code": "GB",
"identity_number": null,
"identity_document_type": "Passport",
"identity_document_front": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mP4z8BAEmIY1TCqYfhqAACQ+f8B8u7oVwAAAABJRU5ErkJggg==",
"identity_document_back": null,
"nationality": "GB"
},
"proof_of_address": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFUlEQVR42mNgYPhPIhrVMKph2GoAAJLb/wHQPqPSAAAAAElFTkSuQmCC"
}
}
],
"documents": [
{
"type": "IncorporationCert",
"country": "GB",
"file": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mP4f+A0SYhhVMOohuGrAQDBXYofzKX3pgAAAABJRU5ErkJggg=="
}
]
},
"questionnaire": {
"type": "SoleProp",
"planned_activities": {
"purpose_and_intended_use": "ReceivingBusinessIncome",
"estimated_monthly_volume": "TenToTwentyFiveThousand",
"estimated_monthly_transactions": "LessThanTen"
},
"source_of_funds": { "source_of_funds": "BusinessRevenue" },
"business_details": {
"industry_type": "LegalAndProfessionalServices",
"business_model_description": "Independent consulting services for small businesses, focused on operations and financial strategy",
"main_business_countries": ["GB"],
"estimated_annual_revenue": "Under100K"
}
}
}
```
See [Create Customer Identification V2](/reference-sandbox/customer/create-customer-identification-v2) for more details on each parameter.
### Duplicate submissions
`Person` and `Business` submissions are de-duplicated before any applicant reaches the provider. MoonPay Enterprise hashes the submitted payload and rejects the request with `409 Conflict` when another identification under your partner account carries the same hash and a status outside `Rejected`, `Expired`, and `Archived`.
The scope is your whole partner account, not the single customer. An identification on a different customer with identical details still blocks the submission.
The response body names the record that blocked you:
```json theme={null}
{
"message": "An identification with the same details already exists for this partner",
"existing_customer_id": "c5f2e8a1-3b4d-4e6f-8a9b-1c2d3e4f5a6b",
"existing_identification_id": "9a667227-6992-4c6b-951e-63765d5d8f33",
"external_id": "cust-1042"
}
```
To resubmit, archive the identification named by `existing_identification_id` with [Archive an identification](/reference/customer/archive-an-identification), then send the payload again. Archiving clears the block because `Archived` is not a live status.
A `409` with no body is a different error. That one comes from the `Idempotency-Key` header, not from duplicate detection, and archiving will not clear it. Check the response body before you archive anything. See [Idempotency](/idempotency).
### KYC Questionnaire Fields
The `kyc_questionnaire` object is required on every `Person` submission. These are the accepted values for each field.
| Field | Values |
| - | - |
| `employment_status` | `Employed`, `SelfEmployed`, `Unemployed`, `Retired`, `Student` |
| `yearly_gross_income` | `LessThan20000`, `From20001To30000`, `From30001To40000`, `From40001To50000`, `From50001To60000`, `From60001To70000`, `From70001To80000`, `From80001To90000`, `From90001To100000`, `From100001To110000`, `From110001To120000`, `From120001To130000`, `From130001To140000`, `From140001To150000`, `From150001To200000`, `From200001To500000`, `MoreThan500000`. `MoreThan150000` is deprecated: submissions using it are coerced to `From150001To200000`. Use one of the three bands above `150000` instead. |
| `source_of_wealth` | `Salary`, `Savings`, `Investments`, `CryptoTrading`, `SaleOfAssetsOrSettlement`, `Pension`, `CompanyProfits`, `SoleProprietorIncome`, `RentalIncome`, `Inheritance`, `Other` |
| `expected_monthly_transaction_count` | `LessThan5`, `Between5And10`, `MoreThan10` |
| `expected_monthly_transaction_volume` | `LessThan500`, `MoreThan500LessThan2000`, `MoreThan2000` |
## Triggering EDD
To proactively trigger Enhanced Due Diligence for a customer (e.g. high-volume or high-risk jurisdiction), include the `edd_questionnaire` object in your Person params. This initializes the identification with the full EDD flow, including Source of Wealth proof collection. The upload field is named `source_of_funds_proof` for legacy reasons: it carries the Source of Wealth document. See [Source of Wealth](/source-of-wealth) for accepted documents.
```json theme={null}
{
"type": "Person",
"first_name": "John",
"last_name": "Smith",
"full_name": "John Smith",
"edd_questionnaire": {
"occupation": "FinancialServices",
"approximate_net_worth": "Between100001And300000",
"source_of_funds_proof": ""
}
}
```
| Field | Values |
| - | - |
| `occupation` | `Agriculture`, `ArtsAndEntertainment`, `Construction`, `Education`, `FinancialServices`, `InformationAndTechnology`, `Retail`, `RealEstate`, `Other`, `BusinessOwner`, `Healthcare`, `Industrial`, `LegalServices`, `PublicSector`, `SeniorManagement` |
| `approximate_net_worth` | `UpTo25000`, `Between25001And50000`, `Between50001And100000`, `Between100001And300000`, `Between300001And500000`, `Between500001And1000000`, `Over1000001` |
| `source_of_funds_proof` | Base64-encoded Source of Wealth proof (jpg, png, pdf, doc, docx, xls, xlsx, odt, ods, txt). Max 10 MB. |
When `edd_questionnaire` is provided, `with_edd` is set to `true` on the identification response. See [Tracking EDD Status](/onboarding#tracking-edd-status) for how to interpret the identification status with EDD.
EDD is only available for individual customers (KYC). Business customers (KYB) are not supported.
# Source of Wealth
Source: https://dev.enterprise.moonpay.com/source-of-wealth
When source of wealth (SOW) is required, accepted documents by source, and acceptance criteria.
To comply with financial regulations, MoonPay must verify an individual customer's source of wealth (SOW) in certain situations. SOW proof is required to upgrade a customer to Tier 2 and for Enhanced Due Diligence. See [Limits and minimums](/limits-and-minimum) and [Enhanced due diligence](/enhanced-due-diligence).
In the API, the customer's declared source is the `source_of_wealth` field of the KYC questionnaire, and the uploaded document goes in `source_of_funds_proof` (a legacy field name that carries the SOW document). See [KYC/KYB Sharing](/sharing) for the request shape.
## Before your customer uploads a document
To avoid rejection and speed up verification, make sure the customer's documents:
* Are clear, complete, and unaltered (no cropping or editing)
* Show all four corners and all relevant pages
* Include the customer's full name
* Display a balance or financial value when required
* Are recent (dated within the last 3 months unless otherwise specified)
## Accepted documents by source
| Funds source | Document(s) | Acceptance criteria |
| - | - | - |
| Salary |
A payslip from the customer's employer, or
One or more bank statements demonstrating two salary payments from the customer's employer
|
Dated within the last 3 months
Contains a balance or financial value
Document must include the customer's name
Payslip (optional) must be issued by the customer's employer and clearly show the customer's salary and name
|
| Savings |
An account statement from the financial institution where the savings are held
|
Dated within the last 3 months
Contains a balance or financial value
Document must include the customer's name
|
| Inheritance |
A signed letter from a licensed solicitor, executor, or estate trustee
|
Contains financial value of inheritance
Document must include the customer's name
|
| Investments |
A statement of the customer's investment portfolio
|
Dated within the last 3 months
Contains a balance or financial value
Document must include the customer's name
|
| Company Profits |
A dividend distribution certificate
|
Dated within the last 1 year
Contains a balance or financial value
Document must include the customer's name
|
| Cryptocurrency Trading |
Screenshot of the accounts the customer used, with their balances and the customer's name and address, and
If the trading account does not have a name or address (because it's a DEX or other non-custodial platform), then also provide a bank statement demonstrating two cryptocurrency transactions with the same platform the customer provided a screenshot from
|
Both uploads (where required) must contain a balance or financial value
Document must include the customer's name
Bank statements must meet the same criteria as Salary above
|
| Self-Employed |
Invoices from the last 2 months and bank statement demonstrating two or more income payments, or
Latest tax statement/tax return
|
Dated within the last 3 months
Contains a balance or financial value
Document must include the customer's name
|
| Pension |
Pension statement dated within the last 12 months, or
Bank statement demonstrating two or more pension payments dated within the last 3 months
|
Contains a balance or financial value
Document must include the customer's name
|
| Rental Income |
A rental agreement
|
Valid for a minimum of 3 months from submission
Contains the rental amount
Document must include the customer's name
|
| Sale of Property |
A sale contract
|
Contains the sale amount
Document must include the customer's name
|
| Sale of Company |
A sale contract, and
A screenshot of the register of companies detailing the sale/change of ownership
|
Contains a balance or financial value
Contains the customer's name and information sufficient to confirm their identity matches the details on their MoonPay account
|
## Why documents are rejected
Documents are often rejected because key details are missing or unclear. Common causes:
* Name not visible
* Date outside the allowed timeframe
* Missing financial values
* Blurry or cropped pages
* Pension statement issued annually but not within the past 12 months
* Incomplete bank statements showing pension deposits
If a submission is rejected, review the acceptance criteria above and have the customer resubmit clearer, complete documents.
If the customer is submitting pension-related documents, provide consecutive statements for irregular deposit periods (for example, mid-month to mid-month cycles) so all pension deposits are clearly shown.
## Additional details
All submitted documents must:
* Be unedited and unaltered
* Display clear, readable information
* Show the customer's full name and, where relevant, address and financial transactions
* Be recent and meet the date requirements listed in the table above
# Stable to Stable
Source: https://dev.enterprise.moonpay.com/stable-to-stable
Setup a stables swap channel. Send Stablecoin 1 and receive Stablecoin 2 across chains.
Set up a **swap channel**: a persistent deposit address that accepts one stablecoin and delivers another, on the same chain or across chains.
Examples:
* A channel that turns incoming **USDC on Solana into EURC on Base**
* A channel that turns incoming **USDT on Ethereum into USDC on Arbitrum**
This lets applications add bridging and automatic conversion without users stepping through multi-step UIs. Any wallet or frontend can integrate it with one API call.
## How it works
The user gets a **deposit address** on Solana owned by MoonPay Enterprise
An `autoramp` ties that address to the destination:
i. it turns all incoming USDC into EURC on Base
ii. and delivers it to the user's connected wallet
The user sends 1000 USDC to the deposit address
As soon as funds arrive, MoonPay Enterprise converts them to EURC on Base and delivers them to the destination wallet
## Two ways to run a channel
| | Standing autoramp | Exact-out quote |
| - | - | - |
| **You specify** | The route only | The exact output amount |
| **Rate** | Current mid-market rate at deposit | Locked for the quote window (max 10 minutes) |
| **Best for** | Continuous conversion, bridging, treasury sweeps | Delivering a precise token amount |
| **Deposit matching** | Any supported deposit converts | Deposit must match the quote's `amount_in` |
Most swap channels use a standing autoramp, because the user decides how much to send. Both are shown below.
## 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.
```bash theme={null}
export API_KEY=""
export BASE_URL="https://api.sandbox.iron.xyz"
```
Your customer must have `Active` status: terms signed and [identification](/onboarding) (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, and subscribe to the `customer_fiat_abilities` [webhook](/webhooks) to track rail changes instead of polling. See [Onramp](/onramp#onboard-a-customer-from-scratch) for a full onboarding walkthrough and [Tracking ability changes](/onboarding#tracking-ability-changes) for the webhook.
Register the destination wallet via [Crypto Addresses](/crypto-addresses) for [Travel Rule](/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.
Either create a standing autoramp, or request an exact-out quote and create a quote-source autoramp. Both are shown below.
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:
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/autoramp/$AUTORAMP_ID" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '"Approved"'
```
In production, an autoramp reaches `Approved` on its own once its deposit account is provisioned and verified.
## Automation route: standing autoramp
Create an autoramp that converts USDC on Solana to EURC on Base at the current mid-market rate.
### Request
```bash Shell theme={null}
AUTORAMP_ID=$(curl -s -X POST "$BASE_URL/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{
"source_currencies": [{
"type": "Crypto",
"token": "USDC",
"blockchain": "Solana"
}],
"destination_currency": {
"type": "Crypto",
"token": "EURC",
"blockchain": "Base"
},
"recipient_account": {
"type": "Crypto",
"chain": "Base",
"address": "0xaf77d065e77c8cC2239327C5EDb3A432268e5831"
},
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"source_is_third_party": false
}' | jq -r '.id')
echo "$AUTORAMP_ID"
```
Capturing the ID into `$AUTORAMP_ID` lets you paste the remaining commands on this page straight into the same shell.
`POST /api/autoramps` requires an `Idempotency-Key` header. Use a unique UUID per request to prevent duplicate autoramps.
The request uses `chain` in `recipient_account`, but the response returns the same value as `blockchain` in `recipient`. Both are correct: map between them when comparing the request and response.
### Response
```json 201 Authorized theme={null}
{
"id": "d4e3c2b1-a9f8-7654-3210-fedcba987654",
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"status": "Authorized",
"kind": "Swap",
"source": "Standalone",
"source_currencies": [
{
"type": "Crypto",
"token": "USDC",
"blockchain": "Solana"
}
],
"destination_currency": {
"type": "Crypto",
"token": "EURC",
"blockchain": "Base"
},
"recipient": {
"type": "Wallet",
"blockchain": "Base",
"address": "0xaf77d065e77c8cC2239327C5EDb3A432268e5831"
},
"is_third_party": false,
"batch_payout": false,
"fee_profile_id": "9b2e7c14-5f3a-4d8b-b1c6-2a4e6f8d0c12",
"quotes": [],
"deposit_rails": [],
"created_at": "2025-01-20T14:23:45Z"
}
```
### 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](/crypto-addresses) returns `400`:
```json 400 Bad Request theme={null}
"Recipient wallet not verified for customer"
```
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](/onboarding#ability-status)).
`deposit_rails` is empty at `Authorized`. It's populated once the autoramp reaches `DepositAccountAdded`, but the deposit account isn't verified yet. Poll `GET /api/autoramps/{id}` or subscribe to webhooks until `status = Approved` before sharing deposit details with end users. See [Autoramp Status](/autoramp-status).
Once the autoramp reaches `Approved`, `deposit_rails` holds the verified deposit wallet address. Share it with the user along with the supported assets for that wallet (e.g. EURC, USDC). Non-supported assets sent to the wallet are returned to sender.
**Shared EVM deposit address.** An autoramp gets one deposit address per source chain, and every EVM chain on the same autoramp shares one address, so the EVM entries in `deposit_rails` repeat the same string. Only the token and chain combinations listed in `source_currencies` are converted: a deposit arriving on an EVM chain you did not declare is not swept, even though the address matches. Declare every chain you want to accept when you create the autoramp. Non-EVM chains such as Solana get their own distinct address.
## Locked rate: exact-out quote
Use this when you need a precise amount of the destination token. Stablecoin to stablecoin pairs lock for up to **10 minutes**.
`GET /api/autoramps/quote`
Set `amount_out` for the exact destination amount, or `amount_in` to price a known source amount. Never set both. `recipient_account_id` is the UUID of the registered crypto address.
```bash theme={null}
curl -X GET "$BASE_URL/api/autoramps/quote?customer_id=&source_currency_code=USDC&source_currency_chain=Solana&destination_currency_code=EURC&destination_currency_chain=Base&recipient_account_id=&amount_out=1000&rate_lock_duration_minutes=10&rate_expiry_policy=Return&expiry_in_hours=1&is_third_party=false" \
-H "Accept: application/json; charset=utf-8" \
-H "X-API-Key: $API_KEY"
```
The response returns `amount_in` (what the user must send), `amount_out`, an itemised `fee` breakdown, `rate`, `rate_lock_valid_until`, and a `signature`.
`POST /api/autoramps`
Submit the signed quote payload verbatim.
```bash theme={null}
AUTORAMP_ID=$(curl -s -X POST "$BASE_URL/api/autoramps" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '' | jq -r '.id')
```
The quote is digitally signed. Any modification, including changing values or omitting fields, causes the request to fail.
Show your customer the `deposit_rails` address and the exact `amount_in` figure. A deposit only matches a quote when its `amount_in` value and source currency match. Deposits that match no active quote, or only an expired one, are returned to sender.
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, and always execute at the current rate. See [Quotes](/quotes#attach-a-new-quote-to-an-existing-autoramp).
## 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.
`POST /api/sandbox/transaction` builds a transaction directly from the route, reusing its currencies, customer, and recipient.
```bash theme={null}
TRANSACTION_ID=$(curl -s -X POST "$BASE_URL/api/sandbox/transaction" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d "{\"autoramp_id\": \"$AUTORAMP_ID\", \"amount\": \"1000\"}" | jq -r '.id')
```
The response carries `amount_in`, `amount_out`, `currency_in`, `currency_out`, and `state`. See [Sandbox](/sandbox#create-a-sandbox-transaction) for the optional `fee`, `fx_rate`, and `initial_state` fields.
The simulated transaction starts in `Pending`. Advance it to trigger the rest of the status webhooks.
```bash theme={null}
curl -X PUT "$BASE_URL/api/sandbox/transaction/$TRANSACTION_ID/state" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-API-Key: $API_KEY" \
-d '{ "state": "Completed" }'
```
Your endpoint should have received two `transaction_status` events: `ConversionInProgress` when you created the transaction, then `Completed` when you advanced its state. The sandbox writes the state you ask for straight onto the transaction, so it does not send the `transaction` event or the intermediate `FundsReviewInProgress` and `PayoutInProgress` statuses that a live deposit produces.
Read `transaction_status`, not the deprecated `status` field, for the current state. Payload shapes and the full status table are in [Monitoring payments](/onramp#monitoring-payments); signature verification is in [Webhooks](/webhooks).
Once the destination token is delivered, `transaction_hash` contains the on-chain hash.
## Simulate it in the Dashboard
You can run this same flow without writing a single API call. The [Partner Dashboard](https://app.sandbox.iron.xyz) covers every step, from creating the route to settling a simulated deposit.
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 .
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.
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.
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.
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
| | Sandbox | Production |
| - | - | - |
| API base URL | `https://api.sandbox.iron.xyz` | `https://api.iron.xyz` |
| Autoramp approval | You call `PUT /api/sandbox/autoramp/{id}`, or set **Authorized** in the Dashboard | Automatic once the deposit account is provisioned and verified |
| Deposits | Simulated via `POST /api/sandbox/transaction` or **New Transaction** in the Dashboard | Real tokens arriving at the deposit address |
| Quote rates | Simulated, but signatures work identically | Live pricing |
Remove every `/api/sandbox/*` call from your integration. Those endpoints do not exist in production.
## What to read next
Which tokens and chains are supported as sources and destinations
Full quote reference: expiry policies, multi-currency input, attaching quotes over time
Register self-hosted or hosted wallets for Travel Rule compliance
Every status an autoramp moves through, and what unblocks each one
Signature verification, payload schemas, and retry behaviour
Status mapping, settlement times, and the polling API
# Stables and Blockchains
Source: https://dev.enterprise.moonpay.com/stablecoins-and-blockchains
Supported blockchains and stablecoins including Ethereum, Solana, Arbitrum, Base, Polygon, and tokens like USDC, USDT, EURC.
## Blockchain and Stablecoin Support Overview
The table below outlines the available blockchains and their compatible stablecoins.
| Blockchain | Live |
| - | - |
| **Solana** | USDC, EURC, USDT |
| **Ethereum** | USDC, EURC, USDT, PYUSD, USDe |
| **Polygon** | USDC, USDT |
| **Base** | USDC, EURC |
| **Arbitrum** | USDC, PYUSD |
| **Tron** | USDT |
| **Avalanche** | USDe |
| **Tempo** | pathUSD, USDC, USDT0 |
| **Stellar** | USDC |
| **Plasma** | USDT0 |
| **Monad** | mUSD |
For a machine-readable list, call [`GET /api/cryptocurrencies`](/reference/currencies/list-all-available-crypto-currencies). It returns one entry per blockchain and stablecoin pair, including the contract address, scoped to what your account has enabled.
Stellar supports onramps only, so fiat to USDC on Stellar. An offramp or stable to stable leg on Stellar returns 400 with `"Stellar is currently supported only for onramps (fiat -> Stellar)"`. Registering a Stellar wallet or creating a Stellar Autoramp before your account is enabled returns 400 with `"Stellar is not enabled for this partner"`.
USDC and EURC are issued by Circle. USDT and USDT0 are issued by Tether. USDe is issued by Ethena. PYUSD is issued by Paxos for PayPal. mUSD and pathUSD are issued by Bridge. USDC on Tempo is bridged (Stargate), not Circle's native issuance.
Some tokens carry geographic restrictions:
* PYUSD is not available to customers in the UK or Canada.
* pathUSD and USDC on Tempo are blocked for customers in the EEA, Canada, and New York.
* USDT0 on Tempo and Plasma is available only to customers outside the UK, US, Canada, EEA, Australia, and the BVI.
Restricted tokens are hidden from a customer's available currencies, and a quote or Autoramp that uses one returns 400 with `"This token is not available in the jurisdiction"`.
mUSD on Ethereum is available in sandbox only. It is not yet live in production.
USDT0 on Tempo has no Moderato testnet deployment, so it is not available in sandbox. pathUSD and USDC on Tempo both work in sandbox. USDT0 on Plasma is available in sandbox to every partner.
### MoonPay-Issued Stablecoins
These stablecoins are issued by MoonPay through the [Issuance product](/issuance/create-your-stablecoin). All three are live. Approved partners can directly mint and redeem supply through the MoonPay Enterprise API. ctUSD and XO mint and redeem against fiat reserves. PYUSDx mints and redeems against PYUSD reserves, so the flow is on-chain crypto-to-crypto end to end. See [PYUSDx](/issuance/pyusdx) for details.
| Stablecoin | Partner | Chain |
| - | - | - |
| **ctUSD** | Citrea | Citrea |
| **XO** | Exodus | Solana |
| **PYUSDx** | PayPal and M0 | Ethereum, Arbitrum |
ctUSD is blocked for customers in the EEA, Canada, the BVI, New York, and Colorado. XO is available only to US customers outside New York and Colorado. A quote or Autoramp that uses a blocked token returns 400 with `"This token is not available in the jurisdiction"`.
Mint and redeem windows for USD wire, SWIFT, and ACH rails are Monday to Friday, 9:00 PM ET (previous day) to 5:00 PM ET. Requests submitted outside the window are processed at the start of the next window. See the [Issuance FAQ](/issuance/faq) for full details.
PYUSDx tokens (and any digital tokens issued on the PYUSDx platform) are not PayPal USD issued by Paxos Trust Company, NA or PayPal, are not a PayPal product or service, and are not otherwise affiliated with PayPal or Paxos Trust Company, NA. PYUSDx is not a regulated stablecoin and its regulatory classification varies by jurisdiction. Availability varies by jurisdiction and issuance model.
### Contract Addresses
| Token | Blockchain | Contract Address |
| - | - | - |
| **ctUSD** | Citrea | `0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D` |
| **EURC** | Base | `0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42` |
| **EURC** | Ethereum | `0x1abaea1f7c830bd89acc67ec4af516284b1bc33c` |
| **EURC** | Solana | `HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr` |
| **mUSD** | Ethereum | `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` |
| **mUSD** | Monad | `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` |
| **pathUSD** | Tempo | `0x20c0000000000000000000000000000000000000` |
| **PYUSD** | Arbitrum | `0x46850ad61c2b7d64d08c9c754f45254596696984` |
| **PYUSD** | Ethereum | `0x6c3ea9036406852006290770BEdFcAbA0e23A0e8` |
| **PYUSDx** | Arbitrum | `0xebdb0942ce16386ab90718c7bd10c91cdb66b14d` |
| **PYUSDx** | Ethereum | `0xebdb0942ce16386ab90718c7bd10c91cdb66b14d` |
| **USDC** | Arbitrum | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` |
| **USDC** | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| **USDC** | Ethereum | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
| **USDC** | Polygon | `0x3c499c542cef5e3811e1192ce70d8cc03d5c3359` |
| **USDC** | Solana | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| **USDC** | Stellar | `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` |
| **USDC** | Tempo | `0x20c000000000000000000000b9537d11c60e8b50` |
| **USDT** | Ethereum | `0xdac17f958d2ee523a2206206994597c13d831ec7` |
| **USDT** | Polygon | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` |
| **USDT** | Solana | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` |
| **USDT** | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` |
| **USDT0** | Plasma | `0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb` |
| **USDT0** | Tempo | `0x20c00000000000000000000014f22ca97301eb73` |
| **USDe** | Avalanche | `0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34` |
| **USDe** | Ethereum | `0x4c9EDD5852cd905f086C759E8383e09bff1E68B3` |
| **XO** | Solana | `xoUSDq85Rjsb6SbUwJyreFgeWQvxdkT7R3c3g7s6p5Y` |
# Support
Source: https://dev.enterprise.moonpay.com/support
We’re here to help with any questions you have or support you may need. Below are the key ways to get support based on your needs:
| Support Required | Contact Method |
| - | - |
| Data and Privacy Inquiries | If you have any questions regarding how we handle your data, please refer to our Privacy Policy or contact us at [compliance@iron.xyz](mailto:compliance@iron.xyz). |
| API and Developer Support | For developers with questions about our API, please refer to our Guide in its totality or reach out to us at [developers@iron.xyz](mailto:developers@iron.xyz). |
| Funds or Account Queries | If you have any questions or concerns regarding your funds, please contact us at [developers@iron.xyz](mailto:developers@iron.xyz). |
| Complaints | To submit a complaint, please contact us directly at [complaints@iron.xyz](mailto:complaints@iron.xyz). |
| Onboarding | Once we’re working together, we will set up a shared Slack channel for streamlined communication during integration. |
| Other | For anything else not explicitly listed above, please contact us at [developers@iron.xyz](mailto:developers@iron.xyz). |
# TIN Format Validation
Source: https://dev.enterprise.moonpay.com/tin-format-validation
Expected tax identification number (TIN) format per country, how separators and whitespace are handled, and the errors a malformed TIN returns.
MoonPay Enterprise checks the format of every tax identification number (TIN) you submit for an individual customer. The format depends on `tax_residence_country`, not on the customer's address or nationality. Match the format below and the TIN passes on the first attempt.
The check runs on two endpoints:
* `POST /api/customers/{id}/identifications/v2` with a `Person` submission. See [Create Customer Identification](/reference/customer/create-customer-identification-v2).
* `PATCH /api/customers/{id}/identification-data`, used to backfill a TIN on an existing customer. See [Update Customer Identification Data](/reference/customer/update-customer-identification-data).
Beneficiary TINs on a `Business` submission aren't checked against this table.
On a `Person` submission whose tax residence is a jurisdiction that doesn't issue tax IDs, the TIN isn't checked. Any value you send is ignored and `identity.identity_number` is used instead. Tax residence is `tax_residence_country`. If you send neither a TIN nor `tax_residence_country`, `address.country` is used. A TIN without `tax_residence_country` still needs it, as described below. US persons (US address, US nationality, or a US-issued identity document) are never covered by this.
## Tax residence country
Send `tax_residence_country` as an ISO 3166-1 alpha-2 code (`DE`, `US`) whenever you send `tax_identification_number`.
* On a `Person` submission with API version `2026-10-15` or later, it's always required, with or without a TIN. A missing one returns [`tin_required`](/errors#tin_required).
* On a `Person` submission with an earlier version, you can omit it only when `identity.identity_country_code` is `US`. The TIN is then checked against the US format.
* On `PATCH /api/customers/{id}/identification-data`, it's always required with a TIN.
## Formatting rules
* **Whitespace is ignored.** Spaces anywhere in the value, including between groups of digits, are removed before the check.
* **Separators are accepted.** Dashes (`-`), periods (`.`), and slashes (`/`) used between groups pass, so a US SSN like `123-45-6789` is accepted the same as `123456789`.
* **Finland is the exception.** The century marker is part of the format, so `010101-123A` passes and `0101011234` fails. See the table below.
* **Letters are case-insensitive.** Wherever a format allows a letter, upper and lower case both pass.
## Country formats
| Country | Format | Example |
| - | - | - |
| Austria (AT) | 9 digits | `123456789` |
| Belgium (BE) | 11 digits | `12345678999` |
| Brazil (BR) | 11 digits | `12345678900` |
| Bulgaria (BG) | 10 digits | `1234567890` |
| Croatia (HR) | 11 digits | `12345678901` |
| Cyprus (CY) | 8 digits + 1 letter | `12345678A` |
| Czech Republic (CZ) | 9-10 digits | `1234567890` |
| Denmark (DK) | 10 digits: day (`01`-`31`) + month (`01`-`12`) + 6 digits | `0101901234` |
| Estonia (EE) | 11 digits, starting `1`-`8` | `12345678901` |
| Finland (FI) | 6 digits + century marker + 3 digits + 1 digit or letter. The century marker is one of `-`, `+`, `A`-`F`, `U`-`Y`. | `010101-123A` |
| France (FR) | 13 digits starting `0`-`3`, or 7 digits starting `1`-`2`, or 5 digits + 1 letter | `0123456789012` |
| Germany (DE) | 11 digits | `12345678901` |
| Greece (GR) | 9 digits | `123456789` |
| Hungary (HU) | 10 digits, starting `8` | `8123456789` |
| Iceland (IS) | 10 digits, starting `0`-`3` | `0123456789` |
| Ireland (IE) | 7 digits + 1-2 letters | `1234567A` |
| Italy (IT) | 6 letters + 2 digits + 1 letter + 2 digits + 1 letter + 3 digits + 1 letter | `ABCDEF12A01A012A` |
| Japan (JP) | 12 digits | `123456789012` |
| Jersey (JE) | 10 digits, or 2 letters + 6 digits + 1 letter | `0123456789` or `AB123456A` |
| Latvia (LV) | 11 digits, starting `0`-`3` | `12345678901` |
| Liechtenstein (LI) | 4-12 digits | `123456789012` |
| Lithuania (LT) | 11 digits, starting `1`-`6` | `12345678901` |
| Luxembourg (LU) | 13 digits, starting `19` or `20` | `1912345678901` |
| Malta (MT) | 7 digits + 1 of `M`, `G`, `A`, `P`, `L`, `H`, `B`, `Z`, or 9 digits | `1234567A` |
| Netherlands (NL) | 9 digits | `123456789` |
| Norway (NO) | 11 digits, starting `0`-`7` | `12345678901` |
| Poland (PL) | 10-11 digits | `1234567890` |
| Portugal (PT) | 9 digits, starting `1`, `2`, `3`, or `45` | `123456789` |
| Romania (RO) | 13 digits starting `1`-`8`, or `9000` + 9 digits | `1234567890123` |
| San Marino (SM) | 2-9 digits | `123456789` |
| Slovakia (SK) | 9-10 digits | `123456789` |
| Slovenia (SI) | 8 digits, starting `1`-`9` | `12345678` |
| South Africa (ZA) | 10 digits, starting `0`, `1`, `2`, `3`, or `9` | `0123456789` |
| Spain (ES) | 8 digits + 1 letter, or 1 of `K`, `L`, `M`, `X`, `Y`, `Z` + 7 digits + 1 letter | `12345678Z` or `Z1234567Z` |
| Sweden (SE) | 10 digits: 2 digits + month (`01`-`12`) + 6 digits | `9910123456` |
| United Kingdom (GB) | 10 digits, or 2 letters + 6 digits + 1 of `A`-`D`, or 2 digits + 1 letter + 5 digits | `0123456789` or `AB123456A` |
| United States (US) | 9 digits | `123456789` |
### Other countries
For a `tax_residence_country` not in the table, the TIN must:
* Contain only letters, digits, `+`, and `-` (plus the whitespace and separators from [Formatting rules](#formatting-rules))
* Include 4-20 digits. Letters and punctuation don't count toward that total.
## Errors
A TIN that doesn't match the format returns `400`. The message names the country and an example of the expected format.
On a `Person` submission under `X-API-Version: 2026-08-01` or later, the error code is [`invalid_request`](/errors#invalid_request):
```json theme={null}
[
{
"code": "invalid_request",
"message": "Tax identification number for country DE must match format: 12345678901",
"docs_url": "https://docs.iron.xyz/errors#invalid_request",
"beneficiary_index": null
}
]
```
Earlier API versions, and `PATCH /api/customers/{id}/identification-data`, return the same message as a plain string:
```json theme={null}
"Tax identification number for country DE must match format: 12345678901"
```
A TIN without `tax_residence_country` returns [`tin_required`](/errors#tin_required) on a `Person` submission, and this plain string on `PATCH /api/customers/{id}/identification-data`:
```json theme={null}
"Tax residence country is required when a tax identification number is provided"
```
Run the same format check in your own UI before you submit. Your customer fixes a typo on the spot instead of your integration handling a `400`.
If a TIN is rejected and you believe it matches the format for its country, [contact support](/support) with the customer ID and the country. Don't send the full TIN.
# Transaction status
Source: https://dev.enterprise.moonpay.com/transaction-status
This document outlines how to interpret transaction statuses returned by MoonPay Enterprise.
## Transaction Status Mapping
| **State Type** | **API Status** | **Dashboard Status** | **Description** |
| - | - | - | - |
| Progress | `FundsReviewInProgress` | **Reviewing Funds** | The deposit has been received and is undergoing a brief compliance and eligibility check (e.g., AML screening). This status is rare and typically resolves quickly. |
| Progress | `ConversionInProgress` | **Exchanging Funds** | The deposit has cleared review and is being converted to the configured destination asset. |
| Progress | `PayoutInProgress` | **Processing Payout** | MoonPay Enterprise has sent the funds to the connected banking provider or blockchain. This step can take time depending on external networks. |
| Terminal | `Completed` | **Transaction Complete** | The funds have been fully processed and handed off to the destination. No further actions will occur. |
| Terminal | `Failed` | **Deposit Failed** | The deposit or transfer failed due to a general processing error. |
| Terminal | `RejectedAml` | **Rejected by Compliance** | The deposit failed AML or compliance checks and has been rejected. |
| Terminal | `RejectedFraud` | **Rejected by Fraud** | The deposit failed anti-fraud review and has been rejected. |
| Terminal | `RejectedMinAmount` | **Deposit Below Minimum** | The amount was too low to process and has been rejected. |
## Settlement Times by Payment Method
The time it takes for a transaction to complete depends on the payment rail used. Below are typical settlement times from initiation to completion:
| **Payment Method** | **Typical Settlement Time** | **Notes** |
| - | - | - |
| **SEPA (Instant)** | Near-instant to 10 seconds | For transactions under €100k within the EEA |
| **SEPA (Credit)** | 1-2 business days | Standard for transactions over €100k; usually settles within 24 hours |
| **ACH** | Same day to 1 business day | Same-Day ACH settles the same business day when submitted before cutoff; Standard ACH settles the next business day. See [Currencies and Rails](/currencies-and-rails) for cutoff windows |
| **Wire** | Same day to 1 business day | Faster option for US domestic transfers; often completes within hours if initiated early in the business day |
| **CHAPS** | Same business day | UK domestic transfers; must be initiated before the daily cutoff time |
| **FPS** | Near-instant to 2 hours | UK Faster Payments; typically completes within minutes |
| **SWIFT** | 1-3 business days | International USD transfers; timing depends on correspondent banks and the destination country |
Settlement times are estimates based on typical payment rail processing. Actual times vary depending on:
* Banking partner processing schedules
* Compliance and AML review requirements (see `FundsReviewInProgress` status)
* Time of day and business day cutoffs
* External network delays
For real-time status updates, monitor the transaction status via the API or webhook notifications.
## When No Transaction Appears
A transaction is created once a deposit lands on a Virtual Account or deposit address with a matching autoramp. If a payment was sent and no transaction shows up, the most likely explanation is that the funds have not reached MoonPay Enterprise at all. The sending bank is often still holding the payment: cutoffs, batch windows, and the sender's own compliance checks add hours, and SWIFT adds days. Check the settlement window for the rail above, then confirm with the payer that the payment left their account.
The less common case is a deposit that arrives but matches no autoramp. That happens when a payment-by-reference Virtual Account receives a payment without the expected reference, when a crypto deposit matches no active quote, or when crypto arrives on a chain you did not declare in `source_currencies`. MoonPay Enterprise returns those funds to the sender. No transaction is created, so there is no status to poll and no webhook to receive.
Returned deposits are not exposed through the API. If the payment left the sender's account and never produced a transaction, contact [support](/support) with the amount, currency, date, and reference used.
Crypto deposits worth less than `1 EUR` are treated as dust. They are neither processed nor returned. See [Limits and minimums](/limits-and-minimum).
## API Endpoint
**GET** `/api/autoramp-transactions/ids`
Use this endpoint to retrieve the latest status of specific transactions (up to 100 at a time). Useful for syncing or polling real-time transaction states. At least one transaction ID is required.
```bash theme={null}
curl -X GET "$BASE_URL/api/autoramp-transactions/ids?ids=9a7b6c5d-e3f1-4a2b-8c7d-6e5f4a3b2c1d" \
-H "X-API-Key: $API_KEY"
```
### Response
```json theme={null}
{
"items": [
{
"id": "9a7b6c5d-e3f1-4a2b-8c7d-6e5f4a3b2c1d",
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
"autoramp_id": "7c1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
"status": "PayoutInProgress",
"source_amount": { "amount": "100.50", "currency": { "type": "Crypto", "token": "USDC", "blockchain": "Ethereum" } },
"destination_amount": { "amount": "92.10", "currency": { "type": "Fiat", "code": "EUR" } },
"created_at": "2025-01-20T13:45:23Z"
}
],
"cursor": null,
"prev_cursor": null
}
```
The example above is trimmed to the fields most integrations need. The full `AutorampTransaction` object also returns `kind`, `recipient`, `fee`, `fee_breakdown`, `fee_settlement`, `exchange_rate`, `is_third_party`, `inbound_payment_reference`, `batch_id`, `payment_tracking`, and `payout_crypto_transaction`. The legacy `state` field is deprecated in favor of `status`.
### Error response
Requesting this endpoint with no `ids` query parameter returns `400` as a plain string body:
```json 400 Bad Request theme={null}
"At least one id must be provided"
```
## Webhook Notifications
Refer to the [MoonPay Enterprise Webhooks Guide](/webhooks) for status-based notifications.
# Travel Rule
Source: https://dev.enterprise.moonpay.com/travel-rule
This guide provides an overview of the Travel Rule, explains when it applies, and outlines the specific steps you need to take to ensure compliance.
## What is the Travel Rule?
The Travel Rule is a regulatory requirement aimed at enhancing transparency in cryptocurrency transactions. It mandates the exchange of certain information between the sender (Originator) and receiver (Beneficiary) in cryptocurrency transfers to comply with anti-money laundering (AML) and combating the financing of terrorism (CFT) regulations.
## When Does the Travel Rule Apply?
The Travel Rule applies in the following scenarios:
### Institution-Hosted Wallets
* Applies to **all transfers** to or from institution-hosted wallets.
* Triggered regardless of the transaction amount.
* Ensures proper information exchange between financial institutions.
### Self-Hosted Wallets
* Applies when the transfer amount is **€1,000 or more (or equivalent)**.
* Ownership must be verified via wallet verification.
* For detailed instructions, refer to our [Address Verification Guide](/crypto-addresses).
## What is Required from You?
### 1. Verify Wallets
Before initiating any Autoramp — whether onramp, offramp, or swap — ensure that the appropriate wallets are verified:
* **Self-hosted wallets**: Verify ownership before using the wallet as an input or output for Autoramps. Register with a signed proof-of-ownership message, or, for US and Rest of World customers, by [self-attestation](/crypto-addresses#register-a-self-attested-crypto-address). Both carry identical Travel Rule obligations. See our [Address Verification Guide](/crypto-addresses).
* **Institution-hosted wallets**: Ensure these wallets are registered under your full name and make sure the institution hosting the wallet meets Travel Rule compliance requirements. If you want the Autoramp's output to be a hosted wallet, read 2 and 3.
Wallet verification is required for **all** Autoramp flows — onramp, offramp, and swap. Unregistered wallets will cause transaction failures or delays.
### 2. Exchange Required Information
Most of the information required for Travel Rule compliance has already been collected during the KYB process. When processing transactions, we only require the following additional details:
#### Beneficiary Data
* **First name and last name** (individual or business trading name).
* **Travel address** (the wallet address for the transaction).
Ensuring the accuracy of this information is essential to prevent delays or rejections during transaction processing.
### 3. Follow Implementation Steps
We handle the Travel Rule compliance process for you. Your primary responsibility is to ensure that the beneficiary's travel address is correctly registered for outgoing transactions:
* **As an Output**:
* Register the wallet's travel address using our [Address Verification Guide](/crypto-addresses).
* Confirm that the travel address matches the wallet registered to the beneficiary's name.
By completing these simple steps, you enable seamless transaction processing while ensuring full compliance with Travel Rule regulations.
## Related Guides
# API Versions
Source: https://dev.enterprise.moonpay.com/versioning
Select an API version with the X-API-Version header. Covers the default version, supported versions, per-version OpenAPI specs, and version rejection errors.
MoonPay Enterprise versions the API by date. Send `X-API-Version` to pin the response shapes your integration was built against. Routes never change, so a version switch is a header change.
## Header: `X-API-Version`
Send the version as an ISO date (`YYYY-MM-DD`):
```bash theme={null}
X-API-Version: 2026-07-01
```
Every endpoint under `/api` reads this header. Omit it and you get the default version.
```bash theme={null}
curl "https://api.sandbox.iron.xyz/api/autoramps/f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50" \
-H "X-API-Key: " \
-H "X-API-Version: 2026-07-01"
```
## Supported Versions
See the [API changelog](/changelog) for the full list and what each version added, removed, or renamed.
`GET /spec/versions` returns the same list live, plus the default and the location of each version's OpenAPI spec.
### Versions That Change Behaviour
Most versions change response shapes. `2026-08-01` instead changes the order of onboarding: a customer is created in `SigningsRequired` and signs terms before identification, rather than being created in `IdentificationRequired`.
Because that plays out across webhooks and background work with no request header to read, the version is **pinned to the customer** when you create it and drives that customer's onboarding for its lifetime:
* Customers you created earlier keep the original order, whatever header you send later.
* Sending `2026-08-01` on `POST /api/customers` opts that customer in permanently.
* The header still selects response shapes per request, as it does for every other version.
`2026-10-15` changes request validation only. It rejects the deprecated `intended_use` value `PaymentToFriendsFamilyorOthers` with [`intended_use_deprecated`](/errors#intended_use_deprecated), on `Business` submissions it requires every `UltimateBeneficialOwner` to set `ubo_subtype` (see [Migrating to API Version 2026-10-15](/kyb#migrating-to-api-version-2026-10-15)), and on `Person` submissions it requires `tax_residence_country`. It reads the header per request and isn't pinned to the customer.
A behavioural version cannot be verified from the OpenAPI document alone — the response shapes are identical. Test the status sequence in sandbox before you adopt it.
## The Default Version
A request with no `X-API-Version` header, or an empty one, is served `2025-03-13`. Defaulting to the newest version instead would hand you response shapes your client has never parsed, which is the break versioning exists to prevent.
Send the header explicitly. It costs one line and it pins your integration to shapes you have tested.
Moving the default to a newer version is itself a breaking change for callers that send no header, so it is announced in advance with a migration window rather than changed underneath you.
## Exploring a Version
The **API Reference** tab has a version selector. Switching versions regenerates every endpoint page from that version's spec, so what you see is what that version returns. The FedNow deposit rail, for example, appears on autoramp responses under `2026-07-01` and is absent under `2025-03-13`.
The selector tags each version so the two defaults are never confused: **Latest** is what the selector opens to, and **API default (no header)** is what an unversioned caller actually gets. They will not always be the same version.
A version makes a shape available. Some shapes also depend on your account configuration, so a rail your account is not enabled for stays absent even on a version that defines it.
## Downloading a Spec
Each version is published as its own OpenAPI document, served straight from the API and always current.
Every document below is available from **both environments**, and none of them needs an API key. Take the base URL for the environment you are integrating against:
* Sandbox: `https://api.sandbox.iron.xyz`
* Production: `https://api.iron.xyz`
| Document | Path |
| - | - |
| A single version | `/spec/2026-07-01` |
| Latest, full surface | `/spec` |
| The version list | `/spec/versions` |
So the `2026-07-01` document is at `https://api.sandbox.iron.xyz/spec/2026-07-01` in sandbox and `https://api.iron.xyz/spec/2026-07-01` in production.
Pick the environment deliberately: the two documents declare different `servers` URLs, and the sandbox one additionally carries the `/sandbox/*` simulation endpoints, which do not exist in production.
Reference pages also offer **Download API spec** in the page context menu, which gives you the spec for the version you are viewing.
If you generate a client, generate it from a dated spec and send that same date as `X-API-Version`. Moving versions then becomes one action: regenerate, and the date your client sends moves with it.
## Errors
An unknown or malformed version returns `400 Bad Request`. MoonPay Enterprise never falls back to a nearby version, because silently serving different shapes than you asked for is the failure versioning is meant to remove.
```json theme={null}
{
"error": "unsupported_api_version",
"reason": "version_unknown",
"requested": "2020-01-01",
"supported": ["2025-03-13", "2026-07-01", "2026-08-01", "2026-10-15"]
}
```
| `reason` | Cause |
| - | - |
| `version_unknown` | A valid date that is not a supported version |
| `version_malformed` | Not a canonical `YYYY-MM-DD` date |
Only the canonical spelling is accepted. `2026-7-1` is rejected as `version_malformed`, because caches key on the raw header value and equivalent spellings would fragment them.
## What Mints a New Version
A new version is cut for changes that break a client which parses responses strictly:
* A response field is removed or renamed
* A variant is added to an existing enum or union
* A request body changes shape
Additions that break nobody do not mint a version and land on every version: a new optional response field, a new endpoint, a new value in a field documented as open-ended.
## Key Considerations
* **Echo**: every `/api` response carries `X-API-Version` with the version actually served, including the default. Read it to confirm which version you are on without inspecting shapes.
* **Caching**: responses carry `Vary: X-API-Version`, so shared caches key on the version.
* **Idempotent retries**: a retried `POST` with the same `IDEMPOTENCY-KEY` returns the stored body and status without response headers, so it carries no version echo. See [Idempotency](/idempotency).
* **Unversioned surfaces**: market data endpoints have no version-gated shapes and ignore the header.
* **Token-scoped endpoints read the header**: `GET /signup/{token}/terms-and-conditions` serves the single global Terms document from `2026-08-01` and the region-specific one below it. Send the same version here that you send on `/api`, or an unversioned call silently gets the region-specific document.
# Wallet as a Service
Source: https://dev.enterprise.moonpay.com/wallet/wallet-as-a-service
Self-custodial embedded wallets, MPC signing, delegated agents, and fiat onramp and offramp for your users.
MoonPay WaaS gives your users a self-custodial, embedded wallet without asking them to install an extension, manage a seed phrase, or leave your app. A wallet is minted with a single SDK call for Ethereum/EVM, Solana/SVM, or Tron, and it is ready to receive, hold, and send stablecoins immediately.
WaaS is a separate integration with its own SDK, credentials, and dashboard. It does not run on your MoonPay Enterprise API key, and you can adopt it with or without the rest of MoonPay Enterprise.
Ethereum/EVM, Solana/SVM, and Tron from one integration.
2-of-2 MPC signing. No party, including MoonPay, can sign alone.
Mint a wallet the moment a user needs one. Never automatic at signup.
Users can export their private key at any time. No lock-in.
## How it works
A wallet key is never stored whole. It exists as two MPC (multi-party computation) shares: one held by the user's browser and protected by their passkey, one held by an isolated server-side signer. Both must participate to produce a signature, so neither MoonPay nor your app can move funds unilaterally.
Install the SDK, log a user in, and create a wallet.
## Why it matters
Partners building banks, fintechs, remittance platforms, payroll products, and PSPs on MoonPay Enterprise all face the same problem. Once fiat becomes a stablecoin, it has to live somewhere. Standing up wallet infrastructure yourself means owning key management, custody risk, and passkey and recovery UX that has nothing to do with your core product.
Wallet as a Service removes that build. If you also run MoonPay Enterprise, the two compose in your own code: [Virtual Accounts](/onramp) turn incoming fiat into stablecoins, and a WaaS wallet is one place those stablecoins can land, self-custodial and under the end customer's control. You pass the address the SDK returns into the MoonPay Enterprise flow yourself.
Sending funds to a WaaS wallet through an [Autoramp](/autoramp) flow means registering its address against the customer first, the same as any other external wallet. See [Crypto Addresses](/crypto-addresses).
## Chain support
| | Ethereum & EVM | Solana | Tron |
| - | - | - | - |
| Networks | Mainnet, all EVM-compatible L2s and sidechains (Base, Arbitrum, Optimism, Polygon, custom EVMs), and testnets | Mainnet, Devnet, Testnet | Mainnet |
| Embedded wallet creation | ✅ | ✅ | ✅ |
| Sign message | ✅ EIP-191 | ✅ | ✅ TIP-191 |
| Sign typed data (EIP-712) | ✅ | ❌ | ❌ |
| Sign transaction | ✅ | ✅ | ✅ client builds with TronWeb, wallet signs |
| Send transaction (sign and broadcast) | ✅ | ✅ | ✅ |
| Sign raw hash | ✅ | ❌ | ❌ |
| EIP-7702 authorization signing | ✅ | ❌ | ❌ |
| Import and export private key | ✅ | ✅ | ✅ |
| Native balance and token account reads | ✅ | ✅ plus SPL token accounts | ✅ |
Every chain shares the same wallet creation, passkey protection, import/export, and delegated signing model described below. Only the signing surface differs per chain.
## Wallet lifecycle
### Creation
Wallets are never minted automatically at signup or login. Your app calls the chain-specific creation hook explicitly. The first wallet a user creates also triggers passkey enrollment, since the passkey is what protects the wallet's key material from then on.
```tsx theme={null}
import { useCreateWallet } from '@moon-x/react-sdk/ethereum';
function CreateWalletButton() {
const { createWallet } = useCreateWallet();
const handleCreate = async () => {
const wallet = await createWallet();
console.log('Wallet created:', wallet.address);
};
return ;
}
```
For a brand-new user, collapse passkey enrollment and the first wallets into a single biometric prompt instead of two:
```tsx theme={null}
import { useRegisterPasskey } from '@moon-x/react-sdk';
const { registerPasskey } = useRegisterPasskey();
const { wallets } = await registerPasskey({
createWallets: [{ chain: 'ethereum' }, { chain: 'solana' }],
});
```
### Listing a user's wallets
```tsx theme={null}
import { useWallets } from '@moon-x/react-sdk/ethereum';
// PublicWallet[]: address, chain, algorithm. Never key material.
const { wallets } = useWallets();
```
### Importing an existing wallet
Users can bring a wallet they already hold, from either a raw private key or a 12 or 24 word BIP-39 seed phrase, with optional passphrase and account/address index selection matching the derivation conventions of MetaMask, Ledger Live, Phantom, and Solflare. The import runs the split into MPC shares client-side. The plaintext key is never sent to MoonPay and is discarded after the ceremony.
```tsx theme={null}
import { useImportKey } from '@moon-x/react-sdk/ethereum';
const { importKey } = useImportKey();
const { wallet } = await importKey({ key: privateKeyFromUserInput });
```
A `bulkImport` variant imports up to 20 keys, potentially mixing chains, in a single passkey prompt instead of one prompt per key. It returns per-key success and failure detail.
Imported addresses are screened for sanctions and AML risk before the wallet is persisted, the same as newly created ones.
### Exporting a private key
Export is the one flow where a complete private key is ever assembled, and it happens only in the user's own browser after a fresh passkey check. Never on MoonPay servers. This is what keeps the model self-custodial: a user can always take their assets and leave.
```tsx theme={null}
import { useExportKey } from '@moon-x/react-sdk/ethereum';
const { exportKey } = useExportKey();
await exportKey({ wallet: userWallet });
```
Export stays available even to compliance-blocked accounts. Blocking prevents new platform activity, it does not confiscate funds.
## Security model
### Passkeys protect the key, not login
A passkey here is not a login method. Users sign in with email OTP or OAuth. Each enrolled passkey wraps the encryption key that protects a user's wallet share, and a fresh passkey assertion proves the user is present for sensitive operations: signing, export, adding a passkey, revoking an agent.
On passkey-only apps, losing every enrolled passkey with no synced copy means the wallet cannot be recovered. Prompt users to enroll more than one, or enable [identity-backed encryption](#identity-backed-encryption-no-passkey) for a recovery path.
### Identity-backed encryption (no passkey)
An optional second encryption path lets a signed-in session read and write key shares, prove key ownership, and sign, with no passkey prompt. Users can then hold a wallet without ever enrolling a passkey. Enrolling or adding a wrap still takes a passkey assertion or an emailed code, and removing the identity wrap takes a passkey.
This is a different trust trade. The share is wrapped by a cloud KMS key that MoonPay operates, which decrypts only for a request proving it acts for one specific user of one app. MoonPay is therefore in the unwrap path, where passkey wrapping leaves it unable to unwrap at all. Users gain recovery if they lose their passkeys and accept that custody risk. That fits consumer apps where device loss is common. It does not fit applications whose users expect that no party but themselves can ever reach key material.
To run identity-first, ask MoonPay to enable it for your app, then:
* Read `kmsWrapEnabled`, `hasKmsWrap`, and `hasPasskey` from `usePasskeyStatus()`
* Call `enableAccountRecovery()` once per authenticated user who has no wrap yet
* Gate wallet operations on `hasPasskey === true || hasKmsWrap === true`, not on `hasPasskey` alone
* Offer `registerPasskey()` from settings as an optional upgrade
There is nothing to configure in your app. MoonPay enables identity-backed encryption per app during onboarding, and once your app stores its first identity-backed wrap the configuration behind it is fixed. See [Identity-backed encryption](https://x-docs.moonpay.com/wallets/identity-backed-encryption) and [Identity-first setup](https://x-docs.moonpay.com/wallets/identity-first-setup).
### Delegated and server-side signing
A user can authorize a scoped, revocable agent identity, such as a trading bot, a payroll scheduler, or an AI agent, to sign on their wallet's behalf without handing over the private key.
Provisioning runs inside an attested secure enclave. The browser verifies the enclave's attestation before sealing key shares to it, and the resulting agent keypair is shown to your app exactly once. Server-side signing then requires two independent credentials, your app's secret key and a signature from the agent's own key, so neither one alone is enough to forge a signature.
The user can revoke any agent at any time via a fresh passkey check.
## Signing and transactions
Every chain exposes chain-specific hooks, imported from `@moon-x/react-sdk/ethereum`, `/solana`, or `/tron`, with a consistent shape. Pass the target wallet and get back a signature, signed transaction, or broadcast result.
### Ethereum and EVM
| Hook | Purpose |
| - | - |
| `useSignMessage` | EIP-191 personal message signing |
| `useSignTypedData` | EIP-712 structured data signing (permits, meta-transactions, order signing) |
| `useSignHash` | Raw 32-byte digest signing for custom protocols |
| `useSignTransaction` | Sign without broadcasting |
| `useSendTransaction` | Sign and broadcast in one call |
| `useSign7702Authorization` | Sign an EIP-7702 authorization, letting an EOA temporarily delegate execution to a smart-contract implementation (account abstraction, gas sponsorship, batched calls) |
| `useSwitchChain` | Switch the wallet's active EVM network |
| `useGetBalance` | Read the wallet's native balance |
```tsx theme={null}
import { useSendTransaction } from '@moon-x/react-sdk/ethereum';
const { sendTransaction } = useSendTransaction();
const result = await sendTransaction({
transaction: { to: '0xRecipient...', value: '0x38d7ea4c68000' },
wallet: userWallet,
});
```
### Solana
| Hook | Purpose |
| - | - |
| `useSignMessage`, `useSignTransaction`, `useSendTransaction` | Same shape as EVM |
| `useGetBalance` | Read the wallet's native SOL balance |
| `useGetTokenAccounts` | Read SPL token balances for the wallet |
### Tron
| Hook | Purpose |
| - | - |
| `useSignMessage` | TIP-191 message signing |
| `useSignTransaction` | Sign a TronWeb-built transaction. Your app builds it, the wallet signs |
| `useSendTransaction` | Sign and broadcast to a configured Tron fullnode |
| `useGetBalance` | Read the wallet's native TRX balance |
## UI components
Pre-built, brand-customizable modals cover the four sensitive wallet moments: sign message, sign transaction, send transaction, and export key. Each accepts `uiConfig` overrides for title, description, button copy, and success and failure screens, so the confirmation experience matches your product without you building custody UI from scratch.
## Funding: onramp and cash out
A wallet can be funded directly and cashed back out to fiat without the user leaving your app. Both flows run inside the WaaS SDK and do not touch the MoonPay Enterprise API.
* **Card payments (onramp)**: debit and credit card, Apple Pay, Google Pay. Runs on a MoonPay Buy widget session, opened in a managed popup and signed server-side so the wallet address and amount cannot be tampered with after signing.
* **Cash out (offramp)**: the reverse flow. The user sells from their embedded wallet and receives fiat to a card or bank account. The wallet handles the on-chain deposit to the payment provider itself, behind a first-party confirmation the user approves before anything is sent.
Both directions run on your own payment-provider account, with keys configured per environment. Real transfers are mainnet only. Sandbox mode tests the full flow with no real money movement.
## REST API
Wallet creation and signing are MPC ceremonies driven by the client SDK, not single REST calls. Key material only ever exists as shares assembled through the ceremony.
The REST API's role is server-side: querying a user's existing wallets, running compliance actions, and submitting agent-signed requests for delegated signing.
WaaS credentials are issued with your WaaS account, separately from any MoonPay Enterprise API key. Contact your MoonPay representative for the Wallets API reference.
## Get started
Email [developers@iron.xyz](mailto:developers@iron.xyz) to get set up. WaaS is provisioned on its own, so there is nothing to switch on in the MoonPay Enterprise Partner Dashboard.
Add the React or React Native SDK, point it at your WaaS credentials, and configure the chains you need. See the [SDK documentation](https://x-docs.moonpay.com/get-started/frontend-sdks/react-native/quick-start).
Pick passkey-only for the strongest custody guarantee, or identity-backed to drop the passkey prompt from routine signing. Choose before your first users enroll, since it is provisioned per app up front.
Test wallet creation, signing, import and export, and funding flows in the SDK's sandbox mode before switching to mainnet.
Tell the team what you are building and which chains you need.
Custodial wallets are in development. If you need MoonPay to hold keys on behalf of your users, we are working with a small number of design partners. [Get in touch](https://www.moonpay.com/business/contact?referral=stablecoins).
# Webhooks
Source: https://dev.enterprise.moonpay.com/webhooks
MoonPay Enterprise sends a signed webhook to your endpoint when a customer, transaction, autoramp, fiat address, or identification changes. The payload contains the event type and the associated data.
## Webhook Console
Webhooks are managed in the Partner Dashboard under **Developer > Webhooks**. The page has three tabs:
* **Webhook Logs**: every delivery attempt your endpoint answered, with the response code and how long your endpoint took
* **Manage Webhooks**: the endpoints you have registered, and the events each one receives
* **Integration Guide**: sample code for verifying signatures in your language
### Fields
* **Label**: The name of your webhook. If a delivery to it failed, a warning icon appears next to the name. Hover over it for the failure reason
* **Status**: Whether the endpoint is enabled. A disabled endpoint stays registered and receives nothing
* **Topics**: The events this endpoint is subscribed to
* **Ping**: Sends a test delivery to the endpoint, so you can confirm your handler answers before real events arrive
### Delivery logs
The Webhook Logs tab lists every attempt your endpoint answered with an HTTP status, newest first. Timeouts and connection errors don't create a row. For those, check the warning icon next to the webhook's label. Open a row for the full request: the signed headers, the exact JSON body sent, the response code, and a **Retry delivery** button for the ones that failed.
## Register Webhook
Click **Register Webhook** on the Webhooks page.
Fill in three things:
* **Label**: Shown in the webhook list and in the delivery logs
* **Endpoint URL**: A public `https://` URL that returns a 2xx within 5 seconds
* **Events**: The event kinds delivered to this endpoint. `ping` is always on, so a test delivery works on every endpoint
Once you create the webhook, the next screen shows the `webhook secret`. This is needed to validate the webhook signature.
The secret is shown once, at creation. Copy it before leaving the screen. If you lose it, register a new webhook.
## Implement Webhooks
The Partner Area webhook console provides sample code for webhook signature validation. However, additional information can be found below.
Overview
* Webhook ID: newEvent
* Method: POST
* Content Type: application/json
* Authentication: HMAC-SHA256 signature (via headers)
* Trigger: Emitted when a relevant customer or platform event occurs
### Receiving Webhook Requests
MoonPay Enterprise will send webhook notifications to the endpoint URL you’ve registered during integration setup. Each request includes headers for verification, and a JSON body describing the event.
#### HTTP Headers
MoonPay Enterprise uses the [Standard Webhooks](https://www.standardwebhooks.com/) header names, but not its signature scheme. Every delivery includes these headers:
| Header | Description |
| - | - |
| `content-type` | Always `application/json` |
| `user-agent` | `MoonPay-Enterprise-Webhooks/1.0`. If you allowlist by user agent, match the `MoonPay-Enterprise-Webhooks/` prefix |
| `webhook-id` | Unique UUID of the event. It stays the same across all retries of the event, including a retry you trigger from the Partner Area, so use it to deduplicate |
| `webhook-timestamp` | Unix timestamp in seconds of when the webhook was sent |
| `webhook-signature` | Signature used to verify request authenticity. Format: `v1=` |
### Signature Verification
Verify the signature with your webhook secret:
Extract the `webhook-timestamp` and `webhook-signature` from the headers.
Remove the `v1=` prefix from the signature.
Concatenate `webhook-timestamp` + `raw_body` (no whitespace or formatting).
Compute the HMAC-SHA256 digest using your webhook secret key.
Reject the request if `webhook-timestamp` falls outside your tolerance window. The examples below use 5 minutes.
Use constant-time comparison to check if the computed digest matches the signature.
Always verify the signature using constant-time comparison to prevent timing attacks.
The official Standard Webhooks libraries reject these signatures. MoonPay Enterprise signs `webhook-timestamp` + raw body with no `webhook-id` and no `.` separators, uses the full secret string (including the `whsec_` prefix) as the HMAC key without base64-decoding it, and sends a hex digest as `v1=`. Follow the steps above instead.
### Webhook Payload
Webhook requests include a top-level `WebhookContainer` object. The message field varies depending on the event type.
#### Example Payloads
Below is one example payload per `type` the webhook can deliver. Each block is a complete `WebhookContainer`, the exact shape MoonPay Enterprise POSTs to your endpoint.
`transaction`: a new transaction happened for this customer. See [Autoramp](/autoramp).
```json theme={null}
{
"type": "transaction",
"timestamp": "2025-06-02T14:59:26.769468+00:00",
"data": {
"customer_id": "2ff3e394-978b-4489-8795-0a4e769a04c6",
"message": {
"Event": {
"id": "7d834f68-cea8-496a-8eae-bb0772365028",
"kind": "Transaction"
}
}
}
}
```
`new_autoramp`: a new autoramp was created by or for this customer. See [Autoramp](/autoramp).
```json theme={null}
{
"type": "new_autoramp",
"timestamp": "2025-06-02T14:59:26.769588+00:00",
"data": {
"customer_id": "4dcfe5af-3947-4928-b17f-a8fb13a68758",
"message": {
"Event": {
"id": "87f1a893-3691-494d-b815-215f5486a5b4",
"kind": "NewAutoramp"
}
}
}
}
```
`new_bank_account`: a new bank account was registered by or for this customer. See [Fiat addresses](/fiat-addresses).
```json theme={null}
{
"type": "new_bank_account",
"timestamp": "2025-06-02T14:59:26.769593+00:00",
"data": {
"customer_id": "0e98736e-0be6-4f76-9451-ec2fab006dd4",
"message": {
"Event": {
"id": "1cd9daaa-1ad1-48b8-a418-a3964dee1028",
"kind": "NewBankAccount"
}
}
}
}
```
`deposit_address_created`: the autoramp with `id` received a deposit address. See [Autoramp](/autoramp).
```json theme={null}
{
"type": "deposit_address_created",
"timestamp": "2025-06-02T14:59:26.769598+00:00",
"data": {
"customer_id": "45cd0169-7978-49b0-99ed-cb855acdba79",
"message": {
"Event": {
"id": "c8422122-5221-478a-95f4-953157277753",
"kind": "DepositAddressCreated"
}
}
}
}
```
`customer_created`: a new customer has been created. See [Onboarding](/onboarding).
```json theme={null}
{
"type": "customer_created",
"timestamp": "2025-06-02T14:59:26.769647+00:00",
"data": {
"customer_id": "af0315f0-a683-4330-a7b9-ca0c6a19ce87",
"message": {
"Event": {
"id": "6be8dd21-52a5-4616-b57d-54c4ec8acbf3",
"kind": "CustomerCreated"
}
}
}
}
```
`transaction_status`: the status of a transaction changed. See [Transaction status](/transaction-status).
```json theme={null}
{
"type": "transaction_status",
"timestamp": "2025-06-02T14:59:26.769658+00:00",
"data": {
"customer_id": "ebcea2b8-7caf-4ecf-ac59-95c5a6d7fefa",
"message": {
"TransactionStatus": {
"id": "e4f31eb4-da3a-4776-b70e-856a88492a17",
"status": "Pending",
"transaction_status": "ConversionInProgress",
"transaction_hash": "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"
}
}
}
}
```
`transaction_refunded`: a refund was recorded for a transaction. See [Transaction status](/transaction-status).
```json theme={null}
{
"type": "transaction_refunded",
"timestamp": "2025-06-02T14:59:26.769659+00:00",
"data": {
"customer_id": "ebcea2b8-7caf-4ecf-ac59-95c5a6d7fefa",
"message": {
"TransactionRefunded": {
"refund_id": "1b3f5a9e-7c2d-4f6a-9e8b-2a4c6d8f0e1b",
"transaction_id": "e4f31eb4-da3a-4776-b70e-856a88492a17"
}
}
}
}
```
`register_fiat_address_status`: the status of a fiat address changed. See [Fiat addresses](/fiat-addresses). This fires for every fiat address registration, including accounts with no external provider step that go straight to `Registered`.
```json theme={null}
{
"type": "register_fiat_address_status",
"timestamp": "2025-06-02T14:59:26.769663+00:00",
"data": {
"customer_id": "0ae92a7d-82c5-4e98-ad6e-3dbb6e573b2c",
"message": {
"RegisterFiatAddressStatus": {
"id": "bf1b777d-2f46-4f7c-a5be-b6f825cfcd7b",
"status": "AuthorizationRequired"
}
}
}
}
```
`customer_status`: the status of a customer has changed. See [Onboarding](/onboarding).
```json theme={null}
{
"type": "customer_status",
"timestamp": "2025-06-02T14:59:26.769750+00:00",
"data": {
"customer_id": "b714a479-0cf8-4390-9f24-d32df02dff36",
"message": {
"CustomerStatus": {
"id": "eb1bbc6a-1256-4653-badf-7d0d7e96deba",
"status": "Active"
}
}
}
}
```
`customer_fiat_abilities`: the customer's fiat deposit or payout rails changed. See [Onboarding](/onboarding#ability-status). Subscribe to this instead of polling `GET /api/customers/{id}/abilities`. MoonPay Enterprise recomputes abilities and fires this event when an identification is approved, a signing is recorded, a compliance review changes the customer, or a banking provider finishes registering a rail.
```json expandable theme={null}
{
"type": "customer_fiat_abilities",
"timestamp": "2025-06-02T14:59:26.769755+00:00",
"data": {
"customer_id": "b714a479-0cf8-4390-9f24-d32df02dff36",
"message": {
"CustomerFiatAbilities": {
"id": "b714a479-0cf8-4390-9f24-d32df02dff36",
"fiat_deposit": {
"usd": {
"ach": "Active",
"ach_thirdparty": "Active",
"wire": "Active",
"wire_thirdparty": "Active",
"rtp": "Unavailable",
"rtp_thirdparty": "Unavailable",
"fednow": "Unavailable",
"fednow_thirdparty": "Unavailable",
"swift": "Active",
"swift_thirdparty": "Unavailable"
},
"eur": {
"sepa": "Active",
"sepa_thirdparty": "Active",
"swift": "Unavailable",
"swift_thirdparty": "Unavailable"
},
"gbp": {
"fps": "Active",
"fps_thirdparty": "Active",
"chaps": "Active",
"chaps_thirdparty": "Active",
"swift": "Unavailable",
"swift_thirdparty": "Unavailable"
},
"brl": {
"pix": "Pending",
"pix_thirdparty": "Unavailable"
}
},
"fiat_payout": {
"usd": {
"ach": "Active",
"ach_thirdparty": "Active",
"wire": "Active",
"wire_thirdparty": "Active",
"rtp": "Unavailable",
"rtp_thirdparty": "Unavailable",
"fednow": "Unavailable",
"fednow_thirdparty": "Unavailable",
"swift": "Active",
"swift_thirdparty": "Unavailable"
},
"eur": {
"sepa": "Active",
"sepa_thirdparty": "Active",
"swift": "Unavailable",
"swift_thirdparty": "Unavailable"
},
"gbp": {
"fps": "Active",
"fps_thirdparty": "Active",
"chaps": "Active",
"chaps_thirdparty": "Active",
"swift": "Unavailable",
"swift_thirdparty": "Unavailable"
},
"brl": {
"pix": "Unavailable",
"pix_thirdparty": "Unavailable"
}
}
}
}
}
}
```
`id` is the customer, the same UUID as `data.customer_id`.
Each delivery carries the customer's full fiat ability snapshot, not a diff, and MoonPay Enterprise does not compare it against what it sent last time. The same snapshot can arrive twice, so diff the payload against your stored copy and act on the rails that actually moved.
The snapshot covers `fiat_deposit` and `fiat_payout` only. It does not include the `currencies` array that `GET /api/customers/{id}/abilities` returns, so call the endpoint when you need per-currency flow availability (mint, redeem, onramp, offramp, swap).
`register_autoramp_status`: the status of an autoramp has changed. See [Autoramp status](/autoramp-status).
```json theme={null}
{
"type": "register_autoramp_status",
"timestamp": "2025-06-02T14:59:26.769759+00:00",
"data": {
"customer_id": "357c34d3-7abe-4334-b8b5-6f8e84ba756e",
"message": {
"RegisterAutorampStatus": {
"id": "bc739df1-2816-4034-8b36-b99510370f18",
"status": "Created"
}
}
}
}
```
`identification_status`: the status of an identification has changed. See [KYC](/kyc).
```json theme={null}
{
"type": "identification_status",
"timestamp": "2025-06-02T14:59:26.769768+00:00",
"data": {
"customer_id": "8a7c34d3-7abe-4334-b8b5-6f8e84ba756f",
"message": {
"IdentificationStatus": {
"id": "cd849ef2-3927-5145-c947-c110481f29ca",
"status": "Approved"
}
}
}
}
```
`ping`: a ping event, sent during webhook setup or from the **Ping** button in the Webhook Console.
```json theme={null}
{
"type": "ping",
"timestamp": "2025-06-02T14:59:26.769763+00:00",
"data": {
"customer_id": "59f42528-3c5a-4448-b2df-65369adcee42",
"message": {
"Ping": {
"id": "1d9c36fb-83bf-49cc-9b1f-aff970b6ce96"
}
}
}
}
```
### Payload Schema
**WebhookContainer** (object)
| Field | Type | Required | Description |
| - | - | - | - |
| type | string | Yes | Event type in `snake_case`, for example `transaction_status`. |
| timestamp | string | Yes | ISO 8601 timestamp of when the event was triggered. |
| data | object | Yes | Event-specific data payload. |
**WebhookNotification** (inside data)
| Field | Type | Required | Description |
| - | - | - | - |
| customer\_id | string | Yes | UUID of the affected customer. |
| message | object | Yes | The core event message. Varies by event type. |
The delivered `type` is `snake_case` (`transaction_status`). The topics you subscribe a webhook to are PascalCase (`TransactionStatus`). Both name the same events, so map between the two casings when you route deliveries.
### Supported Event Types
Each message object follows a typed schema depending on the type of event.
1. **WebhookEventMessage**
General-purpose events (e.g., onboarding, registration).
```json theme={null}
{
"id": "uuid",
"kind": "Transaction" | "NewBankAccount" | "NewAutoramp" | "DepositAddressCreated" | "CustomerCreated"
}
```
2. **WebhookFiatAddressStatusMessage**
Status of a fiat vIBAN account.
```json theme={null}
{
"id": "uuid",
"status": "AuthorizationRequired" | "AuthorizationFailed" | "RegistrationPending" | "RegistrationFailed" | "Registered"
}
```
3. **WebhookAutorampStatusMessage**
Status changes in an Autoramp.
```json theme={null}
{
"id": "uuid",
"status": "Created" | "EditPending" | "Authorized" | "DepositAccountAdded" | "Approved" | "Rejected" | "Cancelled"
}
```
4. **WebhookTransactionStatusMessage**
Real-time updates on transactions.
```json theme={null}
{
"id": "uuid",
"status": "Pending" | "PayoutPending" | "Payout" | "PayoutCompleted" | "Completed" | "Failed" | "InAmlReview" | "AmlRejected" | "AmountRejected" | "FraudRejected",
"transaction_status": "FundsReviewInProgress" | "ConversionInProgress" | "PayoutInProgress" | "Completed" | "Failed" | "RejectedAml" | "RejectedFraud" | "RejectedMinAmount",
"transaction_hash": "string (optional)"
}
```
The `status` field is **deprecated**. Use `transaction_status` instead for the current transaction state.
The `transaction_hash` field is only present when the payout destination is a blockchain address (crypto payouts).
5. **WebhookCustomerStatusMessage**
Customer onboarding and compliance status.
```json theme={null}
{
"id": "uuid",
"status": "UserRequired" | "SigningsRequired" | "IdentificationRequired" | "Active" | "Suspended" | "Archived"
}
```
6. **WebhookCustomerFiatAbilitiesMessage**
The customer's current fiat deposit and payout rails.
```json theme={null}
{
"id": "uuid",
"fiat_deposit": { "usd": { ... }, "eur": { ... }, "gbp": { ... }, "brl": { ... } },
"fiat_payout": { "usd": { ... }, "eur": { ... }, "gbp": { ... }, "brl": { ... } }
}
```
Every currency key is present. Each rail leaf is one of `Active`, `Pending`, `Unavailable`, `Blocked`, `Maintenance`. See [Ability Status](/onboarding#ability-status) for the rails each currency carries and what each status means.
7. **WebhookPingMessage**
Sent during webhook setup or testing.
```json theme={null}
{
"id": "uuid"
}
```
8. **WebhookIdentificationStatusMessage**
Identification status during KYC/KYB process.
```json theme={null}
{
"id": "uuid",
"status": "Pending" | "Processed" | "PendingReview" | "Approved" | "Declined" | "Expired" | "Archived"
}
```
`Pending` → Customer has not started the process, or a business submission is waiting on missing items (resume `url` on the identification)
`Processed` → Customer has completed the input process
`PendingReview` → Identification is ready for review by Compliance Team
`Approved` → Identification has been approved by Compliance Team
`Declined` → Identification has been declined by Compliance Team
`Expired` → The identification process was not completed and has been expired
`Archived` → The identification is no longer active and has been archived
9. **WebhookTransactionRefundedMessage**
A refund recorded for a transaction.
```json theme={null}
{
"refund_id": "uuid",
"transaction_id": "uuid"
}
```
### Response
Your webhook endpoint must return a 2xx status within 5 seconds, for example:
`HTTP/1.1 200 OK`
Any 2xx acknowledges receipt. Any other status code, a timeout, or a connection error triggers a retry.
Log every `webhook-id` and response status for audit and troubleshooting.
## Error Handling & Retries
* Failed deliveries (non-2xx status, timeout, or connection error) are retried with exponential backoff. The first retry comes after about 5 seconds, the wait doubles each time, and it caps at 20 minutes between attempts.
* After 20 failed retries, MoonPay Enterprise drops the event and does not send it again automatically.
* Delivery is at-least-once, so the same event can arrive more than once. Every retry carries the same `webhook-id`; deduplicate on it.
* You can see failing webhooks in the Partner Area
## Sample Signature
The `Raw payload` below is the exact byte string MoonPay Enterprise signs and POSTs: the full `WebhookContainer`, not just the inner notification. HMAC the `timestamp` + this raw body to reproduce the signature.
```bash theme={null}
Secret: whsec_1s/keE/2+3eQUBc+7kedMAFRoM0twsrBYPpGWbt2/csF6pbMws9RMDRU1wtRas0PwDYgDd3t7mamKhO4LBjBiQ
Raw payload: {"type":"ping","timestamp":"2025-05-21T13:49:31.000000+00:00","data":{"customer_id":"3f9830ca-a98e-4020-a25b-80f21da86c97","message":{"Ping":{"id":"0196f318-b593-7803-a8f1-047d53179e06"}}}}
signatureHeader: v1=2cf8c0fc4f7485802689868f4f060ecc942d7a636ed2f14847b8bc4251f81a42
timestamp: 1747835371
deliveryId: f22ba628-4ab6-4a01-8d08-ff5de0ca2334
```
## Example Code
```php PHP expandable theme={null}
TIMESTAMP_TOLERANCE) {
http_response_code(400);
echo "Webhook timestamp outside of tolerance window";
exit;
}
$signedPayload = $timestamp . $rawBody;
$computedHmac = hash_hmac('sha256', $signedPayload, WEHO_SECRET);
if (!hash_equals($computedHmac, $receivedSig)) {
http_response_code(401);
echo "Invalid signature";
exit;
}
// Log and respond
$payload = json_decode($rawBody, true);
error_log("Payload: " . print_r($payload, true));
error_log("✔️ Received valid webhook $deliveryId");
header("Content-Type: application/json");
echo json_encode(["status" => "ok"]);
```
```javascript Javascript expandable theme={null}
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf } }));
const WEHO_SECRET = "whsec_XI+NCZLEYIveyE9JlGzrNz0uin0MW0hU9Ldb9kn+GPGuHEs1huWsov67uqmtUz9fRrgpLLeQwE2+rwv8dF/DJw";
const TIMESTAMP_TOLERANCE = 5 * 60;
app.post("/webhook", (req, res) => {
console.log("Raw payload:", req.rawBody.toString());
const signatureHeader = req.headers["webhook-signature"];
const timestamp = req.headers["webhook-timestamp"];
const deliveryId = req.headers["webhook-id"];
console.log("signatureHeader:", signatureHeader);
console.log("timestamp:", timestamp);
console.log("deliveryId:", deliveryId);
if (!signatureHeader || !timestamp || !deliveryId) {
return res.status(400).send("Missing one of required headers: webhook-id, webhook-timestamp, webhook-signature");
}
if (!signatureHeader.startsWith("v1=")) {
return res.status(400).send("Invalid signature format");
}
const receivedSig = signatureHeader.split("=", 2)[1];
const ts = parseInt(timestamp, 10);
if (isNaN(ts)) {
return res.status(400).send("Invalid webhook-timestamp");
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > TIMESTAMP_TOLERANCE) {
return res.status(400).send("Webhook timestamp outside of tolerance window");
}
const signedPayload = Buffer.concat([
Buffer.from(timestamp, "utf8"),
req.rawBody
]);
const computedHmac = crypto
.createHmac("sha256", WEHO_SECRET)
.update(signedPayload)
.digest("hex");
// timingSafeEqual throws on unequal lengths, so check length first
const isValid = computedHmac.length === receivedSig.length && crypto.timingSafeEqual(
Buffer.from(computedHmac),
Buffer.from(receivedSig)
);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
console.log("Payload:", req.body);
console.info("✔️ Received valid webhook ", deliveryId, req.body);
res.json({ status: "ok" });
});
app.listen(4422, "127.0.0.1", () => {
console.log("Webhook server running on http://127.0.0.1:4422");
});
```
```ruby Ruby expandable theme={null}
require 'webrick'
require 'openssl'
require 'json'
require 'time'
require 'rack'
WEHO_SECRET = 'whsec_zQtx6pI1m/i6j4Hi2A8ukDlaJUNYtG1Jqm+OH9FLvn8Juf/RzD3oABtBfJrGw4c17J4PXjri4GF2h1NZBc78aA'
TIMESTAMP_TOLERANCE = 5 * 60 # 5 minutes
server = WEBrick::HTTPServer.new(Port: 4424)
server.mount_proc '/webhook' do |req, res|
raw_body = req.body
headers = req.header.transform_keys(&:downcase)
signature_header = headers["webhook-signature"]&.first || ""
timestamp = headers["webhook-timestamp"]&.first || ""
delivery_id = headers["webhook-id"]&.first || ""
puts "signatureHeader: #{signature_header}"
puts "timestamp: #{timestamp}"
puts "deliveryId: #{delivery_id}"
if signature_header.empty? || timestamp.empty? || delivery_id.empty?
res.status = 400
res.body = "Missing one of required headers: webhook-id, webhook-timestamp, webhook-signature"
next
end
unless signature_header.start_with?("v1=")
res.status = 400
res.body = "Invalid signature format"
next
end
received_sig = signature_header[3..]
ts = timestamp.to_i
if ts == 0
res.status = 400
res.body = "Invalid webhook-timestamp"
next
end
now = Time.now.to_i
if (now - ts).abs > TIMESTAMP_TOLERANCE
res.status = 400
res.body = "Webhook timestamp outside of tolerance window"
next
end
signed_payload = timestamp + raw_body
computed_hmac = OpenSSL::HMAC.hexdigest('sha256', WEHO_SECRET, signed_payload)
# For security reasons, use secure compare
unless Rack::Utils.secure_compare(computed_hmac, received_sig)
res.status = 401
res.body = "Invalid signature"
next
end
payload = JSON.parse(raw_body) rescue {}
puts "Payload: #{payload}"
puts "✔️ Received valid webhook #{delivery_id}"
res['Content-Type'] = 'application/json'
res.body = { status: 'ok' }.to_json
end
trap("INT") { server.shutdown }
server.start
```
```python Python expandable theme={null}
#!/usr/bin/env python3
import os
import hmac
import hashlib
import time
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
# The webhook secret
WEHO_SECRET = "whsec_TJWDeK/U/JIDtD8Z9VJ00zsqjE7S/fwV47BTg4/hqO+2yi0PcHjaurd0C7S6kbsGwxeo/fX1+E0hjN+QkIHmNg"
TIMESTAMP_TOLERANCE = 5 * 60
@app.route("/webhook", methods=["POST"])
def receive_webhook():
# 1. Extract headers
signature_header = request.headers.get("webhook-signature", "")
print(signature_header)
timestamp = request.headers.get("webhook-timestamp", "")
print(timestamp)
delivery_id = request.headers.get("webhook-id", "")
print(delivery_id)
if not signature_header or not timestamp or not delivery_id:
abort(400, "Missing one of required headers: webhook-id, webhook-timestamp, webhook-signature")
# 2. Verify signature format
if not signature_header.startswith("v1="):
abort(400, "Invalid signature format")
received_sig = signature_header.split("=", 1)[1]
# 3. Prevent replay by ensuring timestamp is recent
try:
ts = int(timestamp)
except ValueError:
abort(400, "Invalid webhook-timestamp")
now = int(time.time())
if abs(now - ts) > TIMESTAMP_TOLERANCE:
abort(400, "Webhook timestamp outside of tolerance window")
# 4. Compute our own HMAC-SHA256 over timestamp + raw body
payload = request.get_data() # raw bytes
signed_payload = timestamp.encode("utf-8") + payload
print(signed_payload)
computed_hmac = hmac.new(
key=WEHO_SECRET.encode("utf-8"),
msg=signed_payload,
digestmod=hashlib.sha256
).hexdigest()
# 5. Constant-time compare
if not hmac.compare_digest(computed_hmac, received_sig):
abort(401, "Invalid signature")
# 6. At this point the payload is verified
data = request.get_json(silent=True) or {}
print(data)
app.logger.info(f"✔️ Received valid webhook {delivery_id}: %s", data)
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
# Listen on localhost only (127.0.0.1) at port 4422
app.run(host="127.0.0.1", port=4422)
```
# What's new
Source: https://dev.enterprise.moonpay.com/whats-new
New MoonPay Enterprise products, features, and improvements, newest first. Updated weekly.
For request, response, and behavior changes to the API itself, see the [API changelog](/changelog).
* **Pay out to individuals over SWIFT.** Individual customers can now send USD payouts over SWIFT to bank accounts in 46 countries, for both first-party and third-party payments. See [Currencies and rails](/currencies-and-rails).
* **Set fees below one basis point.** Your transactional fee profiles now accept fractional basis points, such as `12.5` bps, so you can price more precisely. See [Fees and invoices](/fees-and-invoices).
* **Edit a customer's email in the dashboard.** Open a customer and use the pencil next to their email. See [Dashboard](/partner-dashboard).
* **See a customer's RFIs on their page.** A customer's RFIs now show on their detail sheet, so you don't have to leave the customer to find them. See [Information Requests](/rfi).
* **Let customers answer information requests themselves.** Copy a link from an open RFI round in the dashboard and send it to your customer. They answer the questions and upload documents on their own page, with no relay through your team. See [Information Requests](/rfi).
* **See completed information requests.** The dashboard's RFIs tab has an Open and Completed toggle, so you can look back at requests MoonPay has closed. Completed requests need nothing from you. See [Information Requests](/rfi).
* **Trace SWIFT payments by UETR.** A transaction's payment details now show the SWIFT UETR, next to the ACH trace number, IMAD, and OMAD, so your customer can track a payment with their bank. See [Dashboard](/partner-dashboard#transactions).
* **See how many customers are active.** The dashboard home shows unique active customers each month, with the change from the previous period. See [Dashboard](/partner-dashboard#home).
* **Nigeria is now supported.** Customers in Nigeria can onboard and use USD, EUR, and GBP rails. See [Countries](/country).
* **Business customers can share their AML/CFT policy.** Businesses can now attach their AML/CFT compliance policy to their application. It's optional and doesn't change which documents are required. See [KYB](/kyb).
* **Answer compliance requests in the dashboard or API.** When MoonPay needs more information about one of your customers, the request now arrives in your dashboard and through the API, instead of by email or Slack. Copy the questions to your customer, submit their answers and documents, and handle follow-up rounds in the same place. See [Information Requests](/rfi).
* **Send customer emails from your own domain.** Emails to your customers can carry your logo, your sender name, and your own verified domain. The MoonPay Enterprise team sets it up and sends you the DNS records to publish. See [Customer email branding](/integration-models#customer-email-branding).
* **See which payment rails a customer can't use, and why.** A customer's Banking Capabilities card now lists Unavailable Capabilities by currency, with the reason for each rail. The same data is in the API as `blocking_abilities`. See the [API changelog](/changelog#2026-09-15).
* **One page for every rail and region.** [Rails and capabilities](/rails-and-capabilities) covers Virtual Accounts, payment rails, coverage, onboarding models, and our licenses.
* **Give your users a wallet without a seed phrase.** MoonPay Wallet as a Service mints self-custodial, embedded wallets for Ethereum and EVM chains, Solana, and Tron with a single SDK call. Your users never install an extension or leave your app. See [Wallet as a Service](/wallet/wallet-as-a-service).
* **The dashboard is now at app.enterprise.moonpay.com.** Sign-in, MFA, and password reset all work on the new address. See [Dashboard](/partner-dashboard).
* **Fix rejected documents yourself.** When a customer's document is rejected and can be retried, the dashboard shows why and lets your admins upload a replacement on the customer's behalf. Onboarding keeps moving without a new KYC link. See [Resolve rejected documents](/rejected-documents).
* **Viewers can generate KYC links.** Team members with the Viewer role can now create KYC links for customers, not only copy existing ones.
* **Fewer details needed to pay out to US and SWIFT bank accounts.** Registering a USD bank account no longer needs the recipient's email address or phone number, and a SWIFT account no longer needs the bank's address. See [Fiat addresses](/fiat-addresses).
* **PYUSDx is live.** PYUSDx is a DeFi-oriented issuance platform from MoonPay and M0, built in partnership with PayPal. Launch your own stablecoin backed 1:1 by PYUSD, with programmable rewards. Availability varies by jurisdiction. See [PYUSDx](/issuance/pyusdx).
* **Prohibited industries get an answer at submission.** A business in a prohibited industry is now declined the moment you submit it, instead of waiting in manual review. The full industry list is published, so you can check before you submit. See [Prohibited industries](/kyb#prohibited-industries).
* **US companies verified from official records.** When a US company matches the official company registry by name and registration number, it no longer needs to upload its company documents. See [Automatic verification for US companies](/kyb#automatic-verification-for-us-companies).
* **One set of terms for every customer.** A single global Terms and Conditions document now applies to every customer, wherever they live. See [Onboarding](/onboarding#terms-and-conditions).
* **Collect tax IDs from approved EEA customers.** Send an approved EEA customer a short hosted step that asks only for their tax ID. Their approval stays in place. See [Trigger Customer TIN Step-Up](/reference/customer/trigger-customer-tin-step-up).
* **Spending limits in your customer's currency.** Customer spending limits in the dashboard now show in the customer's own currency instead of EUR.
* **Quote or Route.** Creating a trade in the dashboard now comes down to two options. A Quote is a one-off trade for a set amount at a locked FX rate. A Route is a standing setup that stays live and accepts any amount: an onramp, an offramp, or a stable-to-stable swap. What used to be called Market is now Route. See [Routes](/partner-dashboard#routes).
* **A Compliance page for customers who need attention.** The dashboard's Compliance page sorts customers into tabs by what's blocking them, from incomplete applications to EDD and pending review, so you go straight to the ones that need you. See [Compliance](/partner-dashboard#compliance).
* **See what's missing from an application.** A customer's detail sheet lists the requirements still open on their application, so you know exactly what to collect.
* **Find customers by your own ID.** Search customers by your external ID, and filter the list to businesses or individuals.
* **USD over SWIFT.** USD virtual accounts include SWIFT details for international deposits. Business customers can send and receive SWIFT payments, and individual customers can receive SWIFT deposits. See [Currencies and rails](/currencies-and-rails).
* **Spending limits that fit each customer.** An individual's spending limit is now calculated from the income they declare in the KYC questionnaire, instead of one fixed limit. Business customers have no enforced limit. Check a customer's current limit on their dashboard profile or through the API. See [Limits](/limits-and-minimum).
# Attach a quote to an existing autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/attach-a-quote-to-an-existing-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 post /autoramps/{autoramp_id}/quotes
Attaches a signed rate quote to an existing quote-type autoramp.
# Cancel an Autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/cancel-an-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 delete /autoramps/{autoramp_id}
Cancel the execution of an autoramp. After this, the given autoramp will not perform any transactions anymore.
# Check if a customer can create more autoramps
Source: https://dev.enterprise.moonpay.com/reference/autoramp/check-if-a-customer-can-create-more-autoramps
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/check-limit
Checks if a customer has reached their limit for creating new autoramps based on the deposit account type and whether it's a third-party ramp.
# Create a new Autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/create-a-new-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 post /autoramps
Creates a new autoramp, with a specified recipient account, source and destination currency
# Create an open banking payment link
Source: https://dev.enterprise.moonpay.com/reference/autoramp/create-an-open-banking-payment-link
https://api.sandbox.iron.xyz/spec/2026-10-15 post /autoramps/{autoramp_id}/payments/open-banking
Create an open banking payment link for a given autoramp
# Generate a Pix BR code for an autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/generate-a-pix-br-code-for-an-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 post /autoramps/{autoramp_id}/br-code
Generates a static Pix BR code (copy-and-paste payload) for a BRL
autoramp, tagged with your `reference` and optionally a fixed amount.
The reference is returned with the resulting deposit. Calling again with
the same parameters returns the same code.
# Get a quote for an autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-a-quote-for-an-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/quote
Get a quote for an autoramp with fixed conversion rate, exact amount out or amount in
# Get a rate quote for an existing autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-a-rate-quote-for-an-existing-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/{autoramp_id}/quote
Generates a signed rate quote for an existing quote-type autoramp.
The autoramp's currencies, recipient, and fee profile are used automatically.
# Get an Autoramp by external ID
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-an-autoramp-by-external-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/{external_id}/external
Get a single autoramp by external ID
# Get an Autoramp by ID
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-an-autoramp-by-id
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/{autoramp_id}
Get a single autoramp by ID
# Get autoramp quote preview
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-autoramp-quote-preview
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/quotes-preview
Price a prospective autoramp knowing only the currencies — before a recipient
wallet or bank account is selected. Prices identically to `getAutorampQuote`;
banking fees assume the destination currency's default payout rail (USD→ACH,
EUR→SEPA, GBP→FPS; see `assumed_payout_rail` in the response). The result is an
estimate and cannot be submitted; fetch a signed quote once a recipient is chosen.
# Get autoramp transactions by transaction IDs
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-autoramp-transactions-by-transaction-ids
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramp-transactions/ids
Return a subset of transactions by ID.
This can be used to query multiple transactions for updated states without having to go through all transactions.
You have to provide at least one transaction ID
Returns at most 100 transactions
# Get the autoramp open banking payment for existing payment
Source: https://dev.enterprise.moonpay.com/reference/autoramp/get-the-autoramp-open-banking-payment-for-existing-payment
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/payments/open-banking/{payment_id}
Get the autoramp open banking payment for a given autoramp
# List Autoramps for a Customer
Source: https://dev.enterprise.moonpay.com/reference/autoramp/list-autoramps-for-a-customer
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps
Returns a paginated list of autoramps for all customers
If customer ID is provided, returns all autoramps for the specified customer
If status is provided, returns all autoramps with the specified status
# List open banking payments for an autoramp
Source: https://dev.enterprise.moonpay.com/reference/autoramp/list-open-banking-payments-for-an-autoramp
https://api.sandbox.iron.xyz/spec/2026-10-15 get /autoramps/{autoramp_id}/payments/open-banking
Returns a paginated list of all open banking payment attempts for the
autoramp, ordered by most recent first. Statuses are refreshed from the
provider on every call (skipping payments already in terminal states).
Pagination follows the standard cursor pattern: pass `cursor` =
`iron_payment_id` of the last item in the previous page to fetch the
next (older) page, or `prev_cursor` to walk backward (newer).
# Patch an autoramp market
Source: https://dev.enterprise.moonpay.com/reference/autoramp/patch-an-autoramp-market
https://api.sandbox.iron.xyz/spec/2026-10-15 patch /autoramps/{autoramp_id}
Patch the autoramp market by ID
# Retry authentication
Source: https://dev.enterprise.moonpay.com/reference/autoramp/retry-authentication
https://api.sandbox.iron.xyz/spec/2026-10-15 post /autoramps/{autoramp_id}/retry-auth
Retry the authentication process on a failed autoramp
# List all available crypto currencies
Source: https://dev.enterprise.moonpay.com/reference/currencies/list-all-available-crypto-currencies
https://api.sandbox.iron.xyz/spec/2026-10-15 get /cryptocurrencies
A list of all crypto currencies which are currently supported by Iron
# List all available fiat currencies
Source: https://dev.enterprise.moonpay.com/reference/currencies/list-all-available-fiat-currencies
https://api.sandbox.iron.xyz/spec/2026-10-15 get /fiatcurrencies
A list of all fiat currencies which are currently supported by Iron
# Get exchange rate
Source: https://dev.enterprise.moonpay.com/reference/exchangerate/get-exchange-rate
https://api.sandbox.iron.xyz/spec/2026-10-15 get /exchange-rate
Get the exchange rate from a source currency to a destination currency for a customer with effective rate and fees
# Create Fee Profile
Source: https://dev.enterprise.moonpay.com/reference/feeprofiles/create-fee-profile
https://api.sandbox.iron.xyz/spec/2026-10-15 post /fee-profiles
Creates a new transactional fee profile for the partner.
Iron fee settings are inherited from the partner's invoicing fee profile.
# Get Fee Profile of Partner
Source: https://dev.enterprise.moonpay.com/reference/feeprofiles/get-fee-profile-of-partner
https://api.sandbox.iron.xyz/spec/2026-10-15 get /fee-profiles
Gets fee profile of partner
# Get Platform Fee Configs of Partner
Source: https://dev.enterprise.moonpay.com/reference/feeprofiles/get-platform-fee-configs-of-partner
https://api.sandbox.iron.xyz/spec/2026-10-15 get /platform-fee-configs
Lists every platform fee config for the partner, along with the rule (ramp kind,
currency, customer, price source) that selects each one. Use this instead of the
deprecated `iron_fee` field on a fee profile.
# Mock a transaction
Source: https://dev.enterprise.moonpay.com/reference/sandbox/mock-a-transaction
https://api.sandbox.iron.xyz/spec/2026-10-15 post /sandbox/transaction
Creates a new fiat transaction directly from an autoramp configuration for testing purposes.
This bypasses the normal deposit flow and creates a ramp transaction with the specified amount
using the autoramp's configuration (customer, recipient, currencies, etc.).
# Reset sandbox environment
Source: https://dev.enterprise.moonpay.com/reference/sandbox/reset-sandbox-environment
https://api.sandbox.iron.xyz/spec/2026-10-15 post /sandbox/reset
Resets the sandbox for the partner. Including fiat accounts, wallets, autoramps, transactions, and customers
# Set Identification status
Source: https://dev.enterprise.moonpay.com/reference/sandbox/set-identification-status
https://api.sandbox.iron.xyz/spec/2026-10-15 post /sandbox/identification/{id}/status
Sets a customer's identification to any status, bypassing the normal transition rules,
so partners can rehearse every branch of their onboarding UI. Sandbox only.
# Update autoramp status
Source: https://dev.enterprise.moonpay.com/reference/sandbox/update-autoramp-status
https://api.sandbox.iron.xyz/spec/2026-10-15 put /sandbox/autoramp/{id}
Update the status of an autoramp for testing purposes
# Update fiat address verification status
Source: https://dev.enterprise.moonpay.com/reference/sandbox/update-fiat-address-verification-status
https://api.sandbox.iron.xyz/spec/2026-10-15 put /sandbox/fiat-verification/{id}
Update the verification status of a fiat address
# Update pending Identification status
Source: https://dev.enterprise.moonpay.com/reference/sandbox/update-pending-identification-status
https://api.sandbox.iron.xyz/spec/2026-10-15 post /sandbox/identification/{id}
Updates a pending identification for a customer.
Deprecated: use `POST /sandbox/identification/{id}/status`, which reaches every status.
# Update sandbox transaction state
Source: https://dev.enterprise.moonpay.com/reference/sandbox/update-sandbox-transaction-state
https://api.sandbox.iron.xyz/spec/2026-10-15 put /sandbox/transaction/{id}/state
Updates the state of a sandbox transaction for testing purposes
# Get all current terms and conditions
Source: https://dev.enterprise.moonpay.com/reference/termsandconditions/get-all-current-terms-and-conditions
https://api.sandbox.iron.xyz/spec/2026-10-15 get /terms-and-conditions
Retrieve all latest terms and conditions signings are required for a specific country
# Dispatches an event notification to a configured webhook endpoint
Source: https://dev.enterprise.moonpay.com/reference/webhooks/dispatches-an-event-notification-to-a-configured-webhook-endpoint
https://api.sandbox.iron.xyz/spec/2026-10-15 webhook new_event
This webhook is triggered when a significant event occurs within the system
that a partner might be interested in. The payload contains details about the
event type and the associated data.
## Request
The webhook will be sent as an HTTP `POST` request to the URL configured
for the webhook. The body of the request will be a JSON payload
(`application/json`) containing an `ApiWebhookContainer`.
## Headers
The following headers will be included in the request, adhering to [the
Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md):
- `content-type: application/json`: Indicates the payload format.
- `user-agent: MoonPay-Enterprise-Webhooks/1.0`: Identifies the sender. Match on
the `MoonPay-Enterprise-Webhooks/` prefix if you allowlist it.
- `webhook-id`: A unique UUID identifying the event. It stays the same
across all retries of the same event, so use it to deduplicate.
- `webhook-timestamp`: A string representing the Unix timestamp (seconds since epoch)
of when the webhook notification was sent.
- `webhook-signature`: A signature to verify the authenticity and integrity
of the payload. The format is `v1=`, where `` is
an HMAC-SHA256 hash.
## Signature Verification
To verify the signature:
1. Extract the `webhook-timestamp` from the header.
2. Extract the `webhook-signature` from the header and remove the `v1=` prefix.
3. Concatenate the `webhook-timestamp` string and the raw request body.
4. Calculate the HMAC-SHA256 hash of the concatenated string using your
webhook's secret key.
5. Compare the calculated hash (hex-encoded) with the signature extracted
from the header. They should match.
It is crucial to use a constant-time comparison function when comparing
signatures to prevent timing attacks.
(See the examples for more details)
# List all webhooks
Source: https://dev.enterprise.moonpay.com/reference/webhooks/list-all-webhooks
https://api.sandbox.iron.xyz/spec/2026-10-15 get /webhooks
Lists all webhooks for the currently authenticated partner
# Ping webhook
Source: https://dev.enterprise.moonpay.com/reference/webhooks/ping-webhook
https://api.sandbox.iron.xyz/spec/2026-10-15 post /webhooks/{id}/ping
Sends a test ping event to a webhook for testing purposes
# Update a webhook
Source: https://dev.enterprise.moonpay.com/reference/webhooks/update-a-webhook
https://api.sandbox.iron.xyz/spec/2026-10-15 patch /webhooks/{id}
Updates an existing webhook for the currently authenticated partner