<!-- ZenPays documentation · https://docs.zenpayz.com/docs/examples/off-ramp-flow -->

# Off-Ramp (Sell) Flow

This guide walks through the complete off-ramp integration — allowing your customers to sell crypto and receive fiat. The sell flow uses a **two-phase process**: Phase 1 generates a crypto deposit address, and Phase 2 submits beneficiary details for the fiat payout.

## Flow Overview

<Diagram
  slug="off-ramp-flow"
  alt="Two swimlanes above two phase bands. On your server, step 1 POSTs to /ramp-intents with type sell to get an intentId; the customer is redirected to your frontend, where step 2 GETs /off-ramp/quotes for pricing. Phase one, deposit: step 3 POSTs /off-ramp/sell/checkout to get a deposit address, step 4 the customer sends crypto to it, step 5 waits for deposit confirmation. Phase two, payout: step 6 GETs the payout preview for the required beneficiary fields, step 7 renders the beneficiary form for the customer to fill in, step 8 POSTs /off-ramp/sell/payout to send the fiat, and step 9 the intent completes. The required fields differ by corridor, which is why step 6 exists."
  caption="Selling crypto: deposit first, then the fiat payout"
/>

## Step 1: Create a Sell Intent (Server-Side)

Create a ramp intent with `type: "sell"` from your backend.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
// Server-side — requires API key
const createSellIntent = async (customerEmail, walletAddress) => {
  const response = await fetch('https://api.zenpayz.com/payment/api/v1/ramp-intents', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.ZENPAYS_API_KEY}`,
      'x-merchant-id': process.env.ZENPAYS_MERCHANT_ID,
      'x-request-id': `req_${Date.now()}`,
    },
    body: JSON.stringify({
      type: 'sell',
      wallets: [{ network: 'tron', address: walletAddress }],
      fiatCurrency: 'USD',
      cryptoCurrency: 'usdt_tron',
      customerEmail,
      successUrl: 'https://yourapp.com/sell/success',
      cancelUrl: 'https://yourapp.com/sell/cancel',
    }),
  });

  const { data } = await response.json();
  return data; // { intentId, redirectUrl, expiresAt }
};
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import requests
import os
import time

def create_sell_intent(customer_email: str, wallet_address: str):
    response = requests.post(
        "https://api.zenpayz.com/payment/api/v1/ramp-intents",
        headers={
            "Authorization": f"Bearer {os.environ['ZENPAYS_API_KEY']}",
            "x-merchant-id": os.environ["ZENPAYS_MERCHANT_ID"],
            "x-request-id": f"req_{int(time.time())}",
        },
        json={
            "type": "sell",
            "wallets": [{"network": "tron", "address": wallet_address}],
            "fiatCurrency": "USD",
            "cryptoCurrency": "usdt_tron",
            "customerEmail": customer_email,
            "successUrl": "https://yourapp.com/sell/success",
            "cancelUrl": "https://yourapp.com/sell/cancel",
        },
    )
    return response.json()["data"]
```

  </TabItem>
</Tabs>

## Step 2: Get Sell Quotes

Fetch quotes to show the customer how much fiat they'll receive for their crypto.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
const getSellQuotes = async (cryptoAmount) => {
  const params = new URLSearchParams({
    source: 'usdt_tron',
    destination: 'USD',
    amount: String(cryptoAmount),
  });

  const response = await fetch(
    `https://api.zenpayz.com/payment/api/v1/off-ramp/quotes?${params}`
  );
  const { data } = await response.json();

  data.quotes.forEach((quote) => {
    console.log(`Provider: ${quote.onramp}`);
    console.log(`  You send: ${quote.inputAmount} USDT`);
    console.log(`  You receive: ${quote.outputAmount} USD`);
    console.log(`  Rate: ${quote.rate}, Fee: ${quote.totalFee}`);
  });

  return data.quotes;
};

const quotes = await getSellQuotes(20);
const selectedQuote = quotes[0]; // Or let user choose
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def get_sell_quotes(crypto_amount: float):
    response = requests.get(
        "https://api.zenpayz.com/payment/api/v1/off-ramp/quotes",
        params={
            "source": "usdt_tron",
            "destination": "USD",
            "amount": crypto_amount,
        },
    )
    quotes = response.json()["data"]["quotes"]

    for quote in quotes:
        print(f"Provider: {quote['onramp']}")
        print(f"  Send: {quote['inputAmount']} USDT → Receive: {quote['outputAmount']} USD")
        print(f"  Rate: {quote['rate']}, Fee: {quote['totalFee']}")
    return quotes
```

  </TabItem>
</Tabs>

## Step 3: Phase 1 — Create Sell Deposit

Generate a deposit address for the customer to send crypto to.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
const createDeposit = async (intentId, quote) => {
  const response = await fetch(
    'https://api.zenpayz.com/payment/api/v1/off-ramp/sell/checkout',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        intentId,
        cryptoCurrency: 'USDT',
        chain: 'tron',
        fiatCurrency: 'USD',
        cryptoAmount: quote.inputAmount,
        fiatAmount: quote.outputAmount,
        rate: quote.rate,
        totalFee: quote.totalFee,
      }),
    }
  );

  const { data } = await response.json();

  // Display to customer
  console.log(`Send ${data.expectedCryptoAmount} ${data.currency} to:`);
  console.log(`Address: ${data.depositAddress}`);
  console.log(`Network: ${data.chain}`);
  if (data.memo) console.log(`Memo: ${data.memo}`);

  // Save for Phase 2
  return {
    cryptoDepositId: data.cryptoDepositId,
    depositAddress: data.depositAddress,
    expectedAmount: data.expectedCryptoAmount,
  };
};
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def create_deposit(intent_id: str, quote: dict):
    response = requests.post(
        "https://api.zenpayz.com/payment/api/v1/off-ramp/sell/checkout",
        json={
            "intentId": intent_id,
            "cryptoCurrency": "USDT",
            "chain": "tron",
            "fiatCurrency": "USD",
            "cryptoAmount": quote["inputAmount"],
            "fiatAmount": quote["outputAmount"],
            "rate": quote["rate"],
            "totalFee": quote["totalFee"],
        },
    )
    data = response.json()["data"]

    print(f"Send {data['expectedCryptoAmount']} {data['currency']} to:")
    print(f"Address: {data['depositAddress']}")
    print(f"Network: {data['chain']}")

    return {
        "cryptoDepositId": data["cryptoDepositId"],
        "depositAddress": data["depositAddress"],
    }
```

  </TabItem>
</Tabs>

:::tip Display to Customer
Show the deposit address with a QR code and a countdown timer based on `expiresAt`. Remind the customer to send the exact `expectedCryptoAmount` on the correct chain.
:::

## Step 4: Wait for Deposit Confirmation

After the customer sends crypto, the system automatically detects and confirms the deposit. You can poll the intent status or listen for WebSocket events.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
const waitForDeposit = async (intentId) => {
  console.log('Waiting for crypto deposit...');

  while (true) {
    const response = await fetch(
      `https://api.zenpayz.com/payment/api/v1/ramp-intents/${intentId}`
    );
    const { data } = await response.json();
    const phase = data.intent.metadata?.phase;

    if (phase === 'awaiting_payout' || phase === 'completed') {
      console.log('Deposit confirmed!');
      return data.intent;
    }

    if (data.intent.status === 'expired' || data.intent.status === 'cancelled') {
      throw new Error(`Intent ${data.intent.status}`);
    }

    // Poll every 10 seconds
    await new Promise((resolve) => setTimeout(resolve, 10000));
  }
};
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def wait_for_deposit(intent_id: str):
    print("Waiting for crypto deposit...")

    while True:
        response = requests.get(
            f"https://api.zenpayz.com/payment/api/v1/ramp-intents/{intent_id}"
        )
        data = response.json()["data"]
        phase = (data["intent"].get("metadata") or {}).get("phase")

        if phase in ("awaiting_payout", "completed"):
            print("Deposit confirmed!")
            return data["intent"]

        if data["intent"]["status"] in ("expired", "cancelled"):
            raise Exception(f"Intent {data['intent']['status']}")

        time.sleep(10)
```

  </TabItem>
</Tabs>

## Step 5: Phase 2a — Get Payout Preview

Once the deposit is confirmed, fetch the required beneficiary fields for the fiat payout.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
const getPayoutPreview = async (intentId, fiatCurrency, country = 'US') => {
  const params = new URLSearchParams({ intentId, fiatCurrency, country });

  const response = await fetch(
    `https://api.zenpayz.com/payment/api/v1/off-ramp/sell/payout-preview?${params}`
  );
  const { data } = await response.json();

  console.log(`Provider: ${data.tspDisplayName}`);
  console.log(`Payout: ${data.fiatAmount} ${data.fiatCurrency}`);
  console.log(`Fees: ${data.fees.total}`);
  console.log(`Required fields: ${data.requiredFields.length}`);

  return data;
};

const preview = await getPayoutPreview(intentId, 'USD', 'US');

// Dynamically render form from requiredFields
preview.requiredFields.forEach((field) => {
  // Create form inputs based on field.type, field.label, field.required, etc.
  console.log(`  ${field.required ? '*' : ' '} ${field.label} (${field.fieldName})`);
});
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def get_payout_preview(intent_id: str, fiat_currency: str, country: str = "US"):
    response = requests.get(
        "https://api.zenpayz.com/payment/api/v1/off-ramp/sell/payout-preview",
        params={
            "intentId": intent_id,
            "fiatCurrency": fiat_currency,
            "country": country,
        },
    )
    data = response.json()["data"]

    print(f"Provider: {data['tspDisplayName']}")
    print(f"Payout: {data['fiatAmount']} {data['fiatCurrency']}")
    print(f"Fees: {data['fees']['total']}")
    print(f"\nRequired fields:")
    for field in data["requiredFields"]:
        print(f"  {'*' if field['required'] else ' '} {field['label']} ({field['fieldName']})")
    return data
```

  </TabItem>
</Tabs>

## Step 6: Phase 2b — Submit Payout

Collect the beneficiary details from the customer and submit the payout.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
const submitPayout = async (intentId, cryptoDepositId, beneficiaryDetails) => {
  const response = await fetch(
    'https://api.zenpayz.com/payment/api/v1/off-ramp/sell/payout',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        intentId,
        cryptoDepositId,
        fiatCurrency: 'USD',
        country: 'US',
        beneficiaryDetails,
      }),
    }
  );

  const { data } = await response.json();

  console.log(`Payout ID: ${data.payoutId}`);
  console.log(`Status: ${data.status}`);
  console.log(`Amount: ${data.amount} ${data.currency}`);

  return data;
};

// Example: customer filled in the form
const payout = await submitPayout(intentId, deposit.cryptoDepositId, {
  receiverFirstName: 'John',
  receiverLastName: 'Doe',
  receiverCountry: 'US',
  receiverAccountNumber: '123456789',
  receiverBankName: 'Bank of America',
  receiverBankCode: 'BOFAUS3N',
  receiverAddressLine1: '123 Main St',
  receiverCity: 'New York',
  receiverState: 'NY',
  receiverPinCode: '10001',
  remittancePurpose: 'PAYP001 - Family Support',
  sourceOfFund: 'PAYF001 - Salary',
  relationship: 'PAYR001 - Self',
});

// Intent is now completed — redirect customer
window.location.href = '/sell/success';
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def submit_payout(intent_id: str, crypto_deposit_id: str, beneficiary_details: dict):
    response = requests.post(
        "https://api.zenpayz.com/payment/api/v1/off-ramp/sell/payout",
        json={
            "intentId": intent_id,
            "cryptoDepositId": crypto_deposit_id,
            "fiatCurrency": "USD",
            "country": "US",
            "beneficiaryDetails": beneficiary_details,
        },
    )
    data = response.json()["data"]
    print(f"Payout ID: {data['payoutId']}")
    print(f"Status: {data['status']}")
    print(f"Amount: {data['amount']} {data['currency']}")
    return data

# Example
payout = submit_payout(intent_id, deposit["cryptoDepositId"], {
    "receiverFirstName": "John",
    "receiverLastName": "Doe",
    "receiverCountry": "US",
    "receiverAccountNumber": "123456789",
    "receiverBankName": "Bank of America",
    "receiverBankCode": "BOFAUS3N",
    "receiverAddressLine1": "123 Main St",
    "receiverCity": "New York",
    "receiverState": "NY",
    "receiverPinCode": "10001",
    "remittancePurpose": "PAYP001 - Family Support",
    "sourceOfFund": "PAYF001 - Salary",
    "relationship": "PAYR001 - Self",
})
```

  </TabItem>
</Tabs>

## Step 7: Receive Webhook Notification

After the payout is submitted (or if it fails), ZenPay delivers a webhook to your configured endpoint.

**`ramp.completed`** — fiat payout has been submitted successfully:

```json
{
  "event_type": "ramp.completed",
  "payment_data": {
    "merchant_id": "m_abc123",
    "intentId": "ri_1710345678000_a1b2c3d4e5f6g7h8",
    "type": "sell",
    "status": "completed",
    "fiatCurrency": "USD",
    "cryptoCurrency": "usdt_tron",
    "amount": 20,
    "payoutId": "po_xyz789",
    "payoutAmount": 19.50,
    "payoutCurrency": "USD",
    "completedAt": "2026-03-16T14:30:00.000Z"
  }
}
```

**`ramp.failed`** — payout submission failed:

```json
{
  "event_type": "ramp.failed",
  "payment_data": {
    "merchant_id": "m_abc123",
    "intentId": "ri_1710345678000_a1b2c3d4e5f6g7h8",
    "type": "sell",
    "status": "failed",
    "fiatCurrency": "USD",
    "cryptoCurrency": "usdt_tron",
    "amount": 20,
    "reason": "Payout submission failed",
    "failedAt": "2026-03-16T14:30:00.000Z"
  }
}
```

:::tip Webhook vs Polling
We recommend using webhooks as the primary notification mechanism. Webhooks are delivered as soon as the ramp completes or fails — no delay. See the [Webhooks guide](/docs/guides/webhooks) for setup, signature verification, and retry behavior.
:::

## Complete Example

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```javascript
// Full off-ramp flow
const runSellFlow = async () => {
  // Step 1: Create intent (server-side)
  const intent = await createSellIntent('customer@example.com', 'TLfMk...');
  console.log(`Intent: ${intent.intentId}`);

  // Step 2: Get quotes
  const quotes = await getSellQuotes(20);
  const selectedQuote = quotes[0];

  // Step 3: Phase 1 — create deposit
  const deposit = await createDeposit(intent.intentId, selectedQuote);
  console.log(`Send USDT to: ${deposit.depositAddress}`);

  // Step 4: Wait for deposit confirmation
  await waitForDeposit(intent.intentId);

  // Step 5: Phase 2a — get payout preview
  const preview = await getPayoutPreview(intent.intentId, 'USD', 'US');

  // Step 6: Phase 2b — submit payout (with customer's details)
  const payout = await submitPayout(intent.intentId, deposit.cryptoDepositId, {
    receiverFirstName: 'John',
    receiverLastName: 'Doe',
    receiverCountry: 'US',
    receiverAccountNumber: '123456789',
    receiverBankName: 'Bank of America',
    // ... other fields from preview.requiredFields
  });

  console.log(`Done! Payout ${payout.payoutId} is ${payout.status}`);
};
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
def run_sell_flow():
    # Step 1: Create intent
    intent = create_sell_intent("customer@example.com", "TLfMk...")
    print(f"Intent: {intent['intentId']}")

    # Step 2: Get quotes
    quotes = get_sell_quotes(20)
    selected_quote = quotes[0]

    # Step 3: Phase 1 — create deposit
    deposit = create_deposit(intent["intentId"], selected_quote)

    # Step 4: Wait for deposit confirmation
    wait_for_deposit(intent["intentId"])

    # Step 5: Phase 2a — get payout preview
    preview = get_payout_preview(intent["intentId"], "USD", "US")

    # Step 6: Phase 2b — submit payout
    payout = submit_payout(intent["intentId"], deposit["cryptoDepositId"], {
        "receiverFirstName": "John",
        "receiverLastName": "Doe",
        "receiverCountry": "US",
        "receiverAccountNumber": "123456789",
        "receiverBankName": "Bank of America",
    })

    print(f"Done! Payout {payout['payoutId']} is {payout['status']}")

run_sell_flow()
```

  </TabItem>
</Tabs>

## Error Handling

| Phase | Error | Recovery |
|-------|-------|----------|
| Intent creation | Validation error | Check required fields and retry |
| Deposit (Phase 1) | Intent expired | Create a new intent |
| Deposit (Phase 1) | Deposit address generation failed | Retry — endpoint is idempotent |
| Waiting | Deposit not detected | Ensure correct chain and address; check with block explorer |
| Payout preview | Deposit not confirmed | Wait longer for blockchain confirmation |
| Payout (Phase 2) | FX quotation failed | Retry — the system will obtain a new quote |
| Payout (Phase 2) | Insufficient amount (fees > value) | Customer sent too little crypto |

## Next Steps

- [Create Ramp Intent](/docs/rest-api/endpoints/ramp/create-ramp-intent) — API reference
- [Sell Quotes](/docs/rest-api/endpoints/ramp/sell-quotes) — Quote endpoint details
- [Create Sell Deposit](/docs/rest-api/endpoints/ramp/sell-deposit) — Phase 1 reference
- [Get Payout Preview](/docs/rest-api/endpoints/ramp/payout-preview) — Phase 2a reference
- [Submit Sell Payout](/docs/rest-api/endpoints/ramp/sell-payout) — Phase 2b reference
- [On-Ramp Flow](/docs/examples/on-ramp-flow) — Buy crypto integration guide
