> ## Documentation Index
> Fetch the complete documentation index at: https://dev.enterprise.moonpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Onramp

> Create a named virtual fiat account that settles into stablecoins in your wallet.

export const props_1 = undefined

export const props_0 = undefined

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

<Steps>
  <Step>
    Max sets up a vIBAN with an `autoramp` which turns EUR into USDC on Solana address 3DkN...BV4Lm
  </Step>

  <Step>
    Max triggers a EUR 1000 transfer from their banking app
  </Step>

  <Step>
    MoonPay Enterprise monitors the vIBAN. As soon as funds arrive, USDC is delivered to 3DkN...BV4Lm
  </Step>

  <Step>
    Max can transfer more funds into this vIBAN at any time and they autoramp to USDC. It is persistent.
  </Step>
</Steps>

<Note>
  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.
</Note>

## 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="<your-sandbox-api-key>"
export BASE_URL="https://api.sandbox.iron.xyz"
```

<Steps>
  <Step title="Customer is Active">
    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.
  </Step>

  <Step title="Register the recipient wallet address">
    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.
  </Step>

  <Step title="(Optional) Get a quote">
    If you need a locked rate (and lower-bps fee profiles), request a quote via `GET /api/autoramps/quote`. See [Quotes](#lock-a-rate-with-a-quote) below. Otherwise, the onramp executes at the current mid-market rate.
  </Step>

  <Step title="Create the onramp autoramp">
    Create the autoramp via `POST /api/autoramps` using the registered wallet address as the recipient. See the implementation example below.
  </Step>

  <Step title="Sandbox: approve the autoramp">
    An autoramp is created in `Authorized` status. In Sandbox you advance it yourself, which also provisions the mock deposit account that a simulated deposit needs:

    ```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.
  </Step>
</Steps>

### Onboard a customer from scratch

<Accordion title="Full onboarding walkthrough (Sandbox)">
  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

  <Steps>
    <Step title="Create a customer">
      `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).

      <CodeGroup>
        ```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"
          }'
        ```
      </CodeGroup>

      **Example response:**

      <CodeGroup>
        ```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"
        }
        ```
      </CodeGroup>

      Save the `id`. You need it in every subsequent step.
    </Step>

    <Step title="Present and sign the terms and conditions">
      `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.

      <CodeGroup>
        ```bash Get the terms theme={null}
        curl -X GET "$BASE_URL/api/customers/<customer_id>/required-signings" \
          -H "X-API-Key: $API_KEY"
        ```

        ```bash Record the signing theme={null}
        curl -X POST "$BASE_URL/api/customers/<customer_id>/signings" \
          -H "Content-Type: application/json; charset=utf-8" \
          -H "Idempotency-Key: $(uuidgen)" \
          -H "X-API-Key: $API_KEY" \
          -d '{
            "content_id": "<id_from_terms_response>",
            "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"
        ```
      </CodeGroup>

      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`.
    </Step>

    <Step title="Create a Link identification">
      `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.

      <CodeGroup>
        ```bash theme={null}
        curl -X POST "$BASE_URL/api/customers/<customer_id>/identifications/v2" \
          -H "Content-Type: application/json; charset=utf-8" \
          -H "Idempotency-Key: $(uuidgen)" \
          -H "X-API-Key: $API_KEY" \
          -d '{"type": "Link"}'
        ```
      </CodeGroup>

      **Example response:**

      <CodeGroup>
        ```json theme={null}
        {
          "id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
          "status": "Pending",
          "url": "https://api.sandbox.iron.xyz/verify?<encrypted_id>"
        }
        ```
      </CodeGroup>

      In production, pass the `url` to your customer to complete verification. Save the identification `id`.
    </Step>

    <Step title="Approve the identification (Sandbox only)">
      `POST /api/sandbox/identification/{id}`

      In Sandbox, you control whether an identification is approved or rejected.

      <CodeGroup>
        ```bash theme={null}
        curl -X POST "$BASE_URL/api/sandbox/identification/<identification_id>" \
          -H "Content-Type: application/json; charset=utf-8" \
          -H "Idempotency-Key: $(uuidgen)" \
          -H "X-API-Key: $API_KEY" \
          -d '{"approved": true}'
        ```
      </CodeGroup>

      In production, the MoonPay Enterprise compliance team handles this review (typically 24-48 hours).
    </Step>

    <Step title="Confirm the customer is Active">
      `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`.

      <CodeGroup>
        ```bash Retrieve signings theme={null}
        curl -X GET "$BASE_URL/api/customers/<customer_id>/required-signings" \
          -H "X-API-Key: $API_KEY"
        ```

        ```bash Sign a document theme={null}
        curl -X POST "$BASE_URL/api/customers/<customer_id>/signings" \
          -H "Content-Type: application/json; charset=utf-8" \
          -H "Idempotency-Key: $(uuidgen)" \
          -H "X-API-Key: $API_KEY" \
          -d '{
            "content_id": "<id_from_required_signings>",
            "signed": true
          }'
        ```
      </CodeGroup>

      <Note>
        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).
      </Note>
    </Step>

    <Step title="Register a crypto address">
      `POST /api/addresses/crypto/selfhosted`

      Register the wallet where stablecoins will be delivered. Required for [Travel Rule](/travel-rule) compliance before creating any autoramp.

      <CodeGroup>
        ```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": "<customer_id>",
            "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
            "message": "I am verifying ownership of the wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e as customer <customer_id>. This message was signed on 15/01/2026 to confirm my control over this wallet.",
            "signature": "<wallet-signature>",
            "blockchain": "Ethereum"
          }'
        ```
      </CodeGroup>

      <Note>
        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.
      </Note>
    </Step>
  </Steps>
</Accordion>

## Implementation

Create an autoramp that converts EUR to USDC on Ethereum.

<Note>
  `POST /api/autoramps` requires an `Idempotency-Key` header. Send the same key to safely retry a request without creating a duplicate autoramp.
</Note>

### Request

<CodeGroup>
  ```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"
  ```
</CodeGroup>

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

### Response

<CodeGroup>
  ```json Shell theme={null}
  {
    "id": "f8f9a8c7-d8b1-4f1a-8f15-7e6c8a1a8e50",
    "status": "Authorized",
    "deposit_rails": [],
    "created_at": "2025-01-20T12:34:56Z"
  }
  ```
</CodeGroup>

<Note>
  `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).
</Note>

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

<CodeGroup>
  ```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"
  }
  ```
</CodeGroup>

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.

<Note>
  The `deposit_rails` array contains the payment details. The legacy `deposit_account` field is deprecated.
</Note>

### 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`:

<CodeGroup>
  ```json 400 Bad Request theme={null}
  "Recipient wallet not verified for customer"
  ```
</CodeGroup>

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=<customer_id>&source_currency_code=EUR&destination_currency_code=USDC&destination_currency_chain=Ethereum&recipient_account_id=<crypto_address_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 {props_0.rampNoun} is half the integration. Simulate a deposit to confirm your webhook handler and reconciliation logic work before you take real money.

<Steps>
  <Step title="Simulate an incoming deposit">
    `POST /api/sandbox/transaction` builds a transaction directly from the route, reusing its currencies, customer, and recipient. {props_0.noFundsNote}

    ```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.
  </Step>

  <Step title="Drive the transaction to Completed">
    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" }'
    ```
  </Step>

  <Step title="Check the webhooks you received">
    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).
  </Step>
</Steps>

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

<Steps>
  <Step title="Create the route">
    Switch the sidebar to the **App** view and pick your customer in the customer selector at the top. Open **Operations → Routes**, then click **{props_1.createButton}** on the **{props_1.routeCard}** card and choose {props_1.createFields}.
  </Step>

  <Step title="Authorize the route">
    Go to **Developer → Sandbox** and open the **Autoramps** tab. Your new route appears under **Unverified Autoramps**. Set its status to **Authorized** using the dropdown on the right.

    <Note>
      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.
    </Note>
  </Step>

  <Step title="Create the simulated deposit">
    Still under **Developer → Sandbox**, click **New Transaction** in the top right. On the **Create New Sandbox Transaction** screen:

    1. **Customer**: search by name, email, or ID and select the customer who owns the route.
    2. **Ramp**: pick it from the dropdown. This field still uses the older wording for a route. Entries are labelled by currency pair, for example <code>{props_1.routeExample}</code>.
    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. {props_1.noFundsNote}
  </Step>

  <Step title="Settle the transaction">
    Back on the Sandbox page, open the **Transactions** tab. Your transaction is listed under **Pending Transactions** with **Accept** and **Reject** buttons. Choose **Accept** to complete it, or use the **Change state** dropdown to move it to a specific state and test how your integration reacts.
  </Step>

  <Step title="Check the result">
    Open the transaction from **Operations → Transactions** to see amounts, fees, and {props_1.detailFields}. 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.
  </Step>
</Steps>

<Tip>
  Use **Reset** on the Sandbox page to clear all customers, wallets, fiat accounts, transactions, and autoramps and start from a clean state.
</Tip>

## 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 |

<Note>
  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.
</Note>

## What to read next

<CardGroup cols={2}>
  <Card title="Offramp" icon="arrow-right-from-bracket" href="/offramp">
    The reverse route: stablecoins to a bank account
  </Card>

  <Card title="Autoramp Status" icon="rotate" href="/autoramp-status">
    Every status an autoramp moves through, and what unblocks each one
  </Card>

  <Card title="Crypto Addresses" icon="wallet" href="/crypto-addresses">
    Register self-hosted or hosted wallets for Travel Rule compliance
  </Card>

  <Card title="Webhooks" icon="bell" href="/webhooks">
    Signature verification, payload schemas, and retry behaviour
  </Card>

  <Card title="Quotes" icon="tag" href="/quotes">
    Lock a rate instead of executing at the current mid-market rate
  </Card>

  <Card title="Limits and Minimums" icon="gauge" href="/limits-and-minimum">
    Per-customer and per-transaction thresholds
  </Card>
</CardGroup>
