Skip to main content
Tender generates a unique wallet address for each payment request. Your customer sends the exact crypto amount to that address, Tender detects the on-chain transfer, and marks the transaction complete. No custodial account or key management is required on your side.

How it works

1

Initiate a payment

Call POST /payment/initiate with the amount, chain, and coin. Tender returns a walletAddress and a txId to track the transaction.
2

Customer sends crypto

Display the walletAddress and exact amount to your customer. They send from their own wallet — any compatible wallet on that chain works.
3

Tender detects the transfer

Tender monitors the address on-chain. When the payment arrives, the transaction status updates automatically. You can poll or listen via webhook.
4

Validate the payment

Call POST /payment/validate/{id} to confirm the payment is received and the transaction is complete on your end.

Prerequisites

Authentication guide

All requests require an HMAC-SHA256 signature. See the authentication guide for code examples.
const BASE = 'https://sandbox-api.tender.cash/v1/api';
// makeHeaders() → { 'x-access-id', 'x-request-id', 'x-timestamp', 'authorization', ... }

Step 1 — Initiate a payment

const res = await fetch(`${BASE}/payment/initiate`, {
  method: 'POST',
  headers: makeHeaders(),
  body: JSON.stringify({
    amount: '10.00',
    chain:  'tron',
    coin:   'usdt',
    reference: 'ORDER-9821', // your own order ID, optional
  }),
});

const { data: payment } = await res.json();
console.log(payment);
/*
{
  txId:          "66aec7de809b7f45c42a49f9",
  walletAddress: "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT",
  amount:        "10.00",
  coin:          "usdt",
  chain:         "tron",
  status:        "pending",
  expiresAt:     "2025-06-10T11:00:00.000Z"
}
*/

const { txId, walletAddress, amount } = payment;

Step 2 — Show payment details to your customer

Display the wallet address and exact amount. The customer must send exactly the specified amount — partial sends are tracked as partial payments.
Please send:

  10.00 USDT
  Network: Tron (TRC-20)
  To:      TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT

  Do not send from an exchange. Use a self-custody wallet.
  Payment expires in 1 hour.
The walletAddress is unique to this transaction. Never reuse addresses across payments.

Step 3 — Poll for payment status

async function pollPayment(txId, intervalMs = 8000) {
  while (true) {
    const res = await fetch(`${BASE}/payment/validate/${txId}`, {
      method: 'POST',
      headers: makeHeaders(),
    });
    const { data } = await res.json();

    console.log('Status:', data.status, '| Received:', data.amountReceived);

    if (data.status === 'completed') return data;
    if (data.status === 'failed')    throw new Error('Payment failed');

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

const result = await pollPayment(txId);
Use webhooks instead of polling for production. Configure your webhook URL and Tender will push a notification the moment the payment status changes. See Webhooks.

Payment status values

StatusMeaning
pendingAddress generated; awaiting customer transfer
partialSome funds received but not the full amount
completedFull amount received and confirmed on-chain
failedPayment expired or rejected

Partial payments

If the customer sends less than the required amount, the transaction moves to partial status. The balanceRequired field shows what is still owed. You can:
  • Ask the customer to send the remaining balance to the same address
  • Mark the order as underpaid and handle it in your own logic

Error handling

ScenarioAction
Customer sends wrong amountCheck amountReceived vs amount; handle partial if needed
Payment expiresInitiate a new payment; do not reuse the old address
Wrong networkFunds sent on the wrong chain cannot be recovered automatically — always display the network clearly
status: "failed"Initiate a fresh payment request