Skip to main content

On-Ramp (Buy) Flow

This guide walks through the complete on-ramp integration — allowing your customers to buy crypto with fiat. The flow uses ramp intents to track the entire lifecycle from creation to completion.

Flow Overview

Two swimlanes. On your server, step 1 POSTs to /ramp-intents and receives an intentId and redirectUrl; the customer is redirected to your frontend or the widget. There, step 2 GETs /ramp-intents/:id for the intent details and widgetUrl, step 3 either embeds that widgetUrl or calls the API directly, step 4 is the customer completing payment, and step 5 GETs /on-ramp/transactions/:id until it reports completed. A panel below expands step 3 into the two calls the widget makes for you: 3a GET /on-ramp/quotes returns pricing options, and 3b POST /on-ramp/checkout returns a transactionId and redirectUrl.
Buying crypto: intent on your server, purchase in your frontend

Step 1: Create a Ramp Intent (Server-Side)

Create a ramp intent from your backend. This requires your API key and merchant ID.

// Server-side — requires API key
const createBuyIntent = 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: 'buy',
wallets: [{ network: 'ethereum', address: walletAddress }],
fiatCurrency: 'USD',
cryptoCurrency: 'eth_ethereum',
amount: 100,
customerEmail,
successUrl: 'https://yourapp.com/buy/success',
cancelUrl: 'https://yourapp.com/buy/cancel',
}),
});

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

Step 2: Redirect Customer to Widget

Send the customer to the redirectUrl from Step 1, or fetch the intent on your frontend to get the widget URL for iframe embedding.

// Option A: Simple redirect
window.location.href = intentData.redirectUrl;

// Option B: Fetch intent and embed widget in iframe
const fetchAndEmbed = async (intentId) => {
const response = await fetch(
`https://api.zenpayz.com/payment/api/v1/ramp-intents/${intentId}`
);
const { data } = await response.json();

// Terminal lifecycle states
if (data.intent.status === 'expired') {
showError('This session has expired. Please start again.');
return;
}
if (data.intent.status === 'cancelled') {
// When KYC is declined the intent transitions to cancelled with
// cancelReason='kyc_rejected'. See get-ramp-intent for details.
showError(
data.intent.cancelReason === 'kyc_rejected'
? 'Identity verification could not be completed.'
: 'This session has been cancelled.'
);
return;
}

// KYC gate — surface a friendly state-specific message
switch (data.intent.kycStatus) {
case 'in_review':
showInfo('Verification under review — we\'ll continue automatically once approved.');
return;
case 'pending':
// Customer needs to start / finish verification. If `kycRequired` is
// present in the response and `kycVerificationUrl` is set, embed it;
// otherwise call initiate-kyc to create a session.
break;
}

if (data.widgetUrl) {
const iframe = document.createElement('iframe');
iframe.src = data.widgetUrl;
iframe.style.cssText = 'width:100%;height:600px;border:none;border-radius:12px;';
iframe.allow = 'payment';
document.getElementById('widget-container').appendChild(iframe);
}
};

Step 3: Get Quotes (API Mode)

If you're building a custom UI instead of using the widget, fetch buy quotes to show pricing options.

const getQuotes = async (source, destination, amount) => {
const params = new URLSearchParams({ source, destination, amount: String(amount) });

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

// data.quotes = array of quote options
data.quotes.forEach((quote) => {
console.log(`${quote.onramp}: ${quote.outputAmount} crypto`);
console.log(` Rate: ${quote.rate}, Fee: ${quote.totalFee}`);
console.log(` Payment: ${quote.paymentMethod}`);
if (quote.recommended) console.log(' ⭐ Recommended');
});

return data.quotes;
};

const quotes = await getQuotes('usd', 'eth_ethereum', 100);

Step 4: Create Checkout

After the customer selects a quote, create a checkout to start the payment.

const createCheckout = async (selectedQuote, walletAddress) => {
const response = await fetch(
'https://api.zenpayz.com/payment/api/v1/on-ramp/checkout',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
onramp: selectedQuote.onramp,
source: 'usd',
destination: 'eth_ethereum',
amount: 100,
type: 'buy',
paymentMethod: selectedQuote.paymentMethod,
walletAddress,
}),
}
);

const { data } = await response.json();
// Redirect to payment page
window.location.href = data.redirectUrl;
};

Step 5: Poll Transaction Status

After the customer completes payment, poll the transaction status until it reaches a terminal state.

const pollTransaction = async (transactionId) => {
const terminalStatuses = ['completed', 'failed', 'expired', 'refunded'];

while (true) {
const response = await fetch(
`https://api.zenpayz.com/payment/api/v1/on-ramp/transactions/${transactionId}`
);
const { data } = await response.json();

console.log(`Status: ${data.status}`);

if (terminalStatuses.includes(data.status)) {
return data;
}

// Wait 5 seconds before next poll
await new Promise((resolve) => setTimeout(resolve, 5000));
}
};

const result = await pollTransaction('txn_abc123');

if (result.status === 'completed') {
console.log(`Success! ${result.outAmount} crypto delivered`);
console.log(`TX Hash: ${result.transactionHash}`);
} else {
console.log(`Transaction ${result.status}`);
}

Step 6: Receive Webhook Notification

Instead of polling (or in addition to it), you can receive a webhook when the ramp completes or fails. Configure your webhook URL in the merchant dashboard.

ramp.completed — crypto has been sent to the customer's wallet:

{
"event_type": "ramp.completed",
"payment_data": {
"merchant_id": "m_abc123",
"intentId": "ri_1710345678000_a1b2c3d4e5f6g7h8",
"type": "buy",
"status": "completed",
"fiatCurrency": "USD",
"cryptoCurrency": "eth_ethereum",
"amount": 100,
"chain": "ethereum",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28",
"fireblocksTxId": "fb_tx_9a8b7c6d5e4f",
"completedAt": "2026-03-16T14:30:00.000Z"
}
}

ramp.failed — crypto send failed:

{
"event_type": "ramp.failed",
"payment_data": {
"merchant_id": "m_abc123",
"intentId": "ri_1710345678000_a1b2c3d4e5f6g7h8",
"type": "buy",
"status": "failed",
"fiatCurrency": "USD",
"cryptoCurrency": "eth_ethereum",
"amount": 100,
"reason": "Crypto send failed",
"failedAt": "2026-03-16T14:30:00.000Z"
}
}
Webhook vs Polling

We recommend using webhooks as the primary notification mechanism and polling as a fallback. Webhooks are delivered as soon as the status changes — no delay. See the Webhooks guide for setup instructions, signature verification, and retry behavior.

Intent Status Lifecycle

created  →  active  →  completed
→ cancelled
→ expired (auto, after 1 hour)
StatusDescription
createdIntent created, not yet accessed
activeCustomer has accessed the intent (first GET)
completedTransaction completed successfully
cancelledManually cancelled by merchant or customer
expiredAuto-expired after 1 hour

Error Handling

  • Intent expired: Create a new intent and redirect the customer
  • Quote expired: Fetch fresh quotes — prices change frequently
  • Payment failed: Check the transaction status for details; the customer can retry
  • Network errors: Implement retry with exponential backoff

Next Steps