Skip to main content
The Tender on-ramp converts a fiat bank payment (e.g. NGN transfer) into cryptocurrency and delivers it to a wallet address on the customer’s chosen blockchain — all in a single integration.

How it works

1

Get a quote

Call POST /onramp/quote with the fiat amount and target cryptocurrency. Tender returns a live exchange rate, an estimated crypto output, and a quoteId that is valid for a short window (typically 60 seconds).
2

Initiate the on-ramp

Call POST /onramp/initiate with the quoteId, the customer’s wallet address, and their contact details. Tender creates a virtual bank account and returns the transfer details to show your customer.
3

Customer pays

Customer makes a local bank transfer to the virtual account. No crypto wallet or exchange account is needed on their side — just a standard bank transfer.
4

Tender processes automatically

Once payment is confirmed, Tender runs the pipeline automatically:
  • Forwards the fiat to the swap provider
  • Swaps fiat → intermediate crypto (USDT on Tron)
  • Delivers the final crypto to the targetAddress on the targetChain
5

Poll for completion

Call GET /onramp/{reference} to track progress. The stages object shows exactly where the pipeline is at any moment.

Pipeline stages

After payment is confirmed, the on-ramp moves through these stages in order:
StageWhat happens
paymentFiat payment confirmed by the payment provider
payoutFiat forwarded to the swap provider’s account
swapFiat swapped to intermediate crypto (e.g. USDT on Tron)
swapWithdrawSwapped crypto withdrawn to Tender’s swap wallet (skipped by default)
cryptoSendCrypto sent from Tender’s swap wallet to the customer’s targetAddress
Each stage has a status of pending, in_progress, completed, failed, or skipped.

Overall status values

StatusMeaning
pending_paymentWaiting for the customer’s bank transfer
processingPayment confirmed; pipeline is running
crypto_sentCrypto dispatched to targetAddress
completedFully settled
failedTerminal failure — see failureReason and stages[*].lastError

Key concepts

Quotes are required and single-use

Before initiating, you must call /onramp/quote to lock in a rate. The returned quoteId is:
  • Time-limited — expires after time_to_lock seconds (shown as expiresAt)
  • Single-use — consumed the moment it is passed to /onramp/initiate
Attempting to initiate with an expired or already-used quote returns a 400 error.

Virtual bank accounts expire

The virtual bank account returned by /onramp/initiate expires after 1 hour. If the customer does not pay within that window, the on-ramp request moves to failed status.

Supported chains and currencies

Use the discovery endpoints to present your customers with what is available:
  • GET /onramp/chains — chains that support on-ramp delivery
  • GET /onramp/coins — cryptocurrencies available, optionally filtered by chain

Integration

This section walks through every step of a real on-ramp integration, from discovery to polling for completion. All examples use the sandbox environment.

Prerequisites

  • A Tender merchant account with API credentials
  • Your TENDER_ACCESS_ID and TENDER_ACCESS_SECRET set as environment variables

Authentication guide

All requests require an HMAC-SHA256 signature. Follow the authentication guide for full details and code examples in Node.js, Python, and PHP before continuing.
The examples below assume you have a makeHeaders() helper and a BASE constant as described in that guide:
const BASE = 'https://sandbox-api.tender.cash/v1/api';
// makeHeaders() → { 'x-access-id', 'x-request-id', 'x-timestamp', 'authorization', ... }

Step 1 — Discover available chains and coins

Show your customers which blockchains and currencies they can receive.
// Fetch supported chains
const chainsRes = await fetch(`${BASE}/onramp/chains`, {
  headers: makeHeaders(),
});
const { data: chains } = await chainsRes.json();
// chains = [{ id: "tron", name: "Tron", icon: "...", ... }]

// Fetch supported coins (optionally filter by chain)
const coinsRes = await fetch(`${BASE}/onramp/coins?chain=tron`, {
  headers: makeHeaders(),
});
const { data: coins } = await coinsRes.json();
// coins = [{ id: "usdt", name: "Tether USD", symbol: "USDT", ... }]

Step 2 — Get a quote

Once the customer has chosen a target currency and entered their fiat amount, fetch a live quote.
const quoteRes = await fetch(`${BASE}/onramp/quote`, {
  method: 'POST',
  headers: makeHeaders(),
  body: JSON.stringify({
    fiatAmount:     50000,      // NGN, in whole units (not kobo)
    fiatCurrency:   'NGN',      // defaults to NGN if omitted
    targetCurrency: 'USDT',
    targetChain:    'tron',
  }),
});

const { data: quote } = await quoteRes.json();
console.log(quote);
/*
{
  quoteId:               "b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c",
  fiatCurrency:          "NGN",
  fiatAmount:            50000,
  targetCurrency:        "USDT",
  targetChain:           "tron",
  rate:                  0.000597,
  estimatedCryptoAmount: "29.850000",
  expiresAt:             "2025-06-10T10:05:00.000Z"
}
*/
Quotes expire at expiresAt. Display a countdown and call /onramp/quote again if the customer lets it lapse before confirming.

Step 3 — Show the quote to your customer

Display the estimated output and ask for confirmation before initiating.
You pay:        ₦50,000 NGN
You receive:  ≈ 29.85 USDT
Network:         Tron (TRC-20)
Rate expires:    in 58 seconds
The estimated amount is based on the rate at quote time. The actual amount delivered may differ slightly if the quote expires and a fresh rate is applied internally.

Step 4 — Initiate the on-ramp

After the customer confirms, call /onramp/initiate with the quoteId and their wallet address.
const initiateRes = await fetch(`${BASE}/onramp/initiate`, {
  method: 'POST',
  headers: makeHeaders(),
  body: JSON.stringify({
    quoteId:       quote.quoteId,
    targetAddress: 'TRDFGhjkytywooiueonuoo',  // customer's USDT/Tron address
    customer: {
      email: 'customer@example.com',
      name:  'Ada Obi',
    },
    metadata: { orderId: 'ORD-9821' },  // your own reference, optional
  }),
});

const { data: onramp } = await initiateRes.json();
console.log(onramp);
/*
{
  reference:   "a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d",
  status:      "pending_payment",
  bankTransfer: {
    accountNumber: "0123456789",
    accountName:   "Tender / Ada Obi",
    bankName:      "Wema Bank"
  },
  expiresAt: "2025-06-10T11:00:00.000Z",
  amount:    { value: 50000, currency: "NGN" }
}
*/
Error cases to handle:
Error messageCauseFix
"Quote not found or already used"quoteId was already consumed or never existedFetch a fresh quote
"Quote has expired"expiresAt passed before /initiate was calledFetch a fresh quote
Joi validation errorquoteId missing, not a UUID, or targetAddress emptyFix the request body

Step 5 — Display bank transfer details

Show the returned bankTransfer details so the customer knows where to send money.
Please transfer ₦50,000 to:

  Bank:           Wema Bank
  Account name:   Tender / Ada Obi
  Account number: 0123456789

  This account expires at 11:00 AM UTC.
  Do not send a different amount.
Store the reference — you will use it to poll for status.

Step 6 — Poll for completion

Poll GET /onramp/{reference} until status is crypto_sent or completed.
async function pollOnramp(reference, intervalMs = 10000) {
  while (true) {
    const res  = await fetch(`${BASE}/onramp/${reference}`, { headers: makeHeaders() });
    const { data } = await res.json();

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

    if (data.status === 'crypto_sent' || data.status === 'completed') {
      console.log('On-ramp complete!');
      return data;
    }

    if (data.status === 'failed') {
      throw new Error(`On-ramp failed: ${data.failureReason}`);
    }

    await new Promise(r => setTimeout(r, intervalMs));
  }
}

const result = await pollOnramp(onramp.reference);
Rather than polling, configure a webhook to receive a notification when the status changes. See Webhooks for setup instructions.

Inspecting stage progress

The stages object in the status response shows the exact state of each pipeline step.
{
  "stages": {
    "payment":      { "status": "completed",   "completedAt": "2025-06-10T10:02:30Z" },
    "payout":       { "status": "completed",   "completedAt": "2025-06-10T10:03:10Z" },
    "swap":         { "status": "in_progress", "startedAt":   "2025-06-10T10:03:15Z", "attempts": 1 },
    "swapWithdraw": { "status": "skipped" },
    "cryptoSend":   { "status": "pending" }
  }
}

Error handling

ScenarioRecommended action
status: "failed"Read failureReason and display it; offer the customer a retry
status: "pending_payment" after 1 hourThe virtual account expired; start a new on-ramp
Stage status: "failed"Read stages[*].lastError for the provider-level reason
Network error pollingRetry with exponential backoff; the on-ramp reference is idempotent