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

# Offramp

> Create a liquidation address that accepts stablecoins and settles in fiat into a bank account.

export const props_1 = undefined

export const props_0 = undefined

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

<Steps>
  <Step>
    Max registers his bank account and gets a **liquidation address** owned by MoonPay Enterprise
  </Step>

  <Step>
    An `autoramp` ties that address to his bank account: USDC in, EUR out
  </Step>

  <Step>
    Max sends 1000 USDC to the liquidation address
  </Step>

  <Step>
    As soon as funds hit the address, MoonPay Enterprise converts them to EUR and pays out to Max's bank account
  </Step>

  <Step>
    Max can transfer at any time. The address is persistent.
  </Step>

  <Step>
    Non-supported assets sent to the address are returned to sender
  </Step>
</Steps>

## 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="<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, 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.
  </Step>

  <Step title="Register a bank account">
    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`.
  </Step>

  <Step title="Sandbox: approve the fiat address">
    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.
  </Step>

  <Step title="Register crypto addresses">
    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.
  </Step>

  <Step title="Create the offramp">
    Either request an exact-out quote and create a quote-source autoramp, or create a standing autoramp directly. Both are shown 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>

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

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

<Steps>
  <Step title="Request the quote">
    `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=<customer_id>&source_currency_code=USDC&source_currency_chain=Ethereum&destination_currency_code=EUR&recipient_account_id=<fiat_address_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"
    ```

    <Accordion title="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": "<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"
      }
      ```
    </Accordion>

    `amount_in` is what your customer must send: 111.19863 USDC to receive exactly 100 EUR, with the fee breakdown itemised.
  </Step>

  <Step title="Create the autoramp from the quote">
    `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 '<signed-quote-payload-from-step-1>' | jq -r '.id')

    echo "$AUTORAMP_ID"
    ```

    <Warning>
      The quote is digitally signed. Any modification, including changing values or omitting fields, causes the request to fail.
    </Warning>
  </Step>

  <Step title="Share the deposit address">
    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.
  </Step>
</Steps>

<Tip>
  **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.
</Tip>

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

<Note>
  The `Idempotency-Key` header is required. Send the same key to safely retry a request without creating a duplicate autoramp.
</Note>

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

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

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

<Note>
  The `account_identifier` you provide in `recipient_account` must match a bank account you've already registered via the [Fiat Addresses API](/fiat-addresses).
</Note>

### Response

<CodeGroup>
  ```json theme={null}
  {
    "id": "9a7b6c5d-e3f1-4a2b-8c7d-6e5f4a3b2c1d",
    "status": "Authorized",
    "deposit_rails": [],
    "created_at": "2025-01-20T13:45:23Z"
  }
  ```
</CodeGroup>

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

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

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

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

<CodeGroup>
  ```json 400 Bad Request theme={null}
  "recipient fiat account not verified"
  ```
</CodeGroup>

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

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

## Simulate it in the Dashboard

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

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

<CardGroup cols={2}>
  <Card title="Quotes" icon="tag" href="/quotes">
    Full quote reference: expiry policies, multi-currency input, attaching quotes over time
  </Card>

  <Card title="Fiat Addresses" icon="building-columns" href="/fiat-addresses">
    Register bank accounts per payment rail and track registration status
  </Card>

  <Card title="Onramp" icon="arrow-right-to-bracket" href="/onramp">
    The reverse route: a named virtual account that settles into stablecoins
  </Card>

  <Card title="Transaction Status" icon="chart-line" href="/transaction-status">
    Status mapping, settlement times, and the polling API
  </Card>

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

  <Card title="End of Day Settlement" icon="receipt" href="/end-of-day-settlement">
    How batched payouts and daily settlement work
  </Card>
</CardGroup>
