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

# Stable to Stable

> Setup a stables swap channel. Send Stablecoin 1 and receive Stablecoin 2 across chains.

export const props_1 = undefined

export const createButton_0 = "New Swap Channel"

export const routeCard_0 = "Stable to Stable"

export const createFields_0 = "the source token and chain, the destination token and chain, and the registered wallet that receives the output"

export const routeExample_0 = "USDC (Solana) → EURC (Base)"

export const noFundsNote_1 = "No testnet crypto is involved."

export const detailFields_0 = "both chains and the destination wallet"

export const props_0 = undefined

export const rampNoun_0 = "channel"

export const noFundsNote_0 = "No testnet crypto is involved."

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

<Steps>
  <Step>
    The user gets a **deposit address** on Solana owned by MoonPay Enterprise
  </Step>

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

  <Step>
    The user sends 1000 USDC to the deposit address
  </Step>

  <Step>
    As soon as funds arrive, MoonPay Enterprise converts them to EURC on Base and delivers them to the destination wallet
  </Step>
</Steps>

## 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} 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. See [Onramp](/onramp#onboard-a-customer-from-scratch) for a full onboarding walkthrough.
  </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="Create the channel">
    Either create a standing autoramp, or request an exact-out quote and create a quote-source autoramp. 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>

## Automation route: standing autoramp

Create an autoramp that converts USDC on Solana to EURC on Base at the current mid-market rate.

### Request

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

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

<Note>
  `POST /api/autoramps` requires an `Idempotency-Key` header. Use a unique UUID per request to prevent duplicate autoramps.
</Note>

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

### Response

<Accordion title="Full 201 Authorized response">
  <CodeGroup>
    ```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"
    }
    ```
  </CodeGroup>
</Accordion>

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

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

Once the autoramp reaches `Approved`, the response includes the deposit wallet address under `deposit_rails`. 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.

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

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

<Steps>
  <Step title="Request the quote">
    `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=<customer_id>&source_currency_code=USDC&source_currency_chain=Solana&destination_currency_code=EURC&destination_currency_chain=Base&recipient_account_id=<crypto_address_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`.
  </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')
    ```

    <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 and amount">
    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.
  </Step>
</Steps>

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

## Test the full flow in Sandbox

Creating the {props_0.rampNoun_0} 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_0}

    ```bash theme={null} 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} 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 a `transaction` event when the deposit landed, followed by `transaction_status` events as it moved through `FundsReviewInProgress`, `ConversionInProgress`, `PayoutInProgress`, and `Completed`.

    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>
  Once the destination token is delivered, `transaction_hash` contains the on-chain hash.
</Note>

## 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_0}** on the **{props_1.routeCard_0}** card and choose {props_1.createFields_0}.
  </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_0}</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_1}
  </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_0}. 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`                                         |
| 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

<CardGroup cols={2}>
  <Card title="Stablecoins and Blockchains" icon="link" href="/stablecoins-and-blockchains">
    Which tokens and chains are supported as sources and destinations
  </Card>

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

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

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

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

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