# Authentication Source: https://docs.tender.cash/api-reference/authentication How Tender signs and authenticates API requests. All requests require authentication. You can get your API credentials (Access ID and Access Secret) from the Tender dashboard. See [Getting API Credentials](/get-started/api-credentials). Tender uses two authentication methods, depending on the endpoint: | Method | Headers required | Used by | | - | - | - | | **Signed (HMAC)** | `x-access-id`, `x-request-id`, `x-timestamp`, `authorization` (HMAC signature) | Agents (`/v1/api/agent`), Conversions (`/v1/api/conversions`), Onramp (`/v1/api/onramp`), Payouts (`/v1/api/payout`), Subwallets (`/v1/api/wallet`), Webhooks (`/v1/api/webhook`) | | **Basic (Access ID)** | `x-access-id` only | Payments (`/v1/api/payment`), System (`/v1/api/system`: chains, currencies, rates) | Each endpoint page shows which method it uses in a note at the top. > [!NOTE] > > Your access ID must belong to an API key that is linked to an outlet (agent). > Requests made with a key that has no outlet are rejected with `401 INVALID_REQUEST_SIGNATURE`, > on both Signed and Basic endpoints. *** ## Basic (Access ID) Authentication Payment and System endpoints (`/v1/api/payment/*` and `/v1/api/system/*`) only require your access ID: ```http theme={null} x-access-id: Content-Type: application/json ``` No request signing is needed for these endpoints, and your Access Secret is never sent. Treat your access ID as a credential anyway: anyone who has it can call these endpoints on your behalf. *** ## Signed (HMAC) Authentication All other endpoints require an HMAC-SHA256 signature. Each request must include these headers: ```http theme={null} x-access-id: x-request-id: x-timestamp: authorization: Content-Type: application/json ``` | Header | Description | | - | - | | `x-access-id` | Your public access ID. | | `x-request-id` | A new UUID v4 for every request. | | `x-timestamp` | The current time in ISO 8601 format, for example `2026-10-15T09:45:53.000Z`. It must be within **5 minutes** of Tender's server time, in either direction. | | `authorization` | The Base64-encoded HMAC-SHA256 signature described below. | ### What is signed The signature covers a JSON string built from the three header values, with the keys in exactly this order and no whitespace: ```json theme={null} {"timeStamp":"","requestId":"","accessId":""} ``` This is the output of JavaScript's `JSON.stringify({ timeStamp, requestId, accessId })`. Use the **exact** strings you send in the headers: if you sign one timestamp and send another, the signature won't match. > [!WARNING] > > Only these three values are signed. The request body, HTTP method and path are **not** part > of the signature. Always call the API over HTTPS, and keep your Access Secret on your server. ### Signature algorithm ```text theme={null} message = '{"timeStamp":"' + timestamp + '","requestId":"' + requestId + '","accessId":"' + accessId + '"}' signature = Base64( HMAC_SHA256( key = accessSecret, message = message ) ) ``` The output is standard Base64 (with `+`, `/` and `=` padding), not hex and not URL-safe Base64. *** ### Signed request example Each example below signs a request and registers a webhook with `POST /v1/api/webhook`. ```javascript Node.js theme={null} import crypto from 'crypto'; function signedHeaders(accessId, accessSecret) { const requestId = crypto.randomUUID(); const timeStamp = new Date().toISOString(); // e.g. 2026-10-15T09:45:53.123Z // Key order matters: timeStamp, requestId, accessId const message = JSON.stringify({ timeStamp, requestId, accessId }); const signature = crypto .createHmac('sha256', accessSecret) .update(message) .digest('base64'); return { 'x-access-id': accessId, 'x-request-id': requestId, 'x-timestamp': timeStamp, 'authorization': signature, 'Content-Type': 'application/json', }; } const accessId = process.env.TENDER_ACCESS_ID; const accessSecret = process.env.TENDER_ACCESS_SECRET; const response = await fetch('https://secureapi.tender.cash/v1/api/webhook', { method: 'POST', headers: signedHeaders(accessId, accessSecret), body: JSON.stringify({ description: 'Production webhook', url: 'https://your-domain.com/webhooks/tender', eventTypes: ['transaction_completed'], headers: { 'X-Webhook-Secret': 'your-secret-value' }, }), }); console.log(response.status, await response.json()); ``` ```python Python theme={null} import base64 import hashlib import hmac import json import os import uuid from datetime import datetime, timezone import requests def signed_headers(access_id, access_secret): request_id = str(uuid.uuid4()) # Millisecond UTC timestamp, e.g. 2026-10-15T09:45:53.123Z timestamp = ( datetime.now(timezone.utc) .isoformat(timespec='milliseconds') .replace('+00:00', 'Z') ) # Key order matters, and separators must have no spaces so the # string matches JavaScript's JSON.stringify output exactly. payload = { 'timeStamp': timestamp, 'requestId': request_id, 'accessId': access_id, } message = json.dumps(payload, separators=(',', ':')) signature = base64.b64encode( hmac.new( access_secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256, ).digest() ).decode('utf-8') return { 'x-access-id': access_id, 'x-request-id': request_id, 'x-timestamp': timestamp, 'authorization': signature, 'Content-Type': 'application/json', } access_id = os.environ['TENDER_ACCESS_ID'] access_secret = os.environ['TENDER_ACCESS_SECRET'] response = requests.post( 'https://secureapi.tender.cash/v1/api/webhook', headers=signed_headers(access_id, access_secret), json={ 'description': 'Production webhook', 'url': 'https://your-domain.com/webhooks/tender', 'eventTypes': ['transaction_completed'], 'headers': {'X-Webhook-Secret': 'your-secret-value'}, }, ) print(response.status_code, response.json()) ``` ```php PHP theme={null} format('Y-m-d\TH:i:s.v\Z'); // Key order matters: timeStamp, requestId, accessId. // JSON_UNESCAPED_SLASHES keeps the string identical to JSON.stringify. $message = json_encode([ 'timeStamp' => $timeStamp, 'requestId' => $requestId, 'accessId' => $accessId, ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $signature = base64_encode(hash_hmac('sha256', $message, $accessSecret, true)); return [ 'x-access-id: ' . $accessId, 'x-request-id: ' . $requestId, 'x-timestamp: ' . $timeStamp, 'authorization: ' . $signature, 'Content-Type: application/json', ]; } $accessId = getenv('TENDER_ACCESS_ID'); $accessSecret = getenv('TENDER_ACCESS_SECRET'); $ch = curl_init('https://secureapi.tender.cash/v1/api/webhook'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, signedHeaders($accessId, $accessSecret)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'description' => 'Production webhook', 'url' => 'https://your-domain.com/webhooks/tender', 'eventTypes' => ['transaction_completed'], 'headers' => ['X-Webhook-Secret' => 'your-secret-value'], ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . ' ' . $response; ``` *** ## Authentication errors Authentication failures use the standard [error envelope](/api-reference/errors#error-envelope). | HTTP status | `message` | When it happens | | - | - | - | | `401` | `NOT_ALLOWED` | Signed endpoints: one or more of `authorization`, `x-access-id`, `x-request-id` or `x-timestamp` is missing. | | `401` | `INVALID_REQUEST_SIGNATURE` | The timestamp can't be parsed or is more than 5 minutes from server time; the access ID is unknown or deleted; the key isn't linked to an outlet; or the signature doesn't match. On Basic endpoints, also returned when `x-access-id` is missing. | | `403` | `NOT_ALLOWED` | Your merchant account is suspended. `data.message` is `"This merchant account has been suspended."` | Example: ```json theme={null} { "status": "error", "message": "INVALID_REQUEST_SIGNATURE", "data": {} } ``` ### Troubleshooting `INVALID_REQUEST_SIGNATURE` * **Check your clock.** Sync your server with NTP; a drift of more than 5 minutes fails every request. * **Sign the exact header values.** Build the timestamp once and use the same string in the header and in the signed JSON. * **Check the JSON string.** It must be `{"timeStamp":…,"requestId":…,"accessId":…}` in that order, with no spaces. In Python use `separators=(',', ':')`. * **Use Base64, not hex.** In Node.js, `digest('base64')`; in PHP, pass `true` as the fourth argument to `hash_hmac` before Base64-encoding. * **Use the right environment.** Sandbox credentials only work against `https://sandbox-api.tender.cash`, and production credentials only against `https://secureapi.tender.cash`. *** ## Important notes * **Keep your Access Secret secure.** Never expose it in client-side code or public repositories. * **Generate a new request ID** (UUID v4) for every request. * **Rate limits apply per merchant.** See [Rate limits](/api-reference/rate-limits). * **Payout requests need an `Idempotency-Key` header** in addition to the signed headers. See [Idempotency](/api-reference/idempotency). For other error responses, see [Errors](/api-reference/errors). # Add Webhook Source: https://docs.tender.cash/api-reference/endpoint/add-webhook POST /v1/api/webhook Register or replace the webhook endpoint for your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Rate limit: 120 requests per minute for all webhook endpoints. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Media type of the request body **Example:** `"application/json"` Each merchant account has **one** webhook. If you already have one, this request **replaces** its `description`, `url`, `eventTypes` and `headers`; the webhook keeps its `_id`. See [Webhooks](/get-started/webhooks) for delivery behaviour. ## Body All four fields are required. **description** `string` *required* A description for your own reference **Example:** `"Production webhook"` **url** `string` *required* The URL Tender sends events to. It must use `https`, and must not point to `localhost` or a private, loopback or link-local address. **Example:** `"https://your-domain.com/webhooks/tender"` **eventTypes** `string[]` *required* The events to receive. Names are lowercase and case-sensitive, and aren't checked: a misspelt name is accepted but never delivered. Get valid names from [List Webhook Events](/api-reference/endpoint/list-webhook-events) or see [Webhook Events](/get-started/webhook-events). Send an empty array to stop deliveries without deleting the webhook. **Example:** `["transaction_completed", "transaction_cancelled"]` **headers** `object` *required* Custom headers Tender sends with every delivery, as string key-value pairs. Use them to authenticate deliveries, because webhooks are not signed. `Host`, `Content-Length`, `Connection`, `Transfer-Encoding`, `Upgrade`, `Proxy-Authorization` and `X-Forwarded-*` headers are not sent. **Example:** `{ "X-Webhook-Secret": "a-long-random-value" }` ## Response Returns HTTP `200` with the saved webhook. **status** `string` *required* Always `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* The saved webhook. Additional fields may be present; ignore fields you don't recognise. **data properties** **\_id** `string` The webhook ID. Use it with [Delete Webhook](/api-reference/endpoint/delete-webhook). **Example:** `"691069ff947a702cfad9e861"` **merchantId** `string` Your merchant ID **Example:** `"6538e8f9bdec6d1a21978a64"` **description** `string` Description of the webhook **Example:** `"Production webhook"` **url** `string` Webhook endpoint URL **Example:** `"https://your-domain.com/webhooks/tender"` **eventTypes** `string[]` Subscribed event names **Example:** `["transaction_completed", "transaction_cancelled"]` **headers** `object` Custom headers sent with every delivery **Example:** `{ "X-Webhook-Secret": "a-long-random-value" }` **isActive** `boolean` Always `true` for a registered webhook **Example:** `true` **createdAt** `string` ISO 8601 timestamp when the webhook was first registered **Example:** `"2026-10-15T10:16:31.594Z"` **updatedAt** `string` ISO 8601 timestamp when the webhook was last changed **Example:** `"2026-10-15T10:18:35.286Z"` ## Errors | Status | When | | - | - | | `400` | A field is missing or has the wrong type, or the URL is not allowed, for example `"Webhook URL must use https"`. | | `401`, `403` | See [Authentication](/api-reference/authentication#authentication-errors). | | `429` | Rate limit exceeded. | ```bash cURL theme={null} curl -X POST https://secureapi.tender.cash/v1/api/webhook \ -H "x-access-id: $TENDER_ACCESS_ID" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0=" \ -H "Content-Type: application/json" \ -d '{ "description": "Production webhook", "url": "https://your-domain.com/webhooks/tender", "eventTypes": ["transaction_completed", "transaction_cancelled"], "headers": { "X-Webhook-Secret": "a-long-random-value" } }' ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "_id": "691069ff947a702cfad9e861", "merchantId": "6538e8f9bdec6d1a21978a64", "description": "Production webhook", "url": "https://your-domain.com/webhooks/tender", "eventTypes": ["transaction_completed", "transaction_cancelled"], "headers": { "X-Webhook-Secret": "a-long-random-value" }, "isActive": true, "isDeleted": false, "createdAt": "2026-10-15T10:16:31.594Z", "updatedAt": "2026-10-15T10:18:35.286Z" } } ``` ```json 400 theme={null} { "status": "error", "message": "Webhook URL must use https", "data": null } ``` # Create Outlet Source: https://docs.tender.cash/api-reference/endpoint/agents/create POST /v1/api/agent/create Create an outlet (agent or sub-business) under your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Creates an outlet (also called an agent or sub-business) under your merchant account. Outlets let you separate sales by store, channel or team, each with its own default fiat currency. You can create two outlet types through the API: | `type` | Use for | Required fields | | - | - | - | | `online` (default) | A website, app or other online integration | `name`, `location`, `country`, `outletIntent` | | `whatsapp` | A WhatsApp sales channel | `name`, `phoneNumber`, `outletIntent: "Whatsapp"` | > [!NOTE] > > Physical point-of-sale (`pos`) outlets can't be created through the API. Create them from the > Tender merchant dashboard. Agent endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Media type of the request body **Example:** `"application/json"` ## Body **type** `string` (default: online) Outlet type: `online` or `whatsapp`. Defaults to `online`. **Example:** `"online"` **name** `string` *required* Display name of the outlet **Example:** `"Lagos Fashion Store"` **outletIntent** `string` How the outlet will be used. **Required for `online` and `whatsapp` outlets.** One of `Woocommerce`, `Webhook`, `API integration`, `Shopify`, `Wix`, `Squarespace` or `Whatsapp`. For `whatsapp` outlets it must be `Whatsapp`. **Example:** `"API integration"` **location** `string` Location of the outlet. **Required for `online` outlets.** **Example:** `"Lagos"` **country** `string` Country of the outlet. **Required for `online` outlets.** **Example:** `"Nigeria"` **phoneNumber** `string` Phone number in E.164 format: a `+`, the country code and the number, with no spaces. **Required for `whatsapp` outlets**, and each WhatsApp number can belong to only one outlet. Optional for `online` outlets, but must be E.164 if sent. **Example:** `"+2348012345678"` **email** `string` Email address of the outlet. Optional. Must be unique: an email already used by another outlet is rejected. **Example:** `"lagosfashion@store.com"` **currency** `string` (default: USD) The outlet's default fiat currency. Payments initiated for this outlet are priced in it. Must be a fiat currency Tender supports (see [Fetch All Currencies](/api-reference/endpoint/fetch-currencies) with `type=fiat`). Defaults to `USD`. **Example:** `"NGN"` **avatar** `string` URL of the outlet's avatar image. Optional. **Example:** `"https://example.com/avatar.png"` String fields are limited to 500 characters and may not contain `$ \ ( ) < >`. ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* The new outlet **data properties** **id** `string` Outlet ID **Example:** `"66f33b1fa9446a6b6b8f32cd"` **reference** `string` The outlet's Tender reference code **Example:** `"vbibtucm9ynq3k2x7p"` **name** `string` Display name of the outlet **Example:** `"Lagos Fashion Store"` **type** `string` Outlet type **Example:** `"online"` **email** `string` Email address, lowercased. Omitted if none was sent. **Example:** `"lagosfashion@store.com"` **phoneNumber** `string` Phone number. Omitted if none was sent. **Example:** `"+2348012345678"` **location** `string` Location of the outlet **Example:** `"Lagos"` **country** `string` Country of the outlet **Example:** `"Nigeria"` **active** `boolean` Whether the outlet is active **Example:** `true` **merchantId** `string` ID of your merchant account **Example:** `"6538e8f9bdec6d1a21978a64"` **currency** `string` The outlet's default fiat currency **Example:** `"NGN"` **totalSales** `string` Total sales. `"0"` for a new outlet. **Example:** `"0"` **defaultFiatCurrency** `object` Default fiat currency settings **defaultFiatCurrency properties** **currency** `string` Fiat currency code **Example:** `"NGN"` **rate** `string` Fiat units per USD at creation **Example:** `"1500"` **useSystemRate** `boolean` Whether payments use Tender's live exchange rate. Always `true` for outlets created through the API. **Example:** `true` **createdAt** `string` ISO 8601 timestamp when the outlet was created **Example:** `"2026-10-15T09:45:53.000Z"` ## Errors All errors return HTTP 400. | Message | Cause | | - | - | | `phoneNumber must include a valid country code` | `phoneNumber` isn't in E.164 format | | `outletIntent must be a string` / `outletIntent must be one of the following values: …` | `outletIntent` is missing or not an allowed value | | `location must be a string` / `country must be a string` | Missing `location` or `country` on an `online` outlet | | `Select WhatsApp as the social channel` | A `whatsapp` outlet without `outletIntent: "Whatsapp"` | | `WhatsApp number is linked to another outlet. Use a different number.` | The WhatsApp number is already in use | | `Agent Already exists` / `Email Already taken` | The email is already used by an outlet | | `Currency is not supported` | `currency` isn't a supported fiat currency | ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/agent/create \ -H "Content-Type: application/json" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0=" \ -d '{ "type": "online", "name": "Lagos Fashion Store", "email": "lagosfashion@store.com", "location": "Lagos", "country": "Nigeria", "currency": "NGN", "outletIntent": "API integration" }' ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "id": "66f33b1fa9446a6b6b8f32cd", "name": "Lagos Fashion Store", "email": "lagosfashion@store.com", "country": "Nigeria", "location": "Lagos", "type": "online", "active": true, "reference": "vbibtucm9ynq3k2x7p", "merchantId": "6538e8f9bdec6d1a21978a64", "currency": "NGN", "totalSales": "0", "defaultFiatCurrency": { "currency": "NGN", "rate": "1500", "useSystemRate": true }, "createdAt": "2026-10-15T09:45:53.000Z" } } ``` # List Outlets Source: https://docs.tender.cash/api-reference/endpoint/agents/list GET /v1/api/agent List the outlets (agents or sub-businesses) under your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns a page of the outlets under your merchant account, most recently updated first. Agent endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query Parameters **page** `number` (default: 1) Page number, starting at 1 **Example:** `1` **limit** `number` (default: 12) Number of outlets per page **Example:** `12` **search** `string` Case-insensitive text to match against the outlet's name, email, phone number, reference, location or country **Example:** `"lagos"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* A page of outlets **data properties** **agents** `array` The outlets on this page **outlet object** **id** `string` Outlet ID **Example:** `"66f33b1fa9446a6b6b8f32cd"` **reference** `string` The outlet's Tender reference code **Example:** `"vbibtucm9ynq3k2x7p"` **name** `string` Display name of the outlet **Example:** `"Lagos Fashion Store"` **email** `string` Email address. Omitted if the outlet has none. **Example:** `"lagosfashion@store.com"` **phoneNumber** `string` Phone number. Omitted if the outlet has none. **Example:** `"+2348012345678"` **location** `string` Location of the outlet **Example:** `"Lagos"` **country** `string` Country of the outlet **Example:** `"Nigeria"` **active** `boolean` Whether the outlet is active **Example:** `true` **merchantId** `string` ID of your merchant account **Example:** `"6538e8f9bdec6d1a21978a64"` **currency** `string` The outlet's default fiat currency **Example:** `"NGN"` **totalSales** `string` Total sales recorded for the outlet **Example:** `"0"` **defaultFiatCurrency** `object` Default fiat currency settings: `currency` (string), `rate` (string) and `useSystemRate` (boolean) **createdAt** `string` ISO 8601 timestamp when the outlet was created **Example:** `"2026-10-15T09:45:53.000Z"` **page** `number | string` Current page number. Echoes the `page` query parameter as sent (a string) when you pass one. **Example:** `1` **pages** `number` Total number of pages **Example:** `1` **limit** `number | string` Outlets per page. Echoes the `limit` query parameter as sent (a string) when you pass one. **Example:** `12` **total** `number` Total number of matching outlets **Example:** `1` ## Example request ```bash theme={null} curl "https://sandbox-api.tender.cash/v1/api/agent?search=lagos" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0=" ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "agents": [ { "id": "66f33b1fa9446a6b6b8f32cd", "name": "Lagos Fashion Store", "email": "lagosfashion@store.com", "location": "Lagos", "country": "Nigeria", "active": true, "reference": "vbibtucm9ynq3k2x7p", "merchantId": "6538e8f9bdec6d1a21978a64", "currency": "NGN", "totalSales": "0", "defaultFiatCurrency": { "currency": "NGN", "rate": "1500", "useSystemRate": true }, "createdAt": "2026-10-15T09:45:53.000Z" } ], "pages": 1, "page": 1, "limit": 12, "total": 1 } } ``` # Cancel Payment Source: https://docs.tender.cash/api-reference/endpoint/cancel-payment POST /v1/api/payment/cancel/{id} Cancel a pending payment request > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Cancels a payment request that is still `pending`, for example when the customer abandons checkout. Once cancelled, Tender stops checking the deposit address for this payment, and its `reference` can be used for a new payment. * Only `pending` payments can be cancelled. A payment that has received funds, or has already completed, can't be cancelled. * The call is idempotent: cancelling a payment that is already cancelled returns success again. * A successful cancellation sends the `transaction_cancelled` [webhook event](/get-started/webhook-events). Repeating the call doesn't send it again. You can only cancel payments that belong to your merchant account. Payment endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Path Parameters **id** `string` *required* The `txId` returned when the payment was initiated **Example:** `"66aec7de809b7f45c42a49f9"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* The cancelled payment **data properties** **txId** `string` The payment's transaction ID **Example:** `"66aec7de809b7f45c42a49f9"` **status** `string` Always `cancelled` **Example:** `"cancelled"` **referenceId** `string` Your `reference`, or an empty string **Example:** `"ORDER-9821"` **cancelledAt** `string` ISO 8601 timestamp of the cancellation. Omitted when the payment was already cancelled. **Example:** `"2026-10-15T09:12:00.000Z"` ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Invalid tx id` | The ID is unknown or doesn't belong to your merchant account | | 400 | `Invalid resource ID format` | The ID isn't a valid transaction ID | | 400 | `Transaction with status '' can no longer be cancelled` | The payment isn't `pending` (for example, it is `completed` or `partial-payment`) | Error responses use the standard envelope with `data: null`. See [Errors](/api-reference/errors). ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/payment/cancel/66aec7de809b7f45c42a49f9 \ -H "x-access-id: your-access-id-here" ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "txId": "66aec7de809b7f45c42a49f9", "status": "cancelled", "referenceId": "ORDER-9821", "cancelledAt": "2026-10-15T09:12:00.000Z" } } ``` # Create Conversion Source: https://docs.tender.cash/api-reference/endpoint/conversions/create POST /v1/api/conversions Swap crypto to another coin or chain, or convert it to a saved payout account — no OTP required > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). There are two conversion types: * **`crypto`** swaps `coin` on `chain` into `toCoin` on `toChain`. The result always settles to your own merchant wallet on `toChain`. * **`fiat`** converts `coin` on `chain` and sends the result to one of your saved, active payout accounts (`payoutAccountId`). If that payout account is a bank account, the funds are paid out in its fiat currency; if it is a crypto account, they are sent to its address on its chain and coin. Tender picks the conversion route automatically; you can't choose a provider. The conversion is created with status `pending` and processed asynchronously. There are no conversion webhooks: poll [Get Conversion](/api-reference/endpoint/conversions/get-one) until `status` is `completed` or `failed`. The conversion must be worth **more than \$20 USD**. Call [Calculate Conversion Fee](/api-reference/endpoint/conversions/fee) first to show the fee and the expected output. ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Must be `application/json` **Example:** `"application/json"` ## Body **type** `string` *required* Conversion type. **Allowed values:** `crypto`, `fiat` **Example:** `"crypto"` **chain** `string` *required* Source chain ID **Example:** `"ethereum"` **coin** `string` *required* Source coin ID **Example:** `"usdt"` **amount** `string` *required* Amount to convert, in the source coin. Must be sent as a string. **Example:** `"100"` **toChain** `string` Destination chain ID. **Required when `type` is `crypto`.** **Example:** `"solana"` **toCoin** `string` Destination coin ID. **Required when `type` is `crypto`.** **Example:** `"sol"` **payoutAccountId** `string` ID of one of your active payout accounts. **Required when `type` is `fiat`.** **Example:** `"66aec7de809b7f45c42a49f9"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"Conversion created successfully"` **data** `object` *required* **data properties** **conversion** `object` Summary of the new conversion **conversion properties** **conversionId** `string` Conversion ID. Use it with [Get Conversion](/api-reference/endpoint/conversions/get-one). **Example:** `"66aec7de809b7f45c42a49f9"` **amount** `number` Source amount **Example:** `100` **usdAmount** `number` Source amount in USD **Example:** `100` **status** `string` Always `pending` on creation **Example:** `"pending"` **destinationType** `string` `crypto` or `fiat`. For `type: "fiat"`, this is the type of the payout account. **Example:** `"crypto"` ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Invalid chain reference` / `Invalid currency reference` | Unknown source `chain` or `coin` | | 400 | `Invalid to chain reference` / `Invalid to currency reference` | Unknown `toChain` or `toCoin` (`crypto`) | | 400 | `Merchant wallet not found` | You have no merchant wallet on `toChain` (`crypto`) | | 400 | `Currency is not enabled for swap` | The source coin can't be converted (`fiat`) | | 400 | `Invalid or inactive payout account` | `payoutAccountId` is not one of your active payout accounts (`fiat`) | | 400 | `Conversion request must be more than $20 USD` | The amount is worth \$20 or less | | 400 | Validation message, e.g. `toChain must be a string` | A required field is missing or has the wrong type | | 500 | `Failed to create conversion` | The conversion could not be priced or created. Check the pair with the fee endpoint and retry. | ```bash Crypto swap theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/conversions" \ -H "Content-Type: application/json" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " \ -d '{"type":"crypto","chain":"ethereum","coin":"usdt","amount":"100","toChain":"solana","toCoin":"sol"}' ``` ```bash Fiat theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/conversions" \ -H "Content-Type: application/json" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " \ -d '{"type":"fiat","chain":"ethereum","coin":"usdt","amount":"100","payoutAccountId":"66aec7de809b7f45c42a49f9"}' ``` ```json 200 theme={null} { "status": "success", "message": "Conversion created successfully", "data": { "conversion": { "conversionId": "66aec7de809b7f45c42a49f9", "amount": 100, "usdAmount": 100, "status": "pending", "destinationType": "crypto" } } } ``` ```json 400 theme={null} { "status": "error", "message": "Conversion request must be more than $20 USD", "data": {} } ``` # Calculate Conversion Fee Source: https://docs.tender.cash/api-reference/endpoint/conversions/fee GET /v1/api/conversions/fee/{chain}/{coin}/{amount} Quote the fee, rate and expected output for a conversion before you create it > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). The quote is an estimate at current prices. The final figures are fixed when you [create the conversion](/api-reference/endpoint/conversions/create). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **chain** `string` *required* Source chain ID **Example:** `"ethereum"` **coin** `string` *required* Source coin ID (for example `usdt`, not the symbol `USDT`) **Example:** `"usdt"` **amount** `string` *required* Amount to convert, in the source coin. Must be greater than zero. **Example:** `"100"` ## Query parameters **toChain** `string` Destination chain ID for a crypto swap. Use `fiat` to quote a fiat conversion. **Example:** `"solana"` **toCoin** `string` Destination coin ID for a crypto swap, or a fiat currency ID (for example `ngn`) when `toChain` is `fiat`. If omitted, the destination is the source coin. **Example:** `"sol"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* Conversion quote **data properties** **from** `object` Source side of the quote **from properties** **chain** `string` **Example:** `"ethereum"` **currency** `string` **Example:** `"usdt"` **symbol** `string` **Example:** `"USDT"` **amount** `number` Amount you convert **Example:** `100` **amountUSD** `number` USD value of `amount` **Example:** `100` **to** `object` Destination side of the quote, after fees **to properties** **chain** `string` Destination chain, `fiat` for fiat quotes. Absent when you don't send `toChain`. **Example:** `"solana"` **currency** `string` **Example:** `"sol"` **symbol** `string` **Example:** `"SOL"` **amount** `number` Estimated amount you receive, in the destination currency **Example:** `0.6633` **amountUSD** `number` USD value after the fee **Example:** `99.5` **fee** `object` Conversion fee **fee properties** **fee** `number` Fee in the source coin **Example:** `0.5` **feeUSD** `number` Fee in USD **Example:** `0.5` **rate** `object` Exchange rate **rate properties** **value** `string` Destination units per USD, after fees. Two decimals for fiat, six for crypto. **Example:** `"0.006667"` **perSourceUnit** `string` Value of one source unit in the destination currency, before fees. Use it to display "1 `from.symbol` = `perSourceUnit` `rate.symbol`". **Example:** `"0.006700"` **symbol** `string` Destination symbol **Example:** `"SOL"` **overridden** `boolean` `true` if the quote uses a fixed rate instead of the live market rate **Example:** `false` ## Errors All return HTTP 400. | Message | Cause | | - | - | | `Amount must be greater than zero` | `amount` is zero, negative or not a number | | `Invalid chain` | Unknown source chain | | `Invalid currency` | Unknown source coin | | `Exchange price not available` | No price for the source coin | | `Invalid destination chain` | Unknown `toChain` | | `Invalid destination currency` | Unknown `toCoin` | | `Exchange price not available for destination currency` | No price for `toCoin` | | `Invalid destination exchange rate` | The destination price is unusable | ```bash Crypto swap theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/fee/ethereum/usdt/100?toChain=solana&toCoin=sol" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```bash Fiat theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/fee/ethereum/usdt/100?toChain=fiat&toCoin=ngn" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "from": { "chain": "ethereum", "currency": "usdt", "symbol": "USDT", "amount": 100, "amountUSD": 100 }, "to": { "chain": "solana", "currency": "sol", "symbol": "SOL", "amount": 0.6633, "amountUSD": 99.5 }, "fee": { "fee": 0.5, "feeUSD": 0.5 }, "rate": { "value": "0.006667", "perSourceUnit": "0.006700", "symbol": "SOL", "overridden": false } } } ``` ```json 400 theme={null} { "status": "error", "message": "Invalid destination currency", "data": {} } ``` # Get Conversion Internal Transaction Source: https://docs.tender.cash/api-reference/endpoint/conversions/get-internal GET /v1/api/conversions/{id}/internal/{internalId} One on-chain transaction Tender recorded to settle one of your conversions > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns one internal transaction of a conversion, for example to check whether its `txHash` has been broadcast. Get the IDs from [List Conversion Internal Transactions](/api-reference/endpoint/conversions/list-internal). An ID is only found under the conversion it belongs to. All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The conversion ID: `conversionId` from [Create Conversion](/api-reference/endpoint/conversions/create) **Example:** `"6a12f0c4e4b0a1b2c3d4e5f2"` **internalId** `string` *required* Internal transaction ID: `id` from [List Conversion Internal Transactions](/api-reference/endpoint/conversions/list-internal) **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The internal transaction **data properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Internal transaction not found` | The conversion isn't yours, the ID is malformed, or `internalId` isn't one of this conversion's internal transactions | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/6a12f0c4e4b0a1b2c3d4e5f2/internal/6a12f0c4e4b0a1b2c3d4e6a7" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "6a12f0c4e4b0a1b2c3d4e5f2", "parentType": "conversion", "role": "conversion_send", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } } ``` ```json 404 theme={null} { "status": "error", "message": "Internal transaction not found", "data": {} } ``` # Get Conversion Source: https://docs.tender.cash/api-reference/endpoint/conversions/get-one GET /v1/api/conversions/{id} Retrieve one of your conversions by ID, including the status of each processing stage > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. Polling counts against it, so poll no more often than every 10 seconds. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The conversion ID (`conversionId` from [Create Conversion](/api-reference/endpoint/conversions/create)) **Example:** `"66aec7de809b7f45c42a49f9"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The conversion record **data properties** **\_id** `string` Conversion ID **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` `crypto` or `fiat` **Example:** `"crypto"` **status** `string` Overall status: `pending`, `processing`, `completed` or `failed`. Use this to decide when the conversion is finished. **Example:** `"processing"` **amount** `number` Source amount, in the source coin **Example:** `100` **usdAmount** `number` Source amount in USD at creation **Example:** `100` **rate** `number` Destination units per USD (after fees) used to price the conversion **Example:** `0.006667` **fee** `number` Conversion fee, in the source coin **Example:** `0.5` **feeUSD** `number` Conversion fee in USD **Example:** `0.5` **chain** `string` Internal ID of the source chain record. On this endpoint it is not expanded; use [List Conversions](/api-reference/endpoint/conversions/list) if you need the chain details. **Example:** `"64f7a1c2e4b0a1b2c3d4e5f6"` **currency** `string` Internal ID of the source coin record (not expanded on this endpoint) **Example:** `"64fc11d2e4b0a1b2c3d4e5f7"` **swapChain** `object` Destination chain, with `id`, `name`, `icon` and `chainType`. For fiat conversions its `id` is `fiat`. **Example:** `{ "id": "solana", "name": "Solana", "chainType": "sol" }` **swapCurrency** `string` Internal ID of the destination coin record (not expanded on this endpoint) **Example:** `"64fc11d2e4b0a1b2c3d4e5f8"` **convertedAmount** `number` Amount obtained from the conversion step, in the destination coin. Set once that step completes. **Example:** `0.6633` **swappedAmount** `number` Amount delivered to the destination after network and provider fees. Set at settlement. **Example:** `0.6621` **payoutAmount** `number` Fiat amount paid out (fiat conversions only). Set at settlement. **Example:** `152340.5` **bank** `object` Bank details of the payout account (fiat conversions only): `currency`, `accountName`, `accountNumber`, `bankName`. **destinationCrypto** `object` Crypto destination: `chain`, `coin` and `address`. For `type: "crypto"` this is your own merchant wallet on the destination chain. **Example:** `{ "chain": "solana", "coin": "sol", "address": "7xKX...9fQ" }` **failureReason** `string` Why the conversion failed. Present only when `status` is `failed`. **initiatedVia** `string` Where the conversion was created: `api`, `dashboard`, `admin` or `auto-payout` **Example:** `"api"` **processingStages** `object` Status of each processing stage. Each stage is an object with a single `status` field. **processingStages properties** **sendForConversion** `object` Funds are sent for conversion. Status: `pending`, `initiated`, `executed`, `completed` or `failed`. **Example:** `{ "status": "completed" }` **conversionState** `object` The conversion is executed. Status: `pending`, `executed`, `completed` or `failed`. **Example:** `{ "status": "completed" }` **settleCrypto** `object` Converted funds are settled. Status: `pending`, `executed`, `completed` or `failed`. **Example:** `{ "status": "executed" }` **settleFiat** `object` Fiat is paid out to the bank account. Present only for fiat conversions. Status: `pending`, `processed`, `completed` or `failed`. **Example:** `{ "status": "pending" }` **createdAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:20:15.650Z"` **updatedAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:25:00.000Z"` Additional fields may be present; ignore fields you don't recognise. ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Invalid Reference` | The ID is malformed, does not exist, or belongs to another merchant | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/66aec7de809b7f45c42a49f9" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "_id": "66aec7de809b7f45c42a49f9", "type": "crypto", "status": "processing", "amount": 100, "usdAmount": 100, "rate": 0.006667, "fee": 0.5, "feeUSD": 0.5, "chain": "64f7a1c2e4b0a1b2c3d4e5f6", "currency": "64fc11d2e4b0a1b2c3d4e5f7", "swapChain": { "id": "solana", "name": "Solana", "chainType": "sol" }, "swapCurrency": "64fc11d2e4b0a1b2c3d4e5f8", "convertedAmount": 0.6633, "destinationCrypto": { "chain": "solana", "coin": "sol", "address": "7xKX...9fQ" }, "initiatedVia": "api", "processingStages": { "sendForConversion": { "status": "completed" }, "conversionState": { "status": "completed" }, "settleCrypto": { "status": "executed" } }, "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:25:00.000Z" } } ``` ```json 400 theme={null} { "status": "error", "message": "Invalid Reference", "data": {} } ``` # List Conversions Source: https://docs.tender.cash/api-reference/endpoint/conversions/list GET /v1/api/conversions Retrieve your conversions with pagination, filters and aggregate totals > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. Polling counts against it. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query parameters **page** `number` *required* Page number, starting at 1. There is no default; always send it. **Example:** `1` **limit** `number` *required* Number of records per page. There is no default; always send it. **Example:** `10` **status** `string` Filter by conversion status. Passing `pending` returns both `pending` and `processing` records. **Allowed values:** `pending`, `processing`, `completed`, `failed` **Example:** `"completed"` **type** `string` Filter by conversion type. **Allowed values:** `crypto`, `fiat` **Example:** `"fiat"` **from** `string` Start of a `createdAt` date range (ISO 8601). Only applied when `to` is also sent. **Example:** `"2026-10-01T00:00:00.000Z"` **to** `string` End of a `createdAt` date range (ISO 8601). Only applied when `from` is also sent. **Example:** `"2026-10-15T23:59:59.999Z"` **search** `string` Case-insensitive search across the transaction ID, status, and the bank account name, bank name and account number of fiat conversions. **Example:** `"kuda"` Send only the query parameters listed above. ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* Paginated conversions and aggregate totals **data properties** **conversion** `object` Paginated list of conversions, newest first **conversion properties** **data** `array` Array of conversion objects **conversion object** **\_id** `string` Conversion ID **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` `crypto` (swap to another coin or chain) or `fiat` (sent to a payout account) **Example:** `"crypto"` **status** `string` One of `pending`, `processing`, `completed`, `failed` **Example:** `"completed"` **amount** `number` Source amount, in the source coin **Example:** `100` **usdAmount** `number` Source amount in USD at creation **Example:** `100` **rate** `number` Destination units per USD (after fees) used to price the conversion **Example:** `0.006667` **fee** `number` Conversion fee, in the source coin **Example:** `0.5` **feeUSD** `number` Conversion fee in USD **Example:** `0.5` **chain** `object` Source chain. Includes `id`, `name`, `icon` and `chainType`. **Example:** `{ "id": "ethereum", "name": "Ethereum", "chainType": "evm" }` **currency** `object` Source coin. Includes `id`, `name`, `symbol` and `icon`. **Example:** `{ "id": "usdt", "name": "Tether USD", "symbol": "USDT" }` **swapChain** `object` Destination chain. For fiat conversions its `id` is `fiat`. **Example:** `{ "id": "solana", "name": "Solana", "chainType": "sol" }` **swapCurrency** `object` Destination coin, or the fiat currency for fiat conversions **Example:** `{ "id": "sol", "name": "Solana", "symbol": "SOL" }` **convertedAmount** `number` Amount obtained from the conversion step, in the destination coin. Set once that step completes. **Example:** `0.6633` **swappedAmount** `number` Amount delivered to the destination after network and provider fees. Set at settlement. **Example:** `0.6621` **payoutAmount** `number` Fiat amount paid out (fiat conversions only). Set at settlement. **Example:** `152340.5` **bank** `object` Bank details of the payout account (fiat conversions only): `currency`, `accountName`, `accountNumber`, `bankName`. **Example:** `{ "currency": "ngn", "accountName": "Ada Obi", "accountNumber": "0123456789", "bankName": "KUDA MICROFINANCE BANK" }` **destinationCrypto** `object` Crypto destination: `chain`, `coin` and `address`. Present for crypto conversions and for fiat conversions to a crypto payout account. **Example:** `{ "chain": "solana", "coin": "sol", "address": "7xKX...9fQ" }` **failureReason** `string` Why the conversion failed. Present only when `status` is `failed`. **Example:** `"Destination address rejected"` **initiatedVia** `string` Where the conversion was created: `api`, `dashboard`, `admin` or `auto-payout` **Example:** `"api"` **processingStages** `object` Status of each processing stage. See [Get Conversion](/api-reference/endpoint/conversions/get-one) for the status values. **processingStages properties** **sendForConversion** `object` **Example:** `{ "status": "completed" }` **conversionState** `object` **Example:** `{ "status": "completed" }` **settleCrypto** `object` **Example:** `{ "status": "completed" }` **settleFiat** `object` Present only for fiat conversions. **Example:** `{ "status": "processed" }` **createdAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:20:15.650Z"` **updatedAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:31:02.114Z"` **pages** `number` Total number of pages **Example:** `3` **page** `number` Current page **Example:** `1` **limit** `number` Items per page **Example:** `10` **history** `object` Totals across all your `pending`, `processing` and `completed` conversions. These ignore the query filters. **history properties** **today** `number` USD value of conversions created today **Example:** `250` **allTime** `number` USD value of all conversions **Example:** `12840.5` **count** `number` Number of conversions **Example:** `42` **pending** `number` Number of conversions that are `pending` or `processing` **Example:** `2` Additional fields may be present; ignore fields you don't recognise. ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions?page=1&limit=10&status=completed" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "conversion": { "data": [ { "_id": "66aec7de809b7f45c42a49f9", "type": "crypto", "status": "completed", "amount": 100, "usdAmount": 100, "rate": 0.006667, "fee": 0.5, "feeUSD": 0.5, "chain": { "id": "ethereum", "name": "Ethereum", "chainType": "evm" }, "currency": { "id": "usdt", "name": "Tether USD", "symbol": "USDT" }, "swapChain": { "id": "solana", "name": "Solana", "chainType": "sol" }, "swapCurrency": { "id": "sol", "name": "Solana", "symbol": "SOL" }, "convertedAmount": 0.6633, "swappedAmount": 0.6621, "destinationCrypto": { "chain": "solana", "coin": "sol", "address": "7xKX...9fQ" }, "initiatedVia": "api", "processingStages": { "sendForConversion": { "status": "completed" }, "conversionState": { "status": "completed" }, "settleCrypto": { "status": "completed" } }, "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:31:02.114Z" } ], "pages": 1, "page": 1, "limit": 10 }, "history": { "today": 100, "allTime": 12840.5, "count": 42, "pending": 2 } } } ``` # List Conversion Internal Transactions Source: https://docs.tender.cash/api-reference/endpoint/conversions/list-internal GET /v1/api/conversions/{id}/internal The on-chain transactions Tender recorded to settle one of your conversions > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). The ledger transactions behind one of your conversions: the on-chain send that funds the conversion and, for crypto swaps, the transaction that credits the result. [Get Conversion](/api-reference/endpoint/conversions/get-one) only reports each stage's status. This endpoint returns the transactions themselves, with their hashes. Results are oldest first. A conversion with no internal transactions returns `200` with an empty `data` array. All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The conversion ID: `conversionId` from [Create Conversion](/api-reference/endpoint/conversions/create) **Example:** `"6a12f0c4e4b0a1b2c3d4e5f2"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `array` *required* Internal transactions, oldest first **item properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Conversion not found` | No conversion with this ID belongs to your merchant account, or the ID is malformed | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/6a12f0c4e4b0a1b2c3d4e5f2/internal" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "6a12f0c4e4b0a1b2c3d4e5f2", "parentType": "conversion", "role": "conversion_send", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } ] } ``` ```json 200 Empty theme={null} { "status": "success", "message": "success", "data": [] } ``` ```json 404 theme={null} { "status": "error", "message": "Conversion not found", "data": {} } ``` # Conversion Stats Source: https://docs.tender.cash/api-reference/endpoint/conversions/stats GET /v1/api/conversions/stats Count your conversions: total, pending and created today > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query parameters **from** `string` Start of a `createdAt` date range (ISO 8601). Only applied when `to` is also sent. **Example:** `"2026-10-01T00:00:00.000Z"` **to** `string` End of a `createdAt` date range (ISO 8601). Only applied when `from` is also sent. **Example:** `"2026-10-15T23:59:59.999Z"` **search** `string` Case-insensitive search across the transaction ID, status, and the bank account name, bank name and account number of fiat conversions. **Example:** `"completed"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* Conversion counts for your merchant account, after applying the filters **data properties** **total** `number` Number of conversions matching the filters **Example:** `42` **pending** `number` Number of matching conversions whose status is exactly `pending` (not `processing`) **Example:** `2` **todayConversion** `number` Number of matching conversions created today (server time) **Example:** `3` ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/stats" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "total": 42, "pending": 2, "todayConversion": 3 } } ``` # Swap-Enabled Chains Source: https://docs.tender.cash/api-reference/endpoint/conversions/swap-chains GET /v1/api/conversions/swap/chains List the chains that support conversions, optionally filtered by coin > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query parameters **coin** `string` Return only chains that support this coin. Accepts the coin ID or its symbol. If the coin is unknown or not swap-enabled, `data` is an empty array. **Example:** `"usdt"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `array` *required* Active chains that have swaps enabled **chain object** **id** `string` Chain ID. Use it as `chain` or `toChain` in conversion requests. **Example:** `"ethereum"` **name** `string` Display name **Example:** `"Ethereum"` **icon** `string` Chain icon URL **Example:** `"https://tender-store.s3.amazonaws.com/icons/ethereum.png"` **coin** `string` Native coin symbol **Example:** `"ETH"` **chainType** `string` Blockchain family (for example `evm`, `sol`, `tron`, `btc`, `stellar`) **Example:** `"evm"` **explorer** `string` Block explorer URL (may be empty) **Example:** `"https://etherscan.io"` **status** `string` Always `active` in this list **Example:** `"active"` Additional fields may be present; ignore fields you don't recognise. ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/swap/chains?coin=usdt" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "id": "ethereum", "name": "Ethereum", "icon": "https://tender-store.s3.amazonaws.com/icons/ethereum.png", "coin": "ETH", "chainType": "evm", "explorer": "https://etherscan.io", "status": "active" } ] } ``` # Swap-Enabled Coins Source: https://docs.tender.cash/api-reference/endpoint/conversions/swap-coins GET /v1/api/conversions/swap/coins List the coins that support conversions, optionally filtered by chain > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All conversion endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query parameters **chain** `string` Comma-separated chain IDs. Returns only coins available on at least one of these chains. **Example:** `"ethereum,solana"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `array` *required* Active coins that have swaps enabled **coin object** **id** `string` Coin ID. Use it as `coin` or `toCoin` in conversion requests. **Example:** `"usdt"` **name** `string` Display name **Example:** `"Tether USD"` **symbol** `string` Ticker symbol **Example:** `"USDT"` **icon** `string` Coin icon URL **Example:** `"https://tender-store.s3.amazonaws.com/icons/usdt.png"` **type** `string` `crypto` or `fiat` **Example:** `"crypto"` **isStableCoin** `boolean` Whether the coin is a stablecoin **Example:** `true` **chains** `array` IDs of the chains this coin is available on **Example:** `["ethereum", "tron", "solana"]` **status** `string` Always `active` in this list **Example:** `"active"` Additional fields may be present; ignore fields you don't recognise. ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/conversions/swap/coins?chain=ethereum,solana" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "id": "usdt", "name": "Tether USD", "symbol": "USDT", "icon": "https://tender-store.s3.amazonaws.com/icons/usdt.png", "type": "crypto", "isStableCoin": true, "chains": ["ethereum", "tron", "solana"], "status": "active" } ] } ``` # Delete Webhook Source: https://docs.tender.cash/api-reference/endpoint/delete-webhook DELETE /v1/api/webhook/{id} Delete your webhook and stop all deliveries > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Rate limit: 120 requests per minute for all webhook endpoints. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` Deleting is permanent. Tender stops sending events straight away, and [Fetch Webhook](/api-reference/endpoint/fetch-webhook) returns `406` until you register a new one. Your existing [webhook logs](/api-reference/endpoint/webhook-logs) are kept. ## Path Parameters **id** `string` *required* The webhook `_id`, from [Fetch Webhook](/api-reference/endpoint/fetch-webhook) or [Add Webhook](/api-reference/endpoint/add-webhook) **Example:** `"691069ff947a702cfad9e861"` ## Response Returns HTTP `200`. **status** `string` *required* Always `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **deleted** `boolean` Always `true` **Example:** `true` ## Errors | Status | When | | - | - | | `400` | `"Webhook not found"`: the ID is malformed, doesn't exist, or doesn't belong to your account. | | `401`, `403` | See [Authentication](/api-reference/authentication#authentication-errors). | | `429` | Rate limit exceeded. | ```json 200 theme={null} { "status": "success", "message": "success", "data": { "deleted": true } } ``` ```json 400 theme={null} { "status": "error", "message": "Webhook not found", "data": null } ``` # Fetch Chain Currencies Source: https://docs.tender.cash/api-reference/endpoint/fetch-chain-currencies GET /v1/api/system/chains/{id}/currency Retrieve currencies supported on a specific blockchain > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Returns the active currencies (cryptocurrencies) available on one chain. The response contains at most 12 currencies, sorted by `priority` (lowest first); currencies without a priority come last. This list isn't narrowed to your outlet's allowed currencies. To see only the currencies your outlet accepts, use [Fetch All Currencies](/api-reference/endpoint/fetch-currencies) with `chains=`. System endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Path Parameters **id** `string` *required* Chain identifier, as returned by [Fetch Allowed Chains](/api-reference/endpoint/fetch-chains) **Example:** `"avalanche"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `array` *required* Array of currencies supported on the specified chain **currency object** **id** `string` Unique identifier for the currency **Example:** `"avalanche"` **name** `string` Display name of the currency **Example:** `"Avax"` **icon** `string` URL to the currency's icon image **Example:** `"https://tender-store.s3.amazonaws.com/icons/avax.png"` **symbol** `string` Currency symbol **Example:** `"AVAX"` **status** `string` Current status of the currency **Example:** `"active"` **isContract** `boolean` Whether this is a smart contract token **Example:** `false` **chains** `array` Array of chain identifiers where this currency is available **Example:** `["avalanche"]` **priceTag** `string` Price identifier for market data **Example:** `"avalanche-2"` **fee** `object` Fee configuration per chain (chain id to fee string) **Example:** `{ "avalanche": "5" }` **contractAddress** `object` Smart contract addresses per chain when `isContract` is true (chain id to address) **Example:** `{ "avalanche": "0x5425890298aed601595a70AB815c96711a31Bc65", "ethereum": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" }` **priority** `number` Display order; lower values first. Omitted when not set. **Example:** `1` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": [ { "id": "avalanche", "name": "Avax", "icon": "https://tender-store.s3.amazonaws.com/icons/avax.png", "symbol": "AVAX", "status": "active", "isContract": false, "chains": ["avalanche"], "priceTag": "avalanche-2", "fee": { "avalanche": "5" }, "priority": 1 }, { "id": "usdc", "name": "USDC", "icon": "https://tender-store.s3.amazonaws.com/icons/usdc.png", "symbol": "USDC", "status": "active", "isContract": true, "chains": ["avalanche", "ethereum", "optimism", "aurora", "near", "polygon", "solana", "base", "beam", "stellar", "tron", "8453"], "priceTag": "usd-coin", "fee": { "ethereum": "25", "optimism": "3", "avalanche": "3", "polygon": "3", "solana": "3", "stellar": "3", "base": "5", "beam": "5", "tron": "3" }, "contractAddress": { "avalanche": "0x5425890298aed601595a70AB815c96711a31Bc65", "ethereum": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", "optimism": "0xA6d3287496f7d3f1F20521141161448D15393b67", "polygon": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "stellar": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", "tron": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", "base": "0x53D85F1925fCE8BB93732ed969800Df0b062258A" }, "priority": 2 }, { "id": "usdt", "name": "USDT", "icon": "https://tender-store.s3.amazonaws.com/icons/usdt.png", "symbol": "USDT", "status": "active", "isContract": true, "chains": ["avalanche", "ethereum", "optimism", "aurora", "near", "polygon", "solana", "tron", "pego"], "priceTag": "tether", "fee": { "ethereum": "25", "optimism": "3", "avalanche": "3", "polygon": "3", "solana": "3", "tron": "3", "pego": "3" }, "contractAddress": { "avalanche": "0xa1Ef10416440A13D555B9Dc78F81153D13340588", "ethereum": "0xFcF3950CFB1aCA9a7733839967126799A4D10fdF", "optimism": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", "polygon": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "solana": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "tron": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "pego": "0x02F9Bebf5E54968D8Cc2562356C91ECDE135801B" }, "priority": 3 } ] } ``` # Fetch Allowed Chains Source: https://docs.tender.cash/api-reference/endpoint/fetch-chains GET /v1/api/system/chains Retrieve the blockchain networks your outlet can accept payments on > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Returns the active chains you can use for payments. If your outlet is restricted to certain currencies, only chains that support at least one of those currencies are returned. Chains are sorted by `priority` (lowest first); chains without a priority come last. This endpoint isn't paginated: it accepts only the `type` and `currency` query parameters and returns every matching chain in one response. System endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query Parameters **currency** `string` Only return chains that support this cryptocurrency identifier. Returns `400 Invalid Currency` if the currency is unknown or inactive. **Example:** `"usdt"` **type** `string` Only return chains of this category **Example:** `"chain"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* Container for the chain list **data properties** **data** `array` The chains **chain object** **id** `string` Chain identifier. Use it as `chain` when initiating a payment. **Example:** `"tron"` **name** `string` Display name of the chain **Example:** `"Tron"` **icon** `string` URL of the chain icon **Example:** `"https://tender-store.s3.amazonaws.com/icons/tron.png"` **coin** `string` Native coin of the chain **Example:** `"TRX"` **chainType** `string` Chain family (for example `evm`, `tron`, `btc`, `sol`) **Example:** `"tron"` **status** `string` Chain status. Always `active` in this list. **Example:** `"active"` **explorer** `string` Block explorer URL (may be an empty string) **Example:** `"https://tronscan.org"` **yieldEnabled** `boolean` Whether yield is enabled on this chain **Example:** `false` **priority** `number` Display order; lower values first. Omitted when not set. **Example:** `1` **recommended** `boolean` Whether checkout UIs should flag this chain as recommended. Omitted when not configured; fall back to your own default. **Example:** `true` **page** `number` Always `1` **Example:** `1` **pages** `number` Always `1` for current chain counts **Example:** `1` **limit** `number` Maximum number of chains returned **Example:** `40` ## Example request ```bash theme={null} curl "https://sandbox-api.tender.cash/v1/api/system/chains?currency=usdt" \ -H "x-access-id: your-access-id-here" ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "data": [ { "id": "tron", "name": "Tron", "icon": "https://tender-store.s3.amazonaws.com/icons/tron.png", "coin": "TRX", "chainType": "tron", "status": "active", "explorer": "https://tronscan.org", "yieldEnabled": false, "priority": 1, "recommended": true }, { "id": "ethereum", "name": "Ethereum", "icon": "https://tender-store.s3.amazonaws.com/icons/ethereum.png", "coin": "ETH", "chainType": "evm", "status": "active", "explorer": "https://etherscan.io", "yieldEnabled": false, "priority": 2 } ], "pages": 1, "page": 1, "limit": 40 } } ``` # Fetch All Currencies Source: https://docs.tender.cash/api-reference/endpoint/fetch-currencies GET /v1/api/system/currency Retrieve the supported currencies, with their chain mappings > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Returns a page of active currencies. Use `type=crypto` for the cryptocurrencies customers can pay with, or `type=fiat` for the fiat codes you can price payments in. If your outlet is restricted to certain currencies, only those currencies are returned. Within a page, currencies are sorted by `priority` (lowest first); currencies without a priority come last. System endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query Parameters **chains** `string` Comma-separated chain identifiers. Only currencies available on at least one of these chains are returned. **Example:** `"tron,ethereum"` **type** `string` `crypto` or `fiat`. Omit to return both. **Example:** `"crypto"` **page** `number` (default: 1) Page number, starting at 1 **Example:** `1` **limit** `number` (default: 12) Number of currencies per page **Example:** `12` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* A page of currencies **data properties** **data** `array` The currencies on this page **currency object** **id** `string` Currency identifier. Use it as `currency` when initiating a payment (crypto) or as the fiat code in the path (fiat). **Example:** `"usdc"` **name** `string` Display name of the currency **Example:** `"USDC"` **icon** `string` URL of the currency icon **Example:** `"https://tender-store.s3.amazonaws.com/icons/usdc.png"` **symbol** `string` Currency symbol **Example:** `"USDC"` **status** `string` Currency status. Always `active` in this list. **Example:** `"active"` **isContract** `boolean` Whether this is a token contract **Example:** `true` **chains** `array` Chain identifiers where this currency is available **Example:** `["ethereum", "polygon", "tron"]` **priceTag** `string` Price identifier used for market data **Example:** `"usd-coin"` **fee** `object` Fee configuration per chain (chain identifier to fee string) **Example:** `{ "ethereum": "25", "tron": "3" }` **contractAddress** `object` Token contract address per chain (chain identifier to address), when `isContract` is true **Example:** `{ "ethereum": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }` **priority** `number` Display order; lower values first. Omitted when not set. **Example:** `1` **page** `number` Current page number **Example:** `1` **pages** `number` Total number of pages **Example:** `1` **limit** `number` Currencies per page **Example:** `12` **total** `number` Total number of matching currencies **Example:** `2` ## Example request ```bash theme={null} curl "https://sandbox-api.tender.cash/v1/api/system/currency?type=crypto&chains=tron&page=1&limit=12" \ -H "x-access-id: your-access-id-here" ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "data": [ { "id": "usdt", "name": "USDT", "icon": "https://tender-store.s3.amazonaws.com/icons/usdt.png", "symbol": "USDT", "status": "active", "isContract": true, "chains": ["ethereum", "polygon", "tron"], "priceTag": "tether", "fee": { "ethereum": "25", "polygon": "3", "tron": "3" }, "contractAddress": { "ethereum": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "polygon": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "tron": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, "priority": 1 }, { "id": "usdc", "name": "USDC", "icon": "https://tender-store.s3.amazonaws.com/icons/usdc.png", "symbol": "USDC", "status": "active", "isContract": true, "chains": ["ethereum", "polygon", "tron"], "priceTag": "usd-coin", "fee": { "ethereum": "25", "polygon": "3", "tron": "3" }, "contractAddress": { "ethereum": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "polygon": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "tron": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8" }, "priority": 2 } ], "page": 1, "pages": 1, "limit": 12, "total": 2 } } ``` # Fetch Exchange Rates Source: https://docs.tender.cash/api-reference/endpoint/fetch-exchange-rates GET /v1/api/system/rates/{coin} Get the current price of a cryptocurrency in USD and a fiat currency > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Returns the current price of one unit of a cryptocurrency in USD and in a fiat currency of your choice. Without the `currency` query parameter, the fiat currency is USD, so `rate` equals `usd`. System endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Path Parameters **coin** `string` *required* Cryptocurrency identifier, as returned by [Fetch All Currencies](/api-reference/endpoint/fetch-currencies) **Example:** `"bitcoin"` ## Query Parameters **currency** `string` (default: usd) Fiat currency code to price the coin in. Case-insensitive. **Example:** `"ngn"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* Price of the coin **data properties** **coin** `string` The cryptocurrency identifier you requested **Example:** `"bitcoin"` **usd** `number` Price of one unit in USD **Example:** `82752` **rate** `number | null` Price of one unit in `currency`. `null` if no rate is available for that fiat currency. **Example:** `127843151.04` **currency** `string` The fiat currency `rate` is in, lowercase. `usd` when you didn't send `currency`. **Example:** `"ngn"` ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Invalid Currency` | Unknown `coin` | | 400 | `Exchange Price Error` | No price is available for the coin right now | ## Example request ```bash theme={null} curl "https://sandbox-api.tender.cash/v1/api/system/rates/bitcoin?currency=ngn" \ -H "x-access-id: your-access-id-here" ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "coin": "bitcoin", "usd": 82752, "rate": 127843151.04, "currency": "ngn" } } ``` # Fetch Webhook Source: https://docs.tender.cash/api-reference/endpoint/fetch-webhook GET /v1/api/webhook Get the webhook registered for your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Rate limit: 120 requests per minute for all webhook endpoints. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Response Returns HTTP **`202`** with your webhook as a single object. If no webhook is registered, returns HTTP **`406`**. **status** `string` *required* Always `"success"` **message** `string` *required* **Example:** `"Webhooks fetched"` **data** `object` *required* Your webhook. Additional fields may be present; ignore fields you don't recognise. **data properties** **\_id** `string` The webhook ID. Use it with [Delete Webhook](/api-reference/endpoint/delete-webhook). **Example:** `"691069ff947a702cfad9e861"` **merchantId** `string` Your merchant ID **Example:** `"6538e8f9bdec6d1a21978a64"` **description** `string` Description of the webhook **Example:** `"Production webhook"` **url** `string` Webhook endpoint URL **Example:** `"https://your-domain.com/webhooks/tender"` **eventTypes** `string[]` Subscribed event names **Example:** `["transaction_completed", "transaction_cancelled"]` **headers** `object` Custom headers sent with every delivery **Example:** `{ "X-Webhook-Secret": "a-long-random-value" }` **isActive** `boolean` Always `true` for a registered webhook **Example:** `true` **createdAt** `string` ISO 8601 timestamp when the webhook was first registered **Example:** `"2026-10-15T10:16:31.594Z"` **updatedAt** `string` ISO 8601 timestamp when the webhook was last changed **Example:** `"2026-10-15T10:18:35.286Z"` ## Errors | Status | When | | - | - | | `406` | No webhook is registered. `message` starts with `"Webhook not found"`. | | `401`, `403` | See [Authentication](/api-reference/authentication#authentication-errors). | | `429` | Rate limit exceeded. | ```json 202 theme={null} { "status": "success", "message": "Webhooks fetched", "data": { "_id": "691069ff947a702cfad9e861", "merchantId": "6538e8f9bdec6d1a21978a64", "description": "Production webhook", "url": "https://your-domain.com/webhooks/tender", "eventTypes": ["transaction_completed", "transaction_cancelled"], "headers": { "X-Webhook-Secret": "a-long-random-value" }, "isActive": true, "isDeleted": false, "createdAt": "2026-10-15T10:16:31.594Z", "updatedAt": "2026-10-15T10:18:35.286Z" } } ``` ```json 406 theme={null} { "status": "error", "message": "Webhook not found -- merchant-6538e8f9bdec6d1a21978a64", "data": null } ``` # Initiate Payment Source: https://docs.tender.cash/api-reference/endpoint/initiate-payment POST /v1/api/payment/initiate Create a new cryptocurrency payment request and get a deposit address > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Creates a payment request for the outlet your access ID belongs to. Tender generates a unique deposit address, calculates the all-in amount the customer must send, and returns both. Track the payment with [Validate Payment](/api-reference/endpoint/validate-payment) or the `transaction_requested`, `transaction_completed` and related [webhook events](/get-started/webhook-events). `amount` is denominated in the outlet's default fiat currency (USD if the outlet has none set). To price the payment in a specific fiat currency, use [Initiate Payment By Fiat Currency](/api-reference/endpoint/initiate-payment-currency). To show the customer the total before creating a payment, use [Quote Payment](/api-reference/endpoint/quote-payment). Payment endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Media type of the request body **Example:** `"application/json"` ## Body **amount** `string` *required* The price to charge, in the outlet's default fiat currency (USD if none is set). Send it as a decimal string. Tender converts it to the cryptocurrency amount and adds fees on top. **Example:** `"10.00"` **chain** `string` *required* Chain identifier, as returned by [Fetch Allowed Chains](/api-reference/endpoint/fetch-chains) **Example:** `"tron"` **currency** `string` *required* Cryptocurrency identifier, as returned by [Fetch Chain Currencies](/api-reference/endpoint/fetch-chain-currencies). The currency must be supported on `chain` and allowed for your outlet. **Example:** `"usdt"` **reference** `string` Your own reference for this payment, such as an order ID. `referenceId` is accepted as an alias. A reference can be used by only one payment at a time: reusing the reference of a payment that isn't cancelled returns `409`. **Example:** `"ORDER-9821"` **meta** `object` Optional metadata attached to the payment. Only the keys below are stored; any other keys are ignored. **meta properties** **meta.customerEmail** `string` The customer's email address. Must be a valid email. **Example:** `"customer@example.com"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* The new payment request. Additional fields may be present; ignore fields you don't recognise. **data properties** **txId** `string` The payment's transaction ID. Use it to validate or cancel the payment. **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` Transaction type. Always `receive` for payments. **Example:** `"receive"` **chain** `string` Chain identifier **Example:** `"tron"` **chainId** `string` Internal ID of the chain record **Example:** `"64f782bb4278fc475eb30e29"` **currency** `string` Internal ID of the cryptocurrency record **Example:** `"64fc4cda43812c15552311d7"` **walletAddress** `string` The deposit address the customer must pay to **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **gasFreeAddress** `string` Present on chains where Tender collects payments through a gasless deposit address. When present, it equals `walletAddress`. **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **walletAddressIndex** `number` Index of the generated deposit address **Example:** `7` **contractAddress** `string | null` Token contract address, or `null` for a native coin **Example:** `"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"` **requestedAmount** `string` The `amount` you sent, in fiat **Example:** `"10.00"` **amount** `string` The all-in cryptocurrency amount the customer must send: the payment plus every fee. Show this amount to the customer. **Example:** `"10.15"` **coinAmount** `string` Same as `amount` **Example:** `"10.15"` **usdAmount** `string` USD value of `amount` **Example:** `"10.15"` **fee** `number` Fees charged on this payment, in the cryptocurrency: the receive fee, any activation fee and any auto-payout fees. The Tron gasless service fee is reported separately in `gasFreeServiceFee`. **Example:** `0.1` **feeUSD** `number` `fee` in USD **Example:** `0.1` **activationFee** `number` Activation fee included in `fee`, if any **Example:** `0` **activationFeeUSD** `number` `activationFee` in USD **Example:** `0` **gasFreeServiceFee** `number` Tron gasless network service fee added to `amount`. Omitted when it doesn't apply. **Example:** `0.05` **gasFreeServiceFeeUSD** `number` `gasFreeServiceFee` in USD. Omitted when it doesn't apply. **Example:** `0.05` **feeBreakdown** `object` Itemised fees charged to the customer **feeBreakdown properties** **settlement** `string` Fee settlement state **Example:** `"pending"` **charged** `object` Each fee leg as `{ coin, usd }`, where `coin` is a string and `usd` a number: `receive`, `activation`, `autoPayoutSwap`, `autoPayoutWithdrawal` and `gasFreeService`. `totalCoin` (string) and `totalUSD` (number) equal `fee` and `feeUSD`. **feeSent** `boolean` Whether the fee has been collected. Always `false` at initiation. **Example:** `false` **rate** `number` USD price of one unit of the cryptocurrency at initiation **Example:** `1` **fxRateAt** `number` The USD rate used to calculate `usdAmount`, frozen at initiation **Example:** `1` **agentAmount** `string` The requested amount in the outlet's fiat currency **Example:** `"10.00"` **agentCurrency** `string` The fiat currency `amount` was priced in **Example:** `"usd"` **agentRate** `number` Price of one unit of the cryptocurrency in `agentCurrency` **Example:** `1` **agentRateUSD** `number` USD price of one unit of the cryptocurrency, or the outlet's manual rate if it uses one **Example:** `1` **balanceRequired** `string` Cryptocurrency amount still owed. Equals `amount` at initiation. **Example:** `"10.15"` **balanceRequiredUSD** `string` `balanceRequired` in USD **Example:** `"10.15"` **balanceRequiredAgent** `string` Amount still owed in the outlet's fiat currency **Example:** `"10.00"` **referenceId** `string` Your `reference`, if you sent one **Example:** `"ORDER-9821"` **meta** `object` The metadata you sent, if any **Example:** `{"customerEmail": "customer@example.com"}` **agentId** `string` ID of the outlet receiving the payment **Example:** `"6538eaaebdec6d1a21978e6a"` **merchantId** `string` ID of your merchant account **Example:** `"6538e8f9bdec6d1a21978a64"` > [!NOTE] > > The initiate response has no `status` field. A new payment always starts as `pending`; call > [Validate Payment](/api-reference/endpoint/validate-payment) to read its current status. ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `currency must be a string` (or another validation message) | A required field is missing or has the wrong type | | 400 | `Invalid chain` / `Chain not active` | Unknown or disabled `chain` | | 400 | `invalid Currency` / `Currency not allowed` | Unknown `currency`, or not supported on `chain` | | 400 | `Amount can't be Zero` | `amount` is zero or negative | | 400 | `Minimum initiated amount is USD` | The payment is below the minimum | | 400 | `Invalid Coin Exchange Price` | No price is available for the currency right now | | 403 | `You are not authorized to initiate transactions with this currency` | The currency isn't in your outlet's allowed currencies | | 409 | `A transaction with reference '' already exists` | Another payment that isn't cancelled already uses this `reference` | Error responses use the standard envelope with `data: null`. See [Errors](/api-reference/errors). ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/payment/initiate \ -H "x-access-id: your-access-id-here" \ -H "Content-Type: application/json" \ -d '{ "amount": "10.00", "chain": "tron", "currency": "usdt", "reference": "ORDER-9821", "meta": { "customerEmail": "customer@example.com" } }' ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "chain": "tron", "chainId": "64f782bb4278fc475eb30e29", "currency": "64fc4cda43812c15552311d7", "walletAddress": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "gasFreeAddress": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "walletAddressIndex": 7, "contractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "requestedAmount": "10.00", "amount": "10.15", "coinAmount": "10.15", "usdAmount": "10.15", "fee": 0.1, "feeUSD": 0.1, "activationFee": 0, "activationFeeUSD": 0, "gasFreeServiceFee": 0.05, "gasFreeServiceFeeUSD": 0.05, "feeBreakdown": { "settlement": "pending", "charged": { "receive": { "coin": "0.1", "usd": 0.1 }, "activation": { "coin": "0", "usd": 0 }, "autoPayoutSwap": { "coin": "0", "usd": 0 }, "autoPayoutWithdrawal": { "coin": "0", "usd": 0 }, "gasFreeService": { "coin": "0.05", "usd": 0.05 }, "totalCoin": "0.1", "totalUSD": 0.1 } }, "feeSent": false, "rate": 1, "fxRateAt": 1, "agentAmount": "10.00", "agentCurrency": "usd", "agentRate": 1, "agentRateUSD": 1, "balanceRequired": "10.15", "balanceRequiredUSD": "10.15", "balanceRequiredAgent": "10.00", "referenceId": "ORDER-9821", "meta": { "customerEmail": "customer@example.com" }, "agentId": "6538eaaebdec6d1a21978e6a", "merchantId": "6538e8f9bdec6d1a21978a64" } } ``` # Initiate Payment By Fiat Currency Source: https://docs.tender.cash/api-reference/endpoint/initiate-payment-currency POST /v1/api/payment/initiate/{currency} Create a payment request priced in a specific fiat currency > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Works exactly like [Initiate Payment](/api-reference/endpoint/initiate-payment), except that `amount` is denominated in the fiat currency you name in the path instead of the outlet's default fiat currency. Tender converts it to the cryptocurrency at the current rate. If the path currency is the outlet's default fiat currency and the outlet uses a manual rate, the manual rate applies. Payment endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Path Parameters **currency** `string` *required* Fiat currency code that `amount` is priced in, such as `ngn` or `usd`. Use a fiat currency listed by [Fetch All Currencies](/api-reference/endpoint/fetch-currencies) with `type=fiat`. This is a fiat code, not the cryptocurrency the customer pays with. **Example:** `"ngn"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Media type of the request body **Example:** `"application/json"` ## Body **amount** `string` *required* The price to charge, in the fiat currency named in the path. Send it as a decimal string. Tender converts it to the cryptocurrency amount and adds fees on top. **Example:** `"15000"` **chain** `string` *required* Chain identifier, as returned by [Fetch Allowed Chains](/api-reference/endpoint/fetch-chains) **Example:** `"tron"` **currency** `string` *required* Cryptocurrency identifier, as returned by [Fetch Chain Currencies](/api-reference/endpoint/fetch-chain-currencies). The currency must be supported on `chain` and allowed for your outlet. **Example:** `"usdt"` **reference** `string` Your own reference for this payment, such as an order ID. `referenceId` is accepted as an alias. A reference can be used by only one payment at a time: reusing the reference of a payment that isn't cancelled returns `409`. **Example:** `"ORDER-9821"` **meta** `object` Optional metadata attached to the payment. Only the keys below are stored; any other keys are ignored. **meta properties** **meta.customerEmail** `string` The customer's email address. Must be a valid email. **Example:** `"customer@example.com"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* The new payment request. Additional fields may be present; ignore fields you don't recognise. **data properties** **txId** `string` The payment's transaction ID. Use it to validate or cancel the payment. **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` Transaction type. Always `receive` for payments. **Example:** `"receive"` **chain** `string` Chain identifier **Example:** `"tron"` **chainId** `string` Internal ID of the chain record **Example:** `"64f782bb4278fc475eb30e29"` **currency** `string` Internal ID of the cryptocurrency record **Example:** `"64fc4cda43812c15552311d7"` **walletAddress** `string` The deposit address the customer must pay to **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **gasFreeAddress** `string` Present on chains where Tender collects payments through a gasless deposit address. When present, it equals `walletAddress`. **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **walletAddressIndex** `number` Index of the generated deposit address **Example:** `7` **contractAddress** `string | null` Token contract address, or `null` for a native coin **Example:** `"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"` **requestedAmount** `string` The `amount` you sent, in the path fiat currency **Example:** `"15000"` **amount** `string` The all-in cryptocurrency amount the customer must send: the payment plus every fee. Show this amount to the customer. **Example:** `"10.15"` **coinAmount** `string` Same as `amount` **Example:** `"10.15"` **usdAmount** `string` USD value of `amount` **Example:** `"10.15"` **fee** `number` Fees charged on this payment, in the cryptocurrency: the receive fee, any activation fee and any auto-payout fees. The Tron gasless service fee is reported separately in `gasFreeServiceFee`. **Example:** `0.1` **feeUSD** `number` `fee` in USD **Example:** `0.1` **activationFee** `number` Activation fee included in `fee`, if any **Example:** `0` **activationFeeUSD** `number` `activationFee` in USD **Example:** `0` **gasFreeServiceFee** `number` Tron gasless network service fee added to `amount`. Omitted when it doesn't apply. **Example:** `0.05` **gasFreeServiceFeeUSD** `number` `gasFreeServiceFee` in USD. Omitted when it doesn't apply. **Example:** `0.05` **feeBreakdown** `object` Itemised fees charged to the customer **feeBreakdown properties** **settlement** `string` Fee settlement state **Example:** `"pending"` **charged** `object` Each fee leg as `{ coin, usd }`, where `coin` is a string and `usd` a number: `receive`, `activation`, `autoPayoutSwap`, `autoPayoutWithdrawal` and `gasFreeService`. `totalCoin` (string) and `totalUSD` (number) equal `fee` and `feeUSD`. **feeSent** `boolean` Whether the fee has been collected. Always `false` at initiation. **Example:** `false` **rate** `number` USD price of one unit of the cryptocurrency at initiation **Example:** `1` **fxRateAt** `number` The USD rate used to calculate `usdAmount`, frozen at initiation **Example:** `1` **agentAmount** `string` The requested amount in `agentCurrency` **Example:** `"15000"` **agentCurrency** `string` The fiat currency `amount` was priced in (the path currency) **Example:** `"ngn"` **agentRate** `number` Price of one unit of the cryptocurrency in `agentCurrency` **Example:** `1500` **agentRateUSD** `number` USD price of one unit of the cryptocurrency, or the outlet's manual rate if it uses one **Example:** `1` **balanceRequired** `string` Cryptocurrency amount still owed. Equals `amount` at initiation. **Example:** `"10.15"` **balanceRequiredUSD** `string` `balanceRequired` in USD **Example:** `"10.15"` **balanceRequiredAgent** `string` Amount still owed in `agentCurrency` **Example:** `"15000"` **referenceId** `string` Your `reference`, if you sent one **Example:** `"ORDER-9821"` **meta** `object` The metadata you sent, if any **Example:** `{"customerEmail": "customer@example.com"}` **agentId** `string` ID of the outlet receiving the payment **Example:** `"6538eaaebdec6d1a21978e6a"` **merchantId** `string` ID of your merchant account **Example:** `"6538e8f9bdec6d1a21978a64"` > [!NOTE] > > The initiate response has no `status` field. A new payment always starts as `pending`; call > [Validate Payment](/api-reference/endpoint/validate-payment) to read its current status. ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `currency must be a string` (or another validation message) | A required field is missing or has the wrong type | | 400 | `Invalid chain` / `Chain not active` | Unknown or disabled `chain` | | 400 | `invalid Currency` / `Currency not allowed` | Unknown `currency`, or not supported on `chain` | | 400 | `Amount can't be Zero` | `amount` is zero or negative | | 400 | `Minimum initiated amount is USD` | The payment is below the minimum | | 400 | `Invalid Coin Exchange Price` | No price is available for the currency right now | | 403 | `You are not authorized to initiate transactions with this currency` | The currency isn't in your outlet's allowed currencies | | 409 | `A transaction with reference '' already exists` | Another payment that isn't cancelled already uses this `reference` | An unsupported fiat code in the path fails the request; use a code listed by Fetch All Currencies. Error responses use the standard envelope with `data: null`. See [Errors](/api-reference/errors). ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/payment/initiate/ngn \ -H "x-access-id: your-access-id-here" \ -H "Content-Type: application/json" \ -d '{ "amount": "15000", "chain": "tron", "currency": "usdt", "reference": "ORDER-9821", "meta": { "customerEmail": "customer@example.com" } }' ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "chain": "tron", "chainId": "64f782bb4278fc475eb30e29", "currency": "64fc4cda43812c15552311d7", "walletAddress": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "gasFreeAddress": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "walletAddressIndex": 7, "contractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "requestedAmount": "15000", "amount": "10.15", "coinAmount": "10.15", "usdAmount": "10.15", "fee": 0.1, "feeUSD": 0.1, "activationFee": 0, "activationFeeUSD": 0, "gasFreeServiceFee": 0.05, "gasFreeServiceFeeUSD": 0.05, "feeBreakdown": { "settlement": "pending", "charged": { "receive": { "coin": "0.1", "usd": 0.1 }, "activation": { "coin": "0", "usd": 0 }, "autoPayoutSwap": { "coin": "0", "usd": 0 }, "autoPayoutWithdrawal": { "coin": "0", "usd": 0 }, "gasFreeService": { "coin": "0.05", "usd": 0.05 }, "totalCoin": "0.1", "totalUSD": 0.1 } }, "feeSent": false, "rate": 1, "fxRateAt": 1, "agentAmount": "15000", "agentCurrency": "ngn", "agentRate": 1500, "agentRateUSD": 1, "balanceRequired": "10.15", "balanceRequiredUSD": "10.15", "balanceRequiredAgent": "15000", "referenceId": "ORDER-9821", "meta": { "customerEmail": "customer@example.com" }, "agentId": "6538eaaebdec6d1a21978e6a", "merchantId": "6538e8f9bdec6d1a21978a64" } } ``` # List Webhook Events Source: https://docs.tender.cash/api-reference/endpoint/list-webhook-events GET /v1/api/webhook/events Get the names of the webhook events you can subscribe to > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Rate limit: 120 requests per minute for all webhook endpoints. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` Use these names in `eventTypes` when you [add a webhook](/api-reference/endpoint/add-webhook). For when each event fires and its payload, see [Webhook Events](/get-started/webhook-events). ## Response Returns HTTP **`202`** with an array of event names. **status** `string` *required* Always `"success"` **message** `string` *required* **Example:** `"Webhook events fetched"` **data** `string[]` *required* Event names. `transaction_reserved` is reserved and not currently sent. New events may be added over time. **Example:** `["transaction_completed", "transaction_overpayment"]` ```json 202 theme={null} { "status": "success", "message": "Webhook events fetched", "data": [ "transaction_completed", "transaction_overpayment", "transaction_partially_paid", "transaction_partial_fulfilled", "transaction_requested", "transaction_reserved", "transaction_cancelled", "sub_user_wallet_transaction_detected", "sub_user_wallet_transfer_confirmed", "sub_user_wallet_transfer_failed", "offramp_order_completed", "offramp_order_cancelled", "offramp_order_failed" ] } ``` # Fetch On-ramp Chains Source: https://docs.tender.cash/api-reference/endpoint/onramp/chains GET /v1/api/onramp/chains List blockchain networks available for on-ramp delivery > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Use a chain's `id` as `targetChain` in [Get On-ramp Quote](/api-reference/endpoint/onramp/quote). This request counts against the on-ramp limit of 10 requests per minute. Cache the result. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `array` *required* Array of supported chains **chain object** **\_id** `string` Tender's internal ID for the chain. **Example:** `"64f1a2b3c4d5e6f708192a3b"` **id** `string` Chain identifier. Use as `targetChain` in the quote request. **Example:** `"tron"` **name** `string` **Example:** `"Tron"` **icon** `string` **Example:** `"https://cdn.tender.cash/chains/tron.png"` **chainType** `string` **Example:** `"tron"` **explorer** `string` **Example:** `"https://tronscan.org"` **status** `string` **Example:** `"active"` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "_id": "64f1a2b3c4d5e6f708192a3b", "id": "tron", "name": "Tron", "icon": "https://cdn.tender.cash/chains/tron.png", "chainType": "tron", "explorer": "https://tronscan.org", "status": "active" } ] } ``` # Fetch On-ramp Coins Source: https://docs.tender.cash/api-reference/endpoint/onramp/coins GET /v1/api/onramp/coins List cryptocurrencies available for on-ramp delivery, optionally filtered by chain > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Use a coin's **`symbol`** (for example `USDT`) as `targetCurrency` in [Get On-ramp Quote](/api-reference/endpoint/onramp/quote), and one of its `chains` as `targetChain`. This request counts against the on-ramp limit of 10 requests per minute. Cache the result. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Query Parameters **chain** `string` Return only coins available on this chain. Use a chain `id` from [Fetch On-ramp Chains](/api-reference/endpoint/onramp/chains). **Example:** `"tron"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `array` *required* Array of supported coins **coin object** **\_id** `string` Tender's internal ID for the coin. **Example:** `"64f1b2c3d4e5f60718293a4b"` **id** `string` Coin identifier. **Example:** `"usdt"` **name** `string` **Example:** `"Tether USD"` **symbol** `string` Coin symbol. Use as `targetCurrency` in the quote request. **Example:** `"USDT"` **icon** `string` **Example:** `"https://cdn.tender.cash/coins/usdt.png"` **chains** `string[]` Chain IDs this coin is available on. **Example:** `["tron", "ethereum"]` **isContract** `boolean` Whether this is a token (for example ERC-20 or TRC-20) rather than a native coin. **Example:** `true` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "_id": "64f1b2c3d4e5f60718293a4b", "id": "usdt", "name": "Tether USD", "symbol": "USDT", "icon": "https://cdn.tender.cash/coins/usdt.png", "chains": ["tron", "ethereum"], "isContract": true } ] } ``` # Initiate On-ramp Source: https://docs.tender.cash/api-reference/endpoint/onramp/initiate POST /v1/api/onramp/initiate Start a fiat-to-crypto on-ramp using a valid quote > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Redeems a quote and creates a virtual bank account for the customer to pay into. Show the returned `bankTransfer` details to the customer, then poll [Get On-ramp Status](/api-reference/endpoint/onramp/status) with the `reference`. * Each quote can be redeemed **once**. The quote is consumed as soon as you call this endpoint, even if it has expired; request a new quote after any failure. * Only the merchant that created the quote can redeem it. A quote from another merchant returns `Quote not found or already used`. * Tender doesn't check `targetAddress` against the chain at this point. Validate the address before you call; crypto sent to a wrong address can't be recovered. This request counts against the on-ramp limit of 10 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **quoteId** `string` *required* The `quoteId` returned by [Get On-ramp Quote](/api-reference/endpoint/onramp/quote). Must be a UUID. **Example:** `"b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c"` **targetAddress** `string` *required* Wallet address that receives the crypto, on the quote's `targetChain`. **Example:** `"TQ7xJm4z9..."` **customer** `object` *required* The customer making the bank transfer. **customer properties** **email** `string` *required* Customer email address. **Example:** `"customer@example.com"` **name** `string` *required* Customer full name. **Example:** `"Ada Obi"` **metadata** `object` Optional key-value pairs (string or number values). They are passed along with the payment request but are not returned by [Get On-ramp Status](/api-reference/endpoint/onramp/status), so keep your own mapping from `reference` to your order. **Example:** `{ "orderId": "ORD-9821" }` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"Onramp initiated"` **data** `object` *required* **data properties** **reference** `string` Unique reference for this on-ramp. Use it to poll status. **Example:** `"a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d"` **status** `string` Always `pending_payment` at this point. **Example:** `"pending_payment"` **bankTransfer** `object` Virtual bank account the customer must pay into. **bankTransfer properties** **accountNumber** `string` **Example:** `"0123456789"` **accountName** `string` **Example:** `"Tender / Ada Obi"` **bankName** `string` **Example:** `"Wema Bank"` **expiresAt** `string` ISO 8601 timestamp when the virtual account expires (1 hour after initiation). **Example:** `"2026-10-15T11:00:00.000Z"` **amount** `object` **amount properties** **value** `integer` Fiat amount the customer must pay, from the quote. **Example:** `50000` **currency** `string` Fiat currency, from the quote. **Example:** `"NGN"` **targetChain** `string` **Example:** `"tron"` **targetAddress** `string` **Example:** `"TQ7xJm4z9..."` **targetCurrency** `string` **Example:** `"USDT"` ## Error responses All errors return HTTP `400`. | Condition | Message | | - | - | | Missing or invalid field | Validation message, e.g. `quoteId must be a UUID` | | Quote unknown, already used, or created by another merchant | `Quote not found or already used` | | Quote past its `expiresAt` (the quote is still consumed) | `Quote has expired` | ```bash cURL theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/onramp/initiate \ -H "x-access-id: $TENDER_ACCESS_ID" \ -H "x-request-id: 3d6f0a1b-2c4e-4f8a-9b7c-5e1d2a3b4c5d" \ -H "x-timestamp: 2026-10-15T10:00:20.000Z" \ -H "authorization: $SIGNATURE" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c", "targetAddress": "TQ7xJm4z9...", "customer": { "email": "customer@example.com", "name": "Ada Obi" } }' ``` ```json 200 theme={null} { "status": "success", "message": "Onramp initiated", "data": { "reference": "a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d", "status": "pending_payment", "bankTransfer": { "accountNumber": "0123456789", "accountName": "Tender / Ada Obi", "bankName": "Wema Bank" }, "expiresAt": "2026-10-15T11:00:20.000Z", "amount": { "value": 50000, "currency": "NGN" }, "targetChain": "tron", "targetAddress": "TQ7xJm4z9...", "targetCurrency": "USDT" } } ``` ```json 400 theme={null} { "status": "error", "message": "Quote not found or already used", "data": {} } ``` # Get On-ramp Quote Source: https://docs.tender.cash/api-reference/endpoint/onramp/quote POST /v1/api/onramp/quote Fetch a live exchange rate estimate before initiating an on-ramp > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns a single-use quote that you pass to [Initiate On-ramp](/api-reference/endpoint/onramp/initiate). * **The quote expires 60 seconds after it is created.** Initiate before `expiresAt`. * **The minimum on-ramp is \$15 USD** worth of fiat. * Only the merchant that requested a quote can redeem it. This request counts against the on-ramp limit of 10 requests per minute. See [Rate limits](/api-reference/rate-limits). ## How the estimate is calculated `rate` is the number of fiat units per 1 USD (for example about `1600` for NGN). Tender converts the fiat amount to USD, rounds to 2 decimal places, then divides by the target coin's USD price: ``` usdValue = round(fiatAmount / rate, 2) estimatedCryptoAmount = usdValue / targetCoinUsdPrice (6 decimal places) ``` For example, 50,000 NGN at a rate of 1600 is $31.25, which is `31.250000` USDT at $1.00 per USDT. ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **fiatAmount** `integer` *required* Amount the customer will pay in `fiatCurrency`, in whole units (for example naira, not kobo). Must be an integer. **Example:** `50000` **fiatCurrency** `string` Fiat currency code, matched case-insensitively. Defaults to `NGN`. **Example:** `"NGN"` **targetCurrency** `string` *required* The coin's `symbol` from [Fetch On-ramp Coins](/api-reference/endpoint/onramp/coins), matched case-insensitively. **Example:** `"USDT"` **targetChain** `string` *required* The chain `id` to deliver on, from [Fetch On-ramp Chains](/api-reference/endpoint/onramp/chains). The coin must be available on this chain. **Example:** `"tron"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"Quote generated"` **data** `object` *required* **data properties** **quoteId** `string` UUID that identifies this quote. Pass it to [Initiate On-ramp](/api-reference/endpoint/onramp/initiate). **Example:** `"b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c"` **fiatCurrency** `string` **Example:** `"NGN"` **fiatAmount** `integer` **Example:** `50000` **targetCurrency** `string` As you sent it. **Example:** `"USDT"` **targetChain** `string` **Example:** `"tron"` **swapCurrency** `string` Same as `targetCurrency`. **Example:** `"USDT"` **swapChain** `string` Same as `targetChain`. **Example:** `"tron"` **rate** `number` Fiat units per 1 USD. **Example:** `1600` **estimatedCryptoAmount** `string` Estimated amount of `targetCurrency` the customer receives, to 6 decimal places. **Example:** `"31.250000"` **expiresAt** `string` ISO 8601 timestamp, 60 seconds after the quote was created. **Example:** `"2026-10-15T10:01:00.000Z"` ## Error responses All errors return HTTP `400`. | Condition | Message | | - | - | | Missing or invalid field | Validation message, e.g. `fiatAmount must be an integer number` | | Fiat currency not supported | `Fiat currency '' is not supported for on-ramp` | | Coin not supported | `Target currency '' is not supported` | | Chain not supported | `Target chain '' is not supported` | | Coin not available on the chain | `Currency '' does not support chain ''. Supported chains: …` | | Rate temporarily unavailable | `Buy rate not available for '' — price sync may be pending` | | Below the minimum | `Minimum on-ramp amount is $15 USD. Requested ≈ $ USD.` | ```bash cURL theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/onramp/quote \ -H "x-access-id: $TENDER_ACCESS_ID" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T10:00:00.000Z" \ -H "authorization: $SIGNATURE" \ -H "Content-Type: application/json" \ -d '{"fiatAmount": 50000, "fiatCurrency": "NGN", "targetCurrency": "USDT", "targetChain": "tron"}' ``` ```json 200 theme={null} { "status": "success", "message": "Quote generated", "data": { "quoteId": "b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c", "fiatCurrency": "NGN", "fiatAmount": 50000, "targetCurrency": "USDT", "targetChain": "tron", "swapCurrency": "USDT", "swapChain": "tron", "rate": 1600, "estimatedCryptoAmount": "31.250000", "expiresAt": "2026-10-15T10:01:00.000Z" } } ``` ```json 400 theme={null} { "status": "error", "message": "Minimum on-ramp amount is $15 USD. Requested 20000 NGN ≈ $12.50 USD.", "data": {} } ``` # Get On-ramp Status Source: https://docs.tender.cash/api-reference/endpoint/onramp/status GET /v1/api/onramp/{reference} Retrieve the current status and stage progress of an on-ramp request > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns the on-ramp for a `reference` returned by [Initiate On-ramp](/api-reference/endpoint/onramp/initiate). You can only read on-ramps created by your own merchant account; any other reference returns `404`. There are no on-ramp webhooks, so poll this endpoint to track progress. Polling counts against the on-ramp limit of **10 requests per minute**, shared with the other on-ramp endpoints. Poll every 30 seconds or so, and stop once the status is `crypto_sent` or `failed`. See [Rate limits](/api-reference/rate-limits). ## Status values | Status | Meaning | | - | - | | `pending_payment` | Waiting for the customer's bank transfer | | `processing` | Payment received; the conversion is running | | `pending` | A step failed and will be retried automatically. Keep polling. | | `crypto_sent` | Crypto sent to `targetAddress`. This is the final success status. | | `failed` | The on-ramp failed and won't be retried (for example, the virtual account expired unpaid, or a step failed 5 times) | ## Stages `stages` shows the progress of each step, in order: | Stage | What happens | | - | - | | `payment` | The customer's bank transfer is received | | `payout`, `swap`, `swapWithdraw` | The fiat is converted to the target coin. `swapWithdraw` is usually `skipped`. | | `cryptoSend` | The coin is sent to `targetAddress` | Each stage has a `status` of `pending`, `in_progress`, `completed`, `failed` or `skipped`. ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Path Parameters **reference** `string` *required* The `reference` returned by [Initiate On-ramp](/api-reference/endpoint/onramp/initiate). **Example:** `"a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **reference** `string` **Example:** `"a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d"` **status** `string` Overall status. See [Status values](#status-values). **Example:** `"processing"` **fiatCurrency** `string` **Example:** `"NGN"` **fiatAmount** `string` Fiat amount from the quote, as a string. **Example:** `"50000"` **amountReceived** `string` Fiat amount actually received. Present once the payment is confirmed. **Example:** `"50000"` **cryptoAmount** `string` Amount of `targetCurrency` to deliver, from the quote. **Example:** `"31.250000"` **targetChain** `string` **Example:** `"tron"` **targetCurrency** `string` **Example:** `"USDT"` **targetAddress** `string` **Example:** `"TQ7xJm4z9..."` **stages** `object` Progress of each step: `payment`, `payout`, `swap`, `swapWithdraw`, `cryptoSend`. **stage object** **status** `string` `pending`, `in_progress`, `completed`, `failed` or `skipped`. **Example:** `"completed"` **attempts** `integer` How many times the step has been attempted. **Example:** `1` **startedAt** `string` **Example:** `"2026-10-15T10:05:12.000Z"` **completedAt** `string` **Example:** `"2026-10-15T10:05:40.000Z"` **failedAt** `string` Time of the most recent failure, if any. **Example:** `"2026-10-15T10:05:30.000Z"` **stages.cryptoSend.data.txRef** `string` On-chain hash of the transaction that sent the crypto. Present once `cryptoSend` is `completed`. **Example:** `"7c1e9a4b..."` **createdAt** `string` **Example:** `"2026-10-15T10:00:20.000Z"` **updatedAt** `string` **Example:** `"2026-10-15T10:06:02.000Z"` Additional fields may be present; ignore fields you don't recognise. ## Error responses | Condition | Status | Message | | - | - | - | | Unknown reference, or one created by another merchant | 404 | `Onramp request not found` | | Rate limit exceeded | 429 | `Too many requests. Max 10 per 60s.` | ```json 200 theme={null} { "status": "success", "message": "success", "data": { "reference": "a3f1c2d4-8e7b-4f0a-9c1d-2e3f4a5b6c7d", "status": "crypto_sent", "fiatCurrency": "NGN", "fiatAmount": "50000", "amountReceived": "50000", "cryptoAmount": "31.250000", "targetChain": "tron", "targetCurrency": "USDT", "targetAddress": "TQ7xJm4z9...", "stages": { "payment": { "status": "completed", "attempts": 1, "startedAt": "2026-10-15T10:00:20.000Z", "completedAt": "2026-10-15T10:04:55.000Z" }, "payout": { "status": "completed", "attempts": 1, "startedAt": "2026-10-15T10:04:56.000Z", "completedAt": "2026-10-15T10:05:10.000Z" }, "swap": { "status": "completed", "attempts": 1, "startedAt": "2026-10-15T10:05:11.000Z", "completedAt": "2026-10-15T10:05:40.000Z" }, "swapWithdraw": { "status": "skipped", "attempts": 0 }, "cryptoSend": { "status": "completed", "attempts": 1, "startedAt": "2026-10-15T10:05:41.000Z", "completedAt": "2026-10-15T10:06:02.000Z", "data": { "txRef": "7c1e9a4b..." } } }, "createdAt": "2026-10-15T10:00:20.000Z", "updatedAt": "2026-10-15T10:06:02.000Z" } } ``` ```json 404 theme={null} { "status": "error", "message": "Onramp request not found", "data": {} } ``` # Payout Crypto Source: https://docs.tender.cash/api-reference/endpoint/payouts/crypto POST /v1/api/payout/crypto Send crypto from your balance to a wallet address or a saved payout account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Sends `amount` of `coin` on `chain` from your merchant balance. No OTP or password is needed; the signed headers authenticate the request. The fee is taken out of `amount`: your balance is debited `amount` and the recipient receives `amount` − fee. Check the fee first with [Calculate Payout Fee](/api-reference/endpoint/payouts/fee). The payout is created with status `pending` and processed asynchronously. There are no payout webhooks: poll [Get Payout](/api-reference/endpoint/payouts/get-one) with the returned `_id` until `status` is `completed` or `failed`. > [!WARNING] > > All payout endpoints share a bucket of 10 requests per minute per merchant, and each call to this > endpoint counts twice, so you can create about 5 crypto payouts per minute. See > [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Must be `application/json` **Example:** `"application/json"` **Idempotency-Key** `string` *required* A unique key you generate for this payout, up to 200 characters (a UUID works well). Retrying with the same key and the same body returns the original response instead of creating a second payout. See [Idempotency](/api-reference/idempotency). **Example:** `"payout-2026-10-15-0001"` ## Body **coin** `string` *required* Coin ID to pay out **Example:** `"usdt"` **chain** `string` *required* Chain ID to send on **Example:** `"tron"` **amount** `string` *required* Amount to pay out, in the coin, as a string. Must be worth at least \$20 USD. **Example:** `"50"` **address** `string` Destination wallet address on `chain`. **Required if `payoutAccountId` is not sent.** **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **payoutAccountId** `string` ID of one of your saved, active **crypto** payout accounts. Its chain and coin must match `chain` and `coin`. **Required if `address` is not sent.** If you send both, the saved account's address is used and `address` is ignored. **Example:** `"66aec7de809b7f45c42a49f9"` ## Idempotency Send a unique `Idempotency-Key` header with every payout request. Keys are scoped to your merchant account and to this endpoint, and are kept for 24 hours. * **Same key, same body:** you get the original response again; no new payout is created. * **Same key, different body:** `409` with `Idempotency-Key was already used with a different request`. * **Same key while the first request is still running:** `409` with `A request with this Idempotency-Key is already being processed`. Wait and retry. * **First request failed:** the key is released, so you can retry with the same key. * **Missing key:** `400` with `Idempotency-Key header is required`. See [Idempotency](/api-reference/idempotency). ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The new payout record **data properties** **\_id** `string` Payout ID. Use it with [Get Payout](/api-reference/endpoint/payouts/get-one). **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` Always `crypto` **Example:** `"crypto"` **status** `string` Always `pending` on creation **Example:** `"pending"` **coin** `string` **Example:** `"usdt"` **chain** `string` **Example:** `"tron"` **address** `string` Destination address **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **amount** `string` Net amount the recipient receives (`totalAmount` − `fee`) **Example:** `"49.5"` **totalAmount** `string` The amount you requested, debited from your balance **Example:** `"50"` **usdValue** `string` USD value of `amount` **Example:** `"49.5"` **totalAmountUSD** `string` USD value of `totalAmount` **Example:** `"50"` **fee** `string` Fee in the coin **Example:** `"0.5"` **feeUSD** `string` Fee in USD **Example:** `"0.5"` **payoutAccountId** `string` Present when you paid out to a saved account **createdAt** `string` **Example:** `"2026-10-15T09:20:15.650Z"` **updatedAt** `string` **Example:** `"2026-10-15T09:20:15.650Z"` Additional fields may be present; ignore fields you don't recognise. ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Idempotency-Key header is required` | The header is missing or blank | | 400 | `Idempotency-Key header is too long` | The key is over 200 characters | | 400 | `Invalid Chain` / `Invalid Currency` | Unknown `chain` or `coin` | | 400 | `Exchange Price Error` | No price is available for the coin | | 400 | `Invalid payout account reference` / `Payout account not found` / `Payout account is inactive` | `payoutAccountId` is unknown, not yours, or inactive | | 400 | `Account type mismatch. Expected crypto account` | `payoutAccountId` is a bank account | | 400 | `Saved account chain does not match withdrawal chain` | `payoutAccountId` is on a different chain | | 400 | `Saved account coin does not match withdrawal coin` | `payoutAccountId` is for a different coin | | 400 | `Insufficient balance for usdt. ...` | Your available balance on this chain is below `amount` | | 400 | `Invalid Address` | `address` is not valid for `chain` | | 400 | `Minimum withdrawal amount is $20 USD` | `amount` is worth less than \$20 | | 400 | `Withdrawal exceeds your 30-day offramp limit (...)` | The payout would exceed your account's 30-day limit | | 400 | `Failed to create crypto withdrawal` | The payout could not be created; retry with the same key | | 409 | `Idempotency-Key was already used with a different request` | Reused key with a different body | | 409 | `A request with this Idempotency-Key is already being processed` | The first request with this key hasn't finished | ```bash To an address theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/payout/crypto" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-10-15-0001" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " \ -d '{"coin":"usdt","chain":"tron","amount":"50","address":"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"}' ``` ```bash To a saved account theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/payout/crypto" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-10-15-0002" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 6f1c2a4e-8b3d-4c5e-9f7a-1b2c3d4e5f60" \ -H "x-timestamp: 2026-10-15T09:46:10.000Z" \ -H "authorization: " \ -d '{"coin":"usdt","chain":"tron","amount":"50","payoutAccountId":"66aec7de809b7f45c42a49f9"}' ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "_id": "66aec7de809b7f45c42a49f9", "type": "crypto", "status": "pending", "coin": "usdt", "chain": "tron", "address": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "amount": "49.5", "totalAmount": "50", "usdValue": "49.5", "totalAmountUSD": "50", "fee": "0.5", "feeUSD": "0.5", "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:20:15.650Z" } } ``` ```json 409 theme={null} { "status": "error", "message": "Idempotency-Key was already used with a different request", "data": {} } ``` # Calculate Payout Fee Source: https://docs.tender.cash/api-reference/endpoint/payouts/fee GET /v1/api/payout/fee/{chain}/{coin}/{amount} Get the fee for a crypto payout of a given coin and amount on a chain > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All payout endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). The fee is taken out of the amount you send. For a payout of `amount`, your balance is debited `amount` and the recipient receives `amountDeductedFee` (`amount` − `fee`). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **chain** `string` *required* Chain ID **Example:** `"tron"` **coin** `string` *required* Coin ID **Example:** `"usdt"` **amount** `string` *required* Amount to pay out, in the coin. Must be greater than zero. **Example:** `"50"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* Payout fee breakdown **data properties** **amount** `number` The amount you asked to pay out **Example:** `50` **amountUSD** `number` USD value of `amount` **Example:** `50` **fee** `number` Total fee in the coin, including any activation fee **Example:** `0.5` **feeUSD** `number` Total fee in USD **Example:** `0.5` **feePercent** `number` Percentage fee applied to `amount` **Example:** `1` **baseFeeUSD** `number` Cap on the percentage fee, in USD. The activation fee is added on top. **Example:** `100` **amountDeductedFee** `number` What the recipient receives: `amount` − `fee` **Example:** `49.5` **amountDeductedFeeUSD** `number` USD value of `amountDeductedFee` **Example:** `49.5` **amountAfterFee** `number` `amount` + `fee` (gross). This is not the amount delivered; use `amountDeductedFee` for that. **Example:** `50.5` **amountAfterFeeUSD** `number` USD value of `amountAfterFee` **Example:** `50.5` **activationFee** `number` Part of `fee` charged to activate the destination on chains that need it, in the coin. `0` when none applies. **Example:** `0` **activationFeeUSD** `number` Activation fee in USD **Example:** `0` **activationScope** `string` Which fee setting the activation fee came from: `merchant-coin`, `merchant-chain`, `global-coin`, `global-chain`, or `none` when no activation fee applies **Example:** `"none"` **inclusive** `boolean` Whether your account is set up for fee-inclusive pricing **Example:** `false` **currency** `string` Coin ID **Example:** `"usdt"` **chain** `string` Chain ID **Example:** `"tron"` **estimatedArrival** `string` Typical arrival time on this chain, or `"Not available"` **Example:** `"1-3 minutes"` ## Errors All return HTTP 400. | Message | Cause | | - | - | | `Invalid chain` | Unknown chain | | `Invalid currency` | Unknown coin | | `Exchange price not available` | No price for the coin | | `Amount must be greater than zero` | `amount` is zero or negative | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/payout/fee/tron/usdt/50" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "amount": 50, "amountUSD": 50, "fee": 0.5, "feeUSD": 0.5, "feePercent": 1, "baseFeeUSD": 100, "amountAfterFee": 50.5, "amountAfterFeeUSD": 50.5, "activationFee": 0, "activationFeeUSD": 0, "amountDeductedFee": 49.5, "amountDeductedFeeUSD": 49.5, "activationScope": "none", "inclusive": false, "currency": "usdt", "chain": "tron", "estimatedArrival": "1-3 minutes" } } ``` # Payout Fiat Source: https://docs.tender.cash/api-reference/endpoint/payouts/fiat POST /v1/api/payout/fiat Pay out from your balance to a Nigerian bank account, or as cash through MoneyGram > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Set `method` to choose how the payout is delivered: | `method` | What happens | Response | | - | - | - | | `internal` | Tender converts the asset and sends fiat to a Nigerian bank account. Tender chooses the payout provider; you can't select one. | The payout record (`id`, `status`, …) | | `moneygram` | Starts a MoneyGram payout. Your user opens the returned `url` to complete it. | `{ url, transactionId, id }` | Payouts are processed asynchronously. There are no payout webhooks: poll [Get Payout](/api-reference/endpoint/payouts/get-one) with the returned `id` until `status` is `completed` or `failed`. All payout endpoints share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Must be `application/json` **Example:** `"application/json"` **Idempotency-Key** `string` *required* A unique key you generate for this payout, up to 200 characters (a UUID works well). Retrying with the same key and the same body returns the original response instead of creating a second payout. See [Idempotency](/api-reference/idempotency). **Example:** `"payout-2026-10-15-0001"` ## Body **method** `string` *required* Payout method. **Allowed values:** `internal`, `moneygram` **Example:** `"internal"` **asset** `string` *required* Coin to pay out from your balance. For `internal`, any coin ID you hold (for example `usdc` or `usdt`). For `moneygram`, an asset MoneyGram supports (for example `usdc`). **Example:** `"usdc"` **amount** `string | number` *required* Amount of `asset` to pay out. Accepts a number or a numeric string. * `internal`: at least 20 units of the asset, and at least \$20 USD in value. * `moneygram`: at least 15 units of the asset. **Example:** `"100"` **reference** `string` A stable identifier for the end user this payout is for, up to 500 characters. It is stored on the payout as `withdrawalReference`. For `moneygram`, it is also sent to MoneyGram for reconciliation and decides which MoneyGram consumer the payout belongs to. Each distinct `reference` is a separate consumer, with its own KYC record and transaction-limit window. Reuse the same value for the same person on every payout, and use a different value for a different person. Don't use a per-transaction value such as an order ID. Omit it (or set it to your own merchant ID) to attribute the payout to your merchant account itself. **Example:** `"user-8821"` ### `internal` fields **bank\_code** `string` Recipient's Nigerian bank code. **Required for `internal`.** The bank must be supported. **Example:** `"090267"` **account\_number** `string` Recipient's bank account number. **Required for `internal`.** **Example:** `"0123456789"` ### `moneygram` fields All of these are optional. Any you send are pre-filled in the MoneyGram form your user completes, so they don't have to type them again. **address\_reference** `string` Send from one of your Stellar subwallet addresses instead of your merchant wallet. Use the `address_reference` you gave the address when you created it. **Example:** `"slot-2"` **first\_name** `string` **Example:** `"John"` **last\_name** `string` **Example:** `"Doe"` **dob** `string` Date of birth, `YYYY-MM-DD`. `date_of_birth` is accepted as an alias. **Example:** `"1990-06-15"` **mobile\_number** `string` Mobile number in international format **Example:** `"+2348012345678"` **address** `string` Street address **Example:** `"12 Marina Street"` **city** `string` **Example:** `"Lagos"` **postal\_code** `string` **Example:** `"100001"` **state\_or\_province** `string` **Example:** `"LA"` **country** `string` ISO 3166-1 alpha-2 country code **Example:** `"NG"` ## Idempotency Send a unique `Idempotency-Key` header with every payout request. Keys are scoped to your merchant account and to this endpoint, and are kept for 24 hours. * **Same key, same body:** you get the original response again; no new payout is created. * **Same key, different body:** `409` with `Idempotency-Key was already used with a different request`. * **Same key while the first request is still running:** `409` with `A request with this Idempotency-Key is already being processed`. Wait and retry. * **First request failed:** the key is released, so you can retry with the same key. * **Missing key:** `400` with `Idempotency-Key header is required`. See [Idempotency](/api-reference/idempotency). ## Response ### `internal` **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"Fiat payout initiated"` **data** `object` *required* **data properties** **id** `string` Payout ID. Use it with [Get Payout](/api-reference/endpoint/payouts/get-one). **Example:** `"6a45c1d2e4b0a1b2c3d4e5f9"` **status** `string` `pending` on creation **Example:** `"pending"` **amount** `string` Amount of `asset` paid out **Example:** `"100"` **asset** `string` **Example:** `"usdc"` **usdValue** `string` USD value of `amount` **Example:** `"100"` **bankName** `string` Bank name resolved from `bank_code` **Example:** `"KUDA MICROFINANCE BANK"` **bankCode** `string` **Example:** `"090267"` **accountNumber** `string` **Example:** `"0123456789"` **createdAt** `string` **Example:** `"2026-10-15T09:20:15.650Z"` ### `moneygram` **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"Withdrawal initiated"` **data** `object` *required* **data properties** **url** `string` MoneyGram page for this payout. Open `url` to let your user complete the payout. **Example:** `"https://extstellar.moneygram.com/..."` **transactionId** `string` MoneyGram transaction ID. It also appears as `providerTransactionId` on the payout record. **Example:** `"abc123"` **id** `string | null` Payout ID. Use it with [Get Payout](/api-reference/endpoint/payouts/get-one). In rare cases it is `null`; the MoneyGram payout still goes ahead. **Example:** `"6a45c1d2e4b0a1b2c3d4e5f9"` ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `Idempotency-Key header is required` | The header is missing or blank | | 400 | `method must be one of the following values: moneygram, internal` | Missing or unknown `method` | | 400 | `amount, bank_code, account_number, and asset are required for internal` | A required `internal` field is missing | | 400 | `Invalid asset` / `Exchange price not available` | Unknown or unpriced `asset` (`internal`) | | 400 | `Insufficient USDC balance: requested 100, available 40` | Your balance doesn't cover `amount` | | 400 | `Minimum withdrawal amount is 20 USDC` / `Minimum withdrawal amount is $20 USD` | Below the `internal` minimum | | 400 | `Bank code "123" is not supported` | Unsupported `bank_code` (`internal`) | | 400 | `Withdrawal exceeds your 30-day offramp limit (...)` | The payout would exceed your account's 30-day limit (`internal`) | | 400 | `Minimum MoneyGram withdrawal amount is 15 USDC` | Below the `moneygram` minimum | | 400 | `Merchant Stellar wallet not found` | You have no Stellar merchant wallet (`moneygram`) | | 404 | `Stellar subwallet address not found` | No active Stellar subwallet address matches `address_reference` | | 409 | `Idempotency-Key was already used with a different request` | Reused key with a different body | | 409 | `A request with this Idempotency-Key is already being processed` | The first request with this key hasn't finished | | 503 | `No withdrawal provider available for the selected asset` | MoneyGram doesn't support `asset` right now | ```bash internal theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/payout/fiat" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-10-15-0001" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " \ -d '{"method":"internal","asset":"usdc","amount":"100","bank_code":"090267","account_number":"0123456789","reference":"user-8821"}' ``` ```bash moneygram theme={null} curl -X POST "https://sandbox-api.tender.cash/v1/api/payout/fiat" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-10-15-0002" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 6f1c2a4e-8b3d-4c5e-9f7a-1b2c3d4e5f60" \ -H "x-timestamp: 2026-10-15T09:46:10.000Z" \ -H "authorization: " \ -d '{"method":"moneygram","asset":"usdc","amount":"100","reference":"user-8821","first_name":"John","last_name":"Doe","dob":"1990-06-15","mobile_number":"+2348012345678","address":"12 Marina Street","city":"Lagos","postal_code":"100001","state_or_province":"LA","country":"NG"}' ``` ```json 200 internal theme={null} { "status": "success", "message": "Fiat payout initiated", "data": { "id": "6a45c1d2e4b0a1b2c3d4e5f9", "status": "pending", "amount": "100", "asset": "usdc", "usdValue": "100", "bankName": "KUDA MICROFINANCE BANK", "bankCode": "090267", "accountNumber": "0123456789", "createdAt": "2026-10-15T09:20:15.650Z" } } ``` ```json 200 moneygram theme={null} { "status": "success", "message": "Withdrawal initiated", "data": { "url": "https://extstellar.moneygram.com/...", "transactionId": "abc123", "id": "6a45c1d2e4b0a1b2c3d4e5fa" } } ``` ```json 503 theme={null} { "status": "error", "message": "No withdrawal provider available for the selected asset", "data": {} } ``` # Get Payout Internal Transaction Source: https://docs.tender.cash/api-reference/endpoint/payouts/get-internal GET /v1/api/payout/{id}/internal/{internalId} One on-chain transaction Tender recorded to settle one of your payouts > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns one internal transaction of a payout, for example to check whether its `txHash` has been broadcast. Get the IDs from [List Payout Internal Transactions](/api-reference/endpoint/payouts/list-internal). An ID is only found under the payout it belongs to. All payout endpoints share a bucket of 10 requests per minute per merchant. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The payout ID: `_id` from [Payout Crypto](/api-reference/endpoint/payouts/crypto), or `id` from [Payout Fiat](/api-reference/endpoint/payouts/fiat) **Example:** `"66aec7de809b7f45c42a49f9"` **internalId** `string` *required* Internal transaction ID: `id` from [List Payout Internal Transactions](/api-reference/endpoint/payouts/list-internal) **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The internal transaction **data properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Internal transaction not found` | The payout isn't yours, the ID is malformed, or `internalId` isn't one of this payout's internal transactions | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/payout/66aec7de809b7f45c42a49f9/internal/6a12f0c4e4b0a1b2c3d4e6a7" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "66aec7de809b7f45c42a49f9", "parentType": "payout", "role": "conversion_send", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } } ``` ```json 404 theme={null} { "status": "error", "message": "Internal transaction not found", "data": {} } ``` # Get Payout Source: https://docs.tender.cash/api-reference/endpoint/payouts/get-one GET /v1/api/payout/{id} Retrieve one of your payouts by ID and check its status > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). There are no payout webhooks. Poll this endpoint until `status` is `completed` or `failed`. All payout endpoints share a bucket of 10 requests per minute per merchant, so poll no more often than every 10 seconds. See [Rate limits](/api-reference/rate-limits). For MoneyGram payouts, each call refreshes the status from MoneyGram. The raw MoneyGram status is returned in `providerStatus`, and `status` is set from it: | MoneyGram status | `status` | | - | - | | `completed` | `completed` | | `error`, `expired`, `refunded`, `too_small`, `too_large` | `failed` | | anything else (for example `pending_user_transfer_start`, `pending_anchor`) | `processing` | If MoneyGram can't be reached, the last stored status is returned. ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The payout ID: `_id` from [Payout Crypto](/api-reference/endpoint/payouts/crypto), or `id` from [Payout Fiat](/api-reference/endpoint/payouts/fiat) **Example:** `"66aec7de809b7f45c42a49f9"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The payout record **data properties** **\_id** `string` Payout ID **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` `crypto` or `fiat` **Example:** `"crypto"` **status** `string` `pending` (queued), `processing` (handed to the network or payout provider), `completed` or `failed` **Example:** `"completed"` **coin** `string` Coin paid out **Example:** `"usdt"` **chain** `string` Chain the payout is sent from **Example:** `"tron"` **amount** `string` For crypto payouts, the net amount the recipient receives (requested amount minus the fee). For fiat payouts, the requested amount. **Example:** `"49.5"` **totalAmount** `string` The amount you requested, which is what is debited from your balance **Example:** `"50"` **usdValue** `string` USD value of `amount` **Example:** `"49.5"` **totalAmountUSD** `string` USD value of `totalAmount` **Example:** `"50"` **fee** `string` Payout fee in `coin` (crypto payouts) **Example:** `"0.5"` **feeUSD** `string` Payout fee in USD (crypto payouts) **Example:** `"0.5"` **address** `string` Destination wallet address (crypto payouts) **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **payoutAccountId** `string` Saved payout account used, if any **txId** `string` On-chain transaction hash, once the payout has been broadcast **Example:** `"7c1f0a9d3e5b..."` **bankName** `string` Recipient bank name (bank payouts) **Example:** `"KUDA MICROFINANCE BANK"` **bankCode** `string` Recipient bank code (bank payouts) **Example:** `"090267"` **bankAccountNumber** `string` Recipient account number (bank payouts) **Example:** `"0123456789"` **bankAccountName** `string` Recipient account name, when known (bank payouts) **withdrawalReference** `string` The `reference` you sent with a fiat payout **Example:** `"user-8821"` **providerTransactionId** `string | null` MoneyGram transaction ID (MoneyGram payouts only) **Example:** `"abc123"` **providerStatus** `string | null` Raw status last reported by MoneyGram, for example `pending_user_transfer_start` or `completed` (MoneyGram payouts only) **Example:** `"pending_anchor"` **createdAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:20:15.650Z"` **updatedAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:22:41.003Z"` **chainId** `string` Internal ID of the chain record (not expanded on this endpoint) **currency** `string` Internal ID of the coin record (not expanded on this endpoint) Additional fields may be present; ignore fields you don't recognise. ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Withdrawal not Found` | No payout with this ID belongs to your merchant account | | 404 | `Failed to fetch Transactions` | The ID is malformed | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/payout/66aec7de809b7f45c42a49f9" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 Crypto theme={null} { "status": "success", "message": "success", "data": { "_id": "66aec7de809b7f45c42a49f9", "type": "crypto", "status": "completed", "coin": "usdt", "chain": "tron", "amount": "49.5", "totalAmount": "50", "usdValue": "49.5", "totalAmountUSD": "50", "fee": "0.5", "feeUSD": "0.5", "address": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "txId": "7c1f0a9d3e5b...", "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:22:41.003Z" } } ``` ```json 200 MoneyGram theme={null} { "status": "success", "message": "success", "data": { "_id": "6a45c1d2e4b0a1b2c3d4e5f9", "type": "fiat", "status": "processing", "coin": "USDC", "amount": "100", "withdrawalReference": "user-8821", "providerTransactionId": "abc123", "providerStatus": "pending_user_transfer_start", "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:24:10.220Z" } } ``` ```json 404 theme={null} { "status": "error", "message": "Withdrawal not Found", "data": {} } ``` # List Payouts Source: https://docs.tender.cash/api-reference/endpoint/payouts/list GET /v1/api/payout Retrieve your crypto and fiat payouts with pagination > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). All payout endpoints, including this one, share a bucket of 10 requests per minute per merchant, on top of the general limit. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Query parameters **page** `number` (default: 1) Page number, starting at 1 **Example:** `1` **limit** `number` (default: 10) Number of records per page **Example:** `10` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* Paginated payouts, most recently updated first **data properties** **data** `array` Array of payout objects **payout object** **\_id** `string` Payout ID **Example:** `"66aec7de809b7f45c42a49f9"` **type** `string` `crypto` or `fiat` **Example:** `"crypto"` **status** `string` `pending` (queued), `processing` (handed to the network or payout provider), `completed` or `failed` **Example:** `"completed"` **coin** `string` Coin paid out **Example:** `"usdt"` **chain** `string` Chain the payout is sent from **Example:** `"tron"` **amount** `string` For crypto payouts, the net amount the recipient receives (requested amount minus the fee). For fiat payouts, the requested amount. **Example:** `"49.5"` **totalAmount** `string` The amount you requested, which is what is debited from your balance **Example:** `"50"` **usdValue** `string` USD value of `amount` **Example:** `"49.5"` **totalAmountUSD** `string` USD value of `totalAmount` **Example:** `"50"` **fee** `string` Payout fee in `coin` (crypto payouts) **Example:** `"0.5"` **feeUSD** `string` Payout fee in USD (crypto payouts) **Example:** `"0.5"` **address** `string` Destination wallet address (crypto payouts) **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **payoutAccountId** `string` Saved payout account used, if any **txId** `string` On-chain transaction hash, once the payout has been broadcast **Example:** `"7c1f0a9d3e5b..."` **bankName** `string` Recipient bank name (bank payouts) **Example:** `"KUDA MICROFINANCE BANK"` **bankCode** `string` Recipient bank code (bank payouts) **Example:** `"090267"` **bankAccountNumber** `string` Recipient account number (bank payouts) **Example:** `"0123456789"` **bankAccountName** `string` Recipient account name, when known (bank payouts) **withdrawalReference** `string` The `reference` you sent with a fiat payout **Example:** `"user-8821"` **providerTransactionId** `string | null` MoneyGram transaction ID (MoneyGram payouts only) **Example:** `"abc123"` **providerStatus** `string | null` Raw status last reported by MoneyGram, for example `pending_user_transfer_start` or `completed` (MoneyGram payouts only) **Example:** `"pending_anchor"` **createdAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:20:15.650Z"` **updatedAt** `string` ISO 8601 timestamp **Example:** `"2026-10-15T09:22:41.003Z"` **chainId** `object` The chain record, with `id`, `name`, `icon` and `chainType` **Example:** `{ "id": "tron", "name": "Tron", "chainType": "tron" }` **currency** `object` The coin record, with `id`, `name`, `symbol` and `icon` **Example:** `{ "id": "usdt", "name": "Tether USD", "symbol": "USDT" }` **total** `number` Total number of payouts **Example:** `23` **pages** `number` Total number of pages **Example:** `3` **page** `number` Current page **Example:** `1` **limit** `number` Items per page **Example:** `10` Additional fields may be present; ignore fields you don't recognise. ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/payout?page=1&limit=10" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "data": [ { "_id": "66aec7de809b7f45c42a49f9", "type": "crypto", "status": "completed", "coin": "usdt", "chain": "tron", "amount": "49.5", "totalAmount": "50", "usdValue": "49.5", "totalAmountUSD": "50", "fee": "0.5", "feeUSD": "0.5", "address": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "txId": "7c1f0a9d3e5b...", "chainId": { "id": "tron", "name": "Tron", "chainType": "tron" }, "currency": { "id": "usdt", "name": "Tether USD", "symbol": "USDT" }, "createdAt": "2026-10-15T09:20:15.650Z", "updatedAt": "2026-10-15T09:22:41.003Z" } ], "total": 1, "pages": 1, "page": 1, "limit": 10 } } ``` # List Payout Internal Transactions Source: https://docs.tender.cash/api-reference/endpoint/payouts/list-internal GET /v1/api/payout/{id}/internal The on-chain transactions Tender recorded to settle one of your payouts > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). The ledger transactions behind a fiat payout made with `method: "internal"`. That payout runs as a conversion: your crypto is sent on-chain to the payout provider, which then pays the bank account. This endpoint returns that on-chain send. Results are oldest first. Other payouts (crypto, MoneyGram) have no internal transactions and return `200` with an empty `data` array. All payout endpoints share a bucket of 10 requests per minute per merchant. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The payout ID: `_id` from [Payout Crypto](/api-reference/endpoint/payouts/crypto), or `id` from [Payout Fiat](/api-reference/endpoint/payouts/fiat) **Example:** `"66aec7de809b7f45c42a49f9"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `array` *required* Internal transactions, oldest first **item properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Payout not found` | No payout with this ID belongs to your merchant account, or the ID is malformed | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/payout/66aec7de809b7f45c42a49f9/internal" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "66aec7de809b7f45c42a49f9", "parentType": "payout", "role": "conversion_send", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } ] } ``` ```json 200 Empty theme={null} { "status": "success", "message": "success", "data": [] } ``` ```json 404 theme={null} { "status": "error", "message": "Payout not found", "data": {} } ``` # Quote Payment Source: https://docs.tender.cash/api-reference/endpoint/quote-payment POST /v1/api/payment/quote/{id} Get the all-in amount a customer would pay, without creating a payment > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Prices a payment the same way [Initiate Payment By Fiat Currency](/api-reference/endpoint/initiate-payment-currency) does, but creates nothing: no deposit address, no transaction and no webhook. Use it to show the customer the exact total, including fees, before they commit to a chain and currency. `totalPayable` is the same all-in figure that initiation returns as `amount`/`coinAmount`: the payment plus the receive fee, any activation fee, any auto-payout fees and, on Tron, the gasless network service fee. Prices can move between the quote and initiation, so the initiated amount can differ slightly if time passes. The same checks apply as on initiation, including the minimum amount and your outlet's allowed currencies. Payment endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). ## Path Parameters **id** `string` *required* Fiat currency code that `amount` is priced in, such as `ngn` or `usd`. Use a fiat currency listed by [Fetch All Currencies](/api-reference/endpoint/fetch-currencies) with `type=fiat`. **Example:** `"ngn"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` **Content-Type** `string` *required* Media type of the request body **Example:** `"application/json"` ## Body **amount** `string` *required* The price to charge, in the fiat currency named in the path, as a decimal string **Example:** `"15000"` **chain** `string` *required* Chain identifier, as returned by [Fetch Allowed Chains](/api-reference/endpoint/fetch-chains) **Example:** `"tron"` **currency** `string` *required* Cryptocurrency identifier. Must be supported on `chain` and allowed for your outlet. **Example:** `"usdt"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Human-readable message describing the result **Example:** `"success"` **data** `object` *required* The quote **data properties** **coin** `string` Cryptocurrency identifier **Example:** `"usdt"` **symbol** `string` Cryptocurrency symbol **Example:** `"USDT"` **chain** `string` Chain identifier **Example:** `"tron"` **currency** `string` The fiat currency the quote is priced in **Example:** `"ngn"` **rate** `number` Price of one unit of the cryptocurrency in `currency` **Example:** `1500` **usdRate** `number` Price of one unit of the cryptocurrency in USD **Example:** `1` **coinAmount** `number` The payment itself in the cryptocurrency, before fees **Example:** `10` **fee** `number` Every fee the customer covers, combined, in the cryptocurrency **Example:** `0.15` **feeUSD** `number` `fee` in USD **Example:** `0.15` **totalPayable** `number` The all-in cryptocurrency amount the customer would send (`coinAmount` + `fee`) **Example:** `10.15` **totalPayableUSD** `number` `totalPayable` in USD **Example:** `10.15` ## Errors | HTTP | Message | Cause | | - | - | - | | 400 | `amount must be a string` (or another validation message) | A required field is missing or has the wrong type | | 400 | `Invalid chain` / `Chain not active` | Unknown or disabled `chain` | | 400 | `invalid Currency` / `Currency not allowed` | Unknown `currency`, or not supported on `chain` | | 400 | `Amount can't be Zero` | `amount` is zero or negative | | 400 | `Minimum initiated amount is USD` | The payment is below the minimum | | 400 | `Invalid Coin Exchange Price` | No price is available for the currency right now | | 403 | `You are not authorized to initiate transactions with this currency` | The currency isn't in your outlet's allowed currencies | An unsupported fiat code in the path fails the request; use a code listed by Fetch All Currencies. Error responses use the standard envelope with `data: null`. See [Errors](/api-reference/errors). ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/payment/quote/ngn \ -H "x-access-id: your-access-id-here" \ -H "Content-Type: application/json" \ -d '{ "amount": "15000", "chain": "tron", "currency": "usdt" }' ``` ## Example response ```json theme={null} { "status": "success", "message": "success", "data": { "coin": "usdt", "symbol": "USDT", "chain": "tron", "currency": "ngn", "rate": 1500, "usdRate": 1, "coinAmount": 10, "fee": 0.15, "feeUSD": 0.15, "totalPayable": 10.15, "totalPayableUSD": 10.15 } } ``` # Add Deposit Filter Source: https://docs.tender.cash/api-reference/endpoint/subwallets/add-filter POST /v1/api/wallet/filter Register a blockchain address to be monitored for incoming deposits > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Registers an address with Tender's deposit monitor. Once registered, each deposit detected on the address triggers a `sub_user_wallet_transaction_detected` webhook to your server. See [Subwallets](/guides/subwallets#step-3-watch-for-deposits) for the payload. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/filter`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ address: '0x1a2b3c...', chains: ['EVM'], }), }); const { data } = await res.json(); // data.watched → true // data.chains → ['BTC', 'EVM', 'TRON'] ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet/filter", headers=make_headers(), json={"address": "0x1a2b3c...", "chains": ["EVM"]}, ) data = res.json()["data"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **address** `string` *required* The address to watch. It must be a valid address on at least one of `chains`. Keep the original casing for TRON and BTC addresses. **Example:** `"0x1a2b3c..."` **chains** `string[]` *required* Network types to watch the address on (at least one): `EVM`, `TRON` or `BTC`, case-insensitive. `EVM` covers every EVM chain that subwallets support. **Example:** `["EVM"]` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **address** `string` The address that was registered. **Example:** `"0x1a2b3c..."` **watched** `boolean` `true` once the address is registered. **Example:** `true` **chains** `string[]` Every network type the deposit monitor is currently watching, sorted. This can include types you didn't send in this request. **Example:** `["BTC", "EVM", "TRON"]` ## Error responses | Condition | Status | Message | | - | - | - | | `address` or `chains` missing or invalid | 400 | Validation message, e.g. `chains must contain at least 1 elements` | | Address not valid on any of `chains` | 400 | `address
is invalid for the provided chains` | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | # Check Watched Address Source: https://docs.tender.cash/api-reference/endpoint/subwallets/check-watched GET /v1/api/wallet/watch/{address} Check whether a blockchain address is currently registered with Tender's deposit monitor > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). The address is matched case-insensitively. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/watch/0x1a2b3c...`, { headers: makeHeaders(), }); const { data } = await res.json(); // data.watched → true | false ``` ```python Python theme={null} res = requests.get( f"{BASE}/wallet/watch/0x1a2b3c...", headers=make_headers(), ) watched = res.json()["data"]["watched"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Path Parameters **address** `string` *required* The blockchain address to check. **Example:** `"0x1a2b3c..."` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **watched** `boolean` `true` if the address is registered with the deposit monitor. **Example:** `true` ## Error responses | Condition | Status | Message | | - | - | - | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | # Create Address Source: https://docs.tender.cash/api-reference/endpoint/subwallets/create-address POST /v1/api/wallet/address Generate additional blockchain addresses on specific networks for an existing subwallet > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Creates a new address slot in an existing wallet, with one address per requested network. All addresses created by one call share the same `address_reference`. > [!WARNING] > > `address_reference` values are **globally unique** across Tender. Reusing one, even in a > different wallet, fails with `409`. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/address`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ wallet_reference: 'acme-user-8821', address_reference: 'acme-user-8821-slot-2', networks: ['EVM', 'TRON'], }), }); const { data } = await res.json(); /* { wallet_reference: 'acme-user-8821', address_reference: 'acme-user-8821-slot-2', addresses: [ { network: 'EVM', address: '0x1a2b...', gasless_address: '0x7d8e...', address_reference: 'acme-user-8821-slot-2' }, { network: 'TRON', address: 'TPx3...', gasless_address: 'TLq9...', address_reference: 'acme-user-8821-slot-2' } ] } */ ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet/address", headers=make_headers(), json={ "wallet_reference": "acme-user-8821", "address_reference": "acme-user-8821-slot-2", "networks": ["EVM", "TRON"], }, ) data = res.json()["data"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **wallet\_reference** `string` *required* The `reference` of a wallet you created with [Create Wallet](/api-reference/endpoint/subwallets/create-wallet). **Example:** `"acme-user-8821"` **address\_reference** `string` *required* Your label for the new address slot. Must be unique across Tender. Maximum 500 characters. **Example:** `"acme-user-8821-slot-2"` **networks** `string[]` *required* Networks to create addresses on (at least one). Use `EVM`, `TRON` or `BTC`. The aliases `ethereum`, `eth`, `trx` and `bitcoin` are also accepted, but each returned `network` echoes the value you sent, so use the canonical values to keep your records consistent with the rest of the API. **Example:** `["EVM", "TRON"]` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **wallet\_reference** `string` **Example:** `"acme-user-8821"` **address\_reference** `string` **Example:** `"acme-user-8821-slot-2"` **addresses** `array` The addresses created by this call. **address object** **network** `string` The network as you requested it. **Example:** `"EVM"` **address** `string` **Example:** `"0x1a2b3c..."` **gasless\_address** `string` The address used for gasless token transfers on this network. Empty string when the network has no gasless address. **Example:** `"0x7d8e9f..."` **address\_reference** `string` **Example:** `"acme-user-8821-slot-2"` Additional fields may be present; ignore fields you don't recognise. ## Error responses | Condition | Status | Message | | - | - | - | | Missing or invalid field | 400 | Validation message, e.g. `networks must contain at least 1 elements` | | `address_reference` already exists | 409 | `Address with this reference already exists` | | Wallet belongs to another merchant | 400 | `Wallet reference not found for this merchant` | | Wallet not found or network not supported | 400 | Message describing the problem | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | # Create Wallet Source: https://docs.tender.cash/api-reference/endpoint/subwallets/create-wallet POST /v1/api/wallet Provision a new subwallet for an end-user under your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Creates a wallet and provisions one address on every network that subwallets currently support. Each address gets its own generated `address_reference`. To add more addresses later, use [Create Address](/api-reference/endpoint/subwallets/create-address). > [!WARNING] > > Wallet references are **globally unique** across Tender, not only within your merchant account. > If the reference is already taken, the request fails with `409`. Prefix references with something > specific to your platform (for example `acme-user-8821`). ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ reference: 'acme-user-8821' }), }); const { data } = await res.json(); /* { reference: 'acme-user-8821', addresses: [ { network: 'EVM', address: '0x4f2a...', gasless_address: '0x9c1b...', address_reference: '3f6c1e9a-...' }, { network: 'TRON', address: 'TQ7x...', gasless_address: 'TJm4...', address_reference: '8b2d47f0-...' }, { network: 'BTC', address: 'bc1q...', gasless_address: '', address_reference: 'c90e5a12-...' } ] } */ ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet", headers=make_headers(), json={"reference": "acme-user-8821"}, ) data = res.json()["data"] # data["reference"] → "acme-user-8821" # data["addresses"] → list of address objects ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **reference** `string` *required* A unique identifier for this wallet, such as your own user ID. It must be unique across all Tender wallets. You use it as `wallet_reference` / `merchant_reference` in later calls. Maximum 500 characters. **Example:** `"acme-user-8821"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **reference** `string` The wallet reference you provided. **Example:** `"acme-user-8821"` **addresses** `array` One address per supported network. **address object** **network** `string` The network type of the address, in upper case: for example `EVM`, `TRON` or `BTC`. More values can appear as networks are added; treat the value as an opaque label. **Example:** `"EVM"` **address** `string` The wallet's own address on this network. **Example:** `"0x4f2a9b..."` **gasless\_address** `string` The address used for gasless token transfers on this network. Token transfers that run gasless spend from this address, so tokens you plan to send must be held here. Empty string when the network has no gasless address. **Example:** `"0x9c1b7e..."` **address\_reference** `string` Generated reference for this address slot. Pass it as `address_reference` when you transfer from this address. **Example:** `"3f6c1e9a-2b7d-4e8f-9a10-5c4b3a2d1e0f"` Additional fields may be present; ignore fields you don't recognise. ## Error responses | Condition | Status | Message | | - | - | - | | `reference` missing or invalid | 400 | Validation message, e.g. `reference must be a string` | | Reference already exists | 409 | `Wallet with this reference already exists` | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | A `503` means the wallet was not created, or its state is unknown. Retry with the same `reference`; if the wallet was created, the retry returns `409` and you can read it with [List Wallets](/api-reference/endpoint/subwallets/list-wallets). This endpoint shares the subwallet rate limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). # Get Address Balance Source: https://docs.tender.cash/api-reference/endpoint/subwallets/get-balance GET /v1/api/wallet/balance/{address} Fetch the native and stablecoin balances of a subwallet address > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns the on-chain balances of an address. Without `currency`, you get every balance Tender tracks for the address. With `currency`, you get a single number. Balances are cached for **180 seconds**, so a deposit or transfer can take up to three minutes to show here. Use the deposit and transfer webhooks for real-time updates. ## Example ```javascript Node.js theme={null} // All balances const all = await fetch(`${BASE}/wallet/balance/0x4f2a9b...?chain=ethereum`, { headers: makeHeaders(), }); const { data } = await all.json(); // data.balances → { native: 0.0021, USDT: 8, USDC: 0 } // One currency const one = await fetch(`${BASE}/wallet/balance/0x4f2a9b...?chain=ethereum¤cy=usdt`, { headers: makeHeaders(), }); const { data: single } = await one.json(); // single.balance → 8 ``` ```python Python theme={null} res = requests.get( f"{BASE}/wallet/balance/0x4f2a9b...", params={"chain": "ethereum", "currency": "usdt"}, headers=make_headers(), ) balance = res.json()["data"]["balance"] # balance → 8 ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Path Parameters **address** `string` *required* The blockchain address to query. **Example:** `"0x4f2a9b..."` ## Query Parameters **chain** `string` *required* Chain the address is on: `ethereum`, `tron` or `bitcoin`. **Example:** `"ethereum"` **currency** `string` Optional currency symbol, matched case-insensitively (for example `USDT`, `usdc`, `ETH`). It must be supported on `chain`. Omit it to get all balances. **Example:** `"USDT"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* The shape depends on whether you sent `currency`. **data without currency** **balances** `object` Balances in human-readable units, keyed by asset. **balances properties** **native** `number` Balance of the chain's native coin (ETH, TRX or BTC). **Example:** `0.0021` **USDT** `number` USDT balance. Present on `ethereum` and `tron` only. **Example:** `8` **USDC** `number` USDC balance. Present on `ethereum` and `tron` only. **Example:** `0` **data with currency** **balance** `number` Balance of the requested currency in human-readable units. `0` if the balance could not be read. **Example:** `8` ## Error responses | Condition | Status | Message | | - | - | - | | `chain` missing | 400 | `chain query param is required` | | Chain not recognised | 400 | `invalid chain: ` | | Chain not supported for subwallets | 400 | `Unsupported chain for subwallets` | | Currency not supported on that chain | 400 | `Unsupported currency for chain ` | | Address not valid or network not supported | 400 | Message describing the problem | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | This endpoint has a tighter limit of **30 requests per minute**, in addition to the subwallet limit. See [Rate limits](/api-reference/rate-limits). # List Addresses Source: https://docs.tender.cash/api-reference/endpoint/subwallets/list-addresses GET /v1/api/wallet/{walletReference}/addresses Retrieve all blockchain addresses for a specific subwallet > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns the addresses in one of your wallets, most recently updated first. `data` is a plain array; this endpoint returns no pagination metadata. To read every address, request the next `page` until you get fewer than `limit` items. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/acme-user-8821/addresses?page=1&limit=20`, { headers: makeHeaders(), }); const { data } = await res.json(); /* [ { network: 'EVM', address: '0x1a2b...', gasless_address: '0x7d8e...', address_reference: 'acme-user-8821-slot-2' }, { network: 'TRON', address: 'TPx3...', gasless_address: 'TLq9...', address_reference: 'acme-user-8821-slot-2' }, { network: 'BTC', address: 'bc1q...', gasless_address: null, address_reference: 'c90e5a12-...' } ] */ ``` ```python Python theme={null} res = requests.get( f"{BASE}/wallet/acme-user-8821/addresses", headers=make_headers(), params={"page": 1, "limit": 20}, ) addresses = res.json()["data"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Path Parameters **walletReference** `string` *required* The wallet's reference. **Example:** `"acme-user-8821"` ## Query Parameters **page** `integer` Page number, starting at `1`. Defaults to `1`. **Example:** `1` **limit** `integer` Addresses per page, from `1` to `100`. Defaults to `20`. **Example:** `20` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `array` *required* The wallet's addresses. An unknown wallet reference, or one that belongs to another merchant, returns an empty array. **address object** **network** `string` **Example:** `"EVM"` **address** `string` **Example:** `"0x1a2b3c..."` **gasless\_address** `string | null` The gasless address for this network, or `null` if there is none. **Example:** `"0x7d8e9f..."` **address\_reference** `string` **Example:** `"acme-user-8821-slot-2"` # List Wallet Transactions Source: https://docs.tender.cash/api-reference/endpoint/subwallets/list-transactions GET /v1/api/wallet/{walletReference}/transactions Retrieve the deposits and outbound transfers of a subwallet > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns one wallet's deposits (`inbound`) and transfers (`outbound`) merged into a single list, newest first. Pagination fields are **nested inside `data`**, next to the inner `data` array. > [!WARNING] > > Amounts use different units. Inbound `amount` is in the token's smallest unit (for example > `50000000` for 50 USDT, which has 6 decimals; the deposit webhook carries `decimals`). Outbound > `amount` is in human-readable units, as you sent it to > [Transfer Funds](/api-reference/endpoint/subwallets/transfer). ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/acme-user-8821/transactions?page=1&limit=20`, { headers: makeHeaders(), }); const { data } = await res.json(); const { data: txs, total, pages, page, limit } = data; for (const tx of txs) { if (tx.transaction_type === 'inbound') { // deposit: tx.amount is in base units } else { // transfer: tx.status is pending | confirmed | failed } } ``` ```python Python theme={null} res = requests.get( f"{BASE}/wallet/acme-user-8821/transactions", headers=make_headers(), params={"page": 1, "limit": 20}, ) page = res.json()["data"] txs, total = page["data"], page["total"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Path Parameters **walletReference** `string` *required* The wallet's reference. **Example:** `"acme-user-8821"` ## Query Parameters **page** `integer` Page number, starting at `1`. Defaults to `1`. **Example:** `1` **limit** `integer` Transactions per page, from `1` to `100`. Defaults to `20`. **Example:** `20` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **data** `array` Transactions on this page, newest first. An unknown wallet reference, or one that belongs to another merchant, returns an empty array. **inbound transaction** **transaction\_type** `string` `"inbound"` **txHash** `string` **Example:** `"0x5e8d..."` **walletReference** `string` **Example:** `"acme-user-8821"` **addressReference** `string | null` **Example:** `"acme-user-8821-slot-2"` **symbol** `string` **Example:** `"USDT"` **amount** `string` Amount in the token's smallest unit. **Example:** `"50000000"` **address** `string` The receiving address. **Example:** `"0x1a2b3c..."` **network** `string` Network of the receiving address, as stored on the address (for example `EVM`). **Example:** `"EVM"` **chainId** `string` The chain the deposit arrived on. **Example:** `"ethereum"` **confirmed** `boolean` Whether the deposit had reached its required confirmations when last reported. **Example:** `true` **blockNumber** `integer` **Example:** `21034567` **createdAt** `string` **Example:** `"2026-10-15T09:00:00.000Z"` **outbound transaction** **transaction\_type** `string` `"outbound"` **txHash** `string` On-chain hash. Empty string for a gasless transfer whose hash isn't known yet. **Example:** `"7c1e9a..."` **walletReference** `string` **Example:** `"acme-user-8821"` **addressReference** `string | null` **Example:** `"acme-user-8821-slot-2"` **symbol** `string | null` **Example:** `"USDT"` **amount** `string | null` Amount in human-readable units. **Example:** `"50"` **sender** `string | null` **Example:** `"TLq9..."` **receiver** `string | null` **Example:** `"TABCef..."` **network** `string` Network type of the transfer, returned as-is (for example `tron`). Compare case-insensitively. **Example:** `"tron"` **status** `string` `pending`, `confirmed` or `failed`. **Example:** `"confirmed"` **paymentReference** `string` The `payment_reference` you sent with the transfer. **Example:** `"payout-9922"` **blockNumber** `integer | null` **Example:** `66123456` **createdAt** `string` **Example:** `"2026-10-15T09:05:00.000Z"` **total** `integer` Total number of transactions across all pages. **Example:** `2` **pages** `integer` **Example:** `1` **page** `integer` **Example:** `1` **limit** `integer` **Example:** `20` ### Example response ```json theme={null} { "status": "success", "message": "success", "data": { "data": [ { "transaction_type": "outbound", "txHash": "7c1e9a...", "walletReference": "acme-user-8821", "addressReference": "acme-user-8821-slot-2", "symbol": "USDT", "amount": "50", "sender": "TLq9...", "receiver": "TABCef...", "network": "tron", "status": "confirmed", "paymentReference": "payout-9922", "blockNumber": 66123456, "createdAt": "2026-10-15T09:05:00.000Z" }, { "transaction_type": "inbound", "txHash": "5e8d...", "walletReference": "acme-user-8821", "addressReference": "acme-user-8821-slot-2", "symbol": "USDT", "amount": "50000000", "address": "TLq9...", "network": "TRON", "chainId": "tron", "confirmed": true, "blockNumber": 66120001, "createdAt": "2026-10-15T09:00:00.000Z" } ], "total": 2, "pages": 1, "page": 1, "limit": 20 } } ``` # List Wallets Source: https://docs.tender.cash/api-reference/endpoint/subwallets/list-wallets GET /v1/api/wallet Retrieve all subwallets created under your merchant account > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns your wallets, most recently updated first, with every address in each wallet. Pagination fields are returned at the top level of the response, next to `data`. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet?page=1&limit=20`, { headers: makeHeaders(), }); const { data, total, pages, page, limit } = await res.json(); /* data: [ { wallet_reference: 'acme-user-8821', addresses: [ { network: 'EVM', address: '0x4f2a...', gasless_address: '0x9c1b...', address_reference: '3f6c1e9a-...' }, { network: 'BTC', address: 'bc1q...', gasless_address: null, address_reference: 'c90e5a12-...' } ] } ], total: 42, pages: 3, page: 1, limit: 20 */ ``` ```python Python theme={null} res = requests.get( f"{BASE}/wallet", headers=make_headers(), params={"page": 1, "limit": 20}, ) body = res.json() wallets = body["data"] total = body["total"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` ## Query Parameters **page** `integer` Page number, starting at `1`. Defaults to `1`. **Example:** `1` **limit** `integer` Wallets per page, from `1` to `100`. Defaults to `20`. Values above `100` return `400`. **Example:** `20` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `array` *required* List of wallet objects. **wallet object** **wallet\_reference** `string` The wallet's reference. **Example:** `"acme-user-8821"` **addresses** `array` All addresses belonging to this wallet. **address object** **network** `string` **Example:** `"EVM"` **address** `string` **Example:** `"0x4f2a9b..."` **gasless\_address** `string | null` The gasless address for this network, or `null` if there is none. **Example:** `"0x9c1b7e..."` **address\_reference** `string` **Example:** `"3f6c1e9a-2b7d-4e8f-9a10-5c4b3a2d1e0f"` **total** `integer` Total number of wallets across all pages. **Example:** `42` **pages** `integer` Total number of pages. **Example:** `3` **page** `integer` Current page number. **Example:** `1` **limit** `integer` Number of results per page. **Example:** `20` # Transfer Funds Source: https://docs.tender.cash/api-reference/endpoint/subwallets/transfer POST /v1/api/wallet/transfer Send native coins or tokens from a subwallet address > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). One endpoint handles native coin transfers (ETH, TRX, BTC) and token transfers (USDT, USDC). You identify the source address by its wallet reference and address reference; Tender picks the right transfer type from `currency`. **Supported chains:** `ethereum`, `tron`, `bitcoin` ## Transfer types and responses | Transfer | When | HTTP | `data` | | - | - | - | - | | Coin | `currency` is the chain's native coin | `200` | `{ tx_id }` | | Token | `currency` is a token, sent on-chain directly | `200` | `{ confirmation_count, tx_id }` | | Gasless token | `currency` is a token and the network runs token transfers gasless | `202` | `{ confirmation_count, operation_id }` | A `200` means the transaction was broadcast. Tender sends a `sub_user_wallet_transfer_confirmed` webhook when it confirms on-chain. A `202` means a gasless operation was accepted but has not settled. Either poll [Get Transfer Status](/api-reference/endpoint/subwallets/transfer-status) with the `operation_id`, or wait for the `sub_user_wallet_transfer_confirmed` or `sub_user_wallet_transfer_failed` webhook. Gasless token transfers spend from the address slot's `gasless_address`, so the tokens must be held there. > [!WARNING] > > This endpoint has no idempotency key. `payment_reference` is stored for reconciliation but does > not block duplicates. If a request times out, check > [List Wallet Transactions](/api-reference/endpoint/subwallets/list-transactions) for your > `payment_reference` before you retry. ## Example ```javascript Node.js theme={null} // Token transfer (USDT on Tron) const res = await fetch(`${BASE}/wallet/transfer`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ chain: 'tron', currency: 'usdt', receiver: 'TABCef...', amount: 50, merchant_reference: 'acme-user-8821', // the wallet reference address_reference: 'acme-user-8821-slot-2', // the address slot to send from payment_reference: 'payout-9922', // your reference for this transfer }), }); const body = await res.json(); if (res.status === 202) { // Gasless: body.data → { confirmation_count: 0, operation_id: "f3b0c2..." } } else { // On-chain: body.data.tx_id → "a1b2c3..." } ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet/transfer", headers=make_headers(), json={ "chain": "ethereum", "currency": "ethereum", "receiver": "0x999fff...", "amount": 0.01, "merchant_reference": "acme-user-8821", "address_reference": "3f6c1e9a-2b7d-4e8f-9a10-5c4b3a2d1e0f", "payment_reference": "payout-9921", }, ) data = res.json()["data"] # res.status_code == 200, data["tx_id"] → "0xabc123..." ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **chain** `string` *required* Chain to transfer on: `ethereum`, `tron` or `bitcoin`. **Example:** `"tron"` **currency** `string` *required* Currency ID to transfer: `usdt`, `usdc`, or the native coin ID (`ethereum`, `tron`, `bitcoin`). It must be supported on `chain`. **Example:** `"usdt"` **receiver** `string` *required* Destination address. **Example:** `"TABCef..."` **amount** `number | string` *required* Amount in human-readable units (for example `0.01` ETH or `50` USDT). **Example:** `50` **merchant\_reference** `string` *required* The **wallet reference** of the wallet to send from (the `reference` you passed to [Create Wallet](/api-reference/endpoint/subwallets/create-wallet)). **Example:** `"acme-user-8821"` **address\_reference** `string` *required* The `address_reference` of the address slot to send from. Together with `merchant_reference` and `chain`, it identifies the source address. **Example:** `"acme-user-8821-slot-2"` **payment\_reference** `string` *required* Your own reference for this transfer. It is returned in the transfer webhooks and in [List Wallet Transactions](/api-reference/endpoint/subwallets/list-transactions). **Example:** `"payout-9922"` **sender** `string` Optional. The source address, used only as a cross-check. It must match the slot's `address` or `gasless_address`. For a gasless token transfer, omit it or pass the `gasless_address`; passing the slot's `address` is rejected. We recommend omitting `sender`: the source address is already determined by `merchant_reference`, `address_reference` and `chain`. **Example:** `"TLq9..."` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **tx\_id** `string` On-chain transaction hash. Present for coin transfers and on-chain token transfers. A gasless transfer that settled immediately may also include it. **Example:** `"0xabc123def456..."` **operation\_id** `string` Gasless operation ID. Present for gasless token transfers. Pass it to [Get Transfer Status](/api-reference/endpoint/subwallets/transfer-status). **Example:** `"f3b0c2d1e4a5..."` **confirmation\_count** `integer` Always `0` at submission. Present for token transfers. **Example:** `0` ## Error responses | Condition | Status | Message | | - | - | - | | Missing or invalid field | 400 | Validation message, e.g. `payment_reference must be a string` | | Invalid chain | 400 | `invalid chain: ` | | Invalid currency | 400 | `invalid currency: ` | | Chain not supported for subwallets | 400 | `Unsupported chain for subwallets` | | Currency not valid on that chain | 400 | `Unsupported currency for chain ` | | No contract for the token on that chain | 400 | `no contract address for on ` | | `sender` doesn't match the address slot | 400 | `Transfer address does not match any known address for this wallet` | | Insufficient balance or gas, gasless address required, or other transfer rejection | 400 | Message describing the problem | | No address for that wallet, slot and chain | 404 | `Wallet address reference not found for this merchant` | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | A `503` doesn't tell you whether the transfer was broadcast. Check [List Wallet Transactions](/api-reference/endpoint/subwallets/list-transactions) before retrying. # Get Transfer Status Source: https://docs.tender.cash/api-reference/endpoint/subwallets/transfer-status POST /v1/api/wallet/transfer/status Check the status of a gasless subwallet transfer by its operation ID > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Use this endpoint to follow a gasless token transfer after [Transfer Funds](/api-reference/endpoint/subwallets/transfer) returned `202` with an `operation_id`. You can only look up operations created by your own merchant account. The HTTP status tells you whether the operation is final: * **`200`**: the operation has settled (succeeded or failed). * **`202`**: the operation is still in progress. Poll again later. You don't have to poll: Tender also sends `sub_user_wallet_transfer_confirmed` or `sub_user_wallet_transfer_failed` when the operation settles. If you poll, keep the interval at 10 seconds or more; this endpoint counts against the subwallet rate limit. ## Status values `status` is returned as-is from the gasless network and depends on the chain: | Chain | In progress (`202`) | Succeeded (`200`) | Failed (`200`) | | - | - | - | - | | `EVM` | `pending` | `succeeded` | `failed` | | `TRON` | `waiting`, `inprogress`, `confirming` | `succeed` | `failed` | Treat any value other than `succeeded`, `succeed` or `failed` as in progress. ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/transfer/status`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ network: 'TRON', operation_id: 'f3b0c2d1e4a5...', }), }); const { data } = await res.json(); if (res.status === 202) { // still in progress — data.status e.g. "inprogress" } else if (data.status === 'failed') { // data.message explains why } else { // settled — data.transaction_hash is the on-chain hash } ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet/transfer/status", headers=make_headers(), json={"network": "TRON", "operation_id": "f3b0c2d1e4a5..."}, ) data = res.json()["data"] final = res.status_code == 200 ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **network** `string` *required* Network of the transfer: `EVM` for `ethereum` transfers or `TRON` for `tron` transfers. The aliases `ethereum`, `eth` and `trx` are also accepted, case-insensitively. Bitcoin transfers are never gasless. **Example:** `"TRON"` **operation\_id** `string` *required* The `operation_id` returned by [Transfer Funds](/api-reference/endpoint/subwallets/transfer). **Example:** `"f3b0c2d1e4a5..."` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"transfer status retrieved"` **data** `object` *required* **data properties** **operation\_id** `string` **Example:** `"f3b0c2d1e4a5..."` **status** `string` Operation status. See [Status values](#status-values). **Example:** `"succeed"` **transaction\_hash** `string` On-chain transaction hash once known; empty string before that. **Example:** `"7c1e9a..."` **message** `string` Extra detail, mainly the failure reason. Often an empty string. **Example:** `""` **confirmation\_count** `integer` On-chain confirmations recorded so far. `0` until the transaction is confirmed. **Example:** `1` **provider** `string` Identifier of the gasless service that ran the transfer, returned as-is. **Example:** `"gasfree"` ## Error responses | Condition | Status | Message | | - | - | - | | Missing or invalid field | 400 | Validation message, e.g. `operation_id must be a string` | | Unknown operation, or one created by another merchant | 404 | `Transfer operation not found` | | Network not supported for status lookups | 400 | Message describing the problem | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | # Validate Address Source: https://docs.tender.cash/api-reference/endpoint/subwallets/validate-address POST /v1/api/wallet/validate Check whether a string is a valid address on a given network > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Checks the format of an address for one network. Use it to validate a `receiver` before you call [Transfer Funds](/api-reference/endpoint/subwallets/transfer). ## Example ```javascript Node.js theme={null} const res = await fetch(`${BASE}/wallet/validate`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ address: 'TABCef...', networks: 'TRON', }), }); const { data } = await res.json(); // data.valid → true ``` ```python Python theme={null} res = requests.post( f"{BASE}/wallet/validate", headers=make_headers(), json={"address": "TABCef...", "networks": "TRON"}, ) valid = res.json()["data"]["valid"] ``` ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID **Example:** `"your-access-id-here"` **Content-Type** `string` *required* **Example:** `"application/json"` ## Body **address** `string` *required* The address to validate. **Example:** `"TABCef..."` **networks** `string | string[]` *required* The network to validate against: `EVM`, `TRON` or `BTC` (the aliases `ethereum`, `eth`, `trx` and `bitcoin` are also accepted). Send a single value. An array is accepted for compatibility, but only a one-element array is validated reliably. Send an empty string to let Tender infer the network from the address format. **Example:** `"TRON"` ## Response **status** `string` *required* **Example:** `"success"` **message** `string` *required* **Example:** `"success"` **data** `object` *required* **data properties** **valid** `boolean` `true` if the address is valid on the network. **Example:** `true` ## Error responses | Condition | Status | Message | | - | - | - | | `address` or `networks` missing | 400 | Validation message, e.g. `networks should not be null or undefined` | | Wallet service unavailable | 503 | `Wallet service is temporarily unavailable. Please try again.` | # Get Payment Internal Transaction Source: https://docs.tender.cash/api-reference/endpoint/transactions/get-internal GET /v1/api/transactions/{id}/internal/{internalId} One on-chain transaction Tender recorded to settle one of your payments > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Returns one internal transaction of a payment, for example to check whether its `txHash` has been broadcast. Get the IDs from [List Payment Internal Transactions](/api-reference/endpoint/transactions/list-internal). An ID is only found under the payment it belongs to. Transaction endpoints use the general limit of 120 requests per minute per merchant. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The payment ID: the `txId` returned by [Initiate Payment](/api-reference/endpoint/initiate-payment) **Example:** `"6a12f0c4e4b0a1b2c3d4e5f1"` **internalId** `string` *required* Internal transaction ID: `id` from [List Payment Internal Transactions](/api-reference/endpoint/transactions/list-internal) **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `object` *required* The internal transaction **data properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Internal transaction not found` | The payment isn't yours, the ID is malformed, or `internalId` isn't one of this payment's internal transactions | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/transactions/6a12f0c4e4b0a1b2c3d4e5f1/internal/6a12f0c4e4b0a1b2c3d4e6a7" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "6a12f0c4e4b0a1b2c3d4e5f1", "parentType": "transaction", "role": "bridge_deposit", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } } ``` ```json 404 theme={null} { "status": "error", "message": "Internal transaction not found", "data": {} } ``` # List Payment Internal Transactions Source: https://docs.tender.cash/api-reference/endpoint/transactions/list-internal GET /v1/api/transactions/{id}/internal The on-chain transactions Tender recorded to settle one of your payments > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). The ledger transactions behind one of your payments. A payment can include a bridge hop that moves the funds to another chain, and a conversion that settles them. Each of those writes its own on-chain transaction, and those transactions don't appear in your payment list. Results are oldest first. A payment with no internal transactions returns `200` with an empty `data` array. Transaction endpoints use the general limit of 120 requests per minute per merchant. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Path parameters **id** `string` *required* The payment ID: the `txId` returned by [Initiate Payment](/api-reference/endpoint/initiate-payment) **Example:** `"6a12f0c4e4b0a1b2c3d4e5f1"` ## Response **status** `string` *required* `success` on success **Example:** `"success"` **message** `string` *required* Human-readable result message **Example:** `"success"` **data** `array` *required* Internal transactions, oldest first **item properties** **id** `string` Internal transaction ID **Example:** `"6a12f0c4e4b0a1b2c3d4e6a7"` **parentId** `string` ID of the payment, conversion or payout in the path **parentType** `string` `transaction` (a payment), `conversion` or `payout` **role** `string` What this transaction did for its parent. See [Roles](#roles). **Example:** `"conversion_send"` **type** `string` `send` (left your balance) or `receive` (credited to it) **Example:** `"send"` **status** `string` Ledger status, for example `pending` or `completed` **Example:** `"completed"` **chain** `string` Chain the transaction is on **Example:** `"base"` **asset** `string | null` Coin moved **Example:** `"usdc"` **amount** `string` Amount of `asset` **Example:** `"99.2"` **usdAmount** `string` USD value of `amount` when recorded **Example:** `"99.2"` **txHash** `string | null` On-chain transaction hash; `null` until the transaction is broadcast **explorerUrl** `string | null` Block explorer link for `txHash`, when the chain has an explorer configured **createdAt** `string | null` ISO 8601 timestamp **Example:** `"2026-10-15T09:21:03.114Z"` Additional fields may be added later; ignore fields you don't recognise. ## Roles | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move a payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion or `internal` fiat payout | | `conversion_settlement` | The transaction that credits the result of a conversion | ## Errors | HTTP | Message | Cause | | - | - | - | | 404 | `Transaction not found` | No payment with this ID belongs to your merchant account, or the ID is malformed | ```bash cURL theme={null} curl "https://sandbox-api.tender.cash/v1/api/transactions/6a12f0c4e4b0a1b2c3d4e5f1/internal" \ -H "x-access-id: your-access-id-here" \ -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \ -H "x-timestamp: 2026-10-15T09:45:53.000Z" \ -H "authorization: " ``` ```json 200 theme={null} { "status": "success", "message": "success", "data": [ { "id": "6a12f0c4e4b0a1b2c3d4e6a7", "parentId": "6a12f0c4e4b0a1b2c3d4e5f1", "parentType": "transaction", "role": "bridge_deposit", "type": "send", "status": "completed", "chain": "base", "asset": "usdc", "amount": "99.2", "usdAmount": "99.2", "txHash": "0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "explorerUrl": "https://basescan.org/tx/0x9f2c4be1a7d03e55c81b6fa2d4e7091bc3a5f86d2e0471b9ac5d3f6e8b1027c4", "createdAt": "2026-10-15T09:21:03.114Z" } ] } ``` ```json 200 Empty theme={null} { "status": "success", "message": "success", "data": [] } ``` ```json 404 theme={null} { "status": "error", "message": "Transaction not found", "data": {} } ``` # Validate Payment Source: https://docs.tender.cash/api-reference/endpoint/validate-payment POST /v1/api/payment/validate/{id} Check a payment on-chain and get its current status > [!NOTE] > > **Auth method: Basic (Access ID)** — this endpoint only requires your > `x-access-id` header. No request signing is needed. See > [Authentication](/api-reference/authentication#basic-access-id-authentication). Checks the payment's deposit address on-chain, updates the payment if funds have arrived, and returns its current state. You can only validate payments that belong to your merchant account. `GET /v1/api/payment/validate/{id}` is an alias of this endpoint and behaves identically. Payment endpoints share a limit of 120 requests per minute. See [Rate limits](/api-reference/rate-limits). > [!WARNING] > > A successful check returns HTTP **202**. Every failure returns HTTP **406** with `data: {}`, > **including** `Payment Not made yet`, which simply means no funds have arrived yet. When you > poll, treat a 406 as "keep polling", not as a failed payment. ## Path Parameters **id** `string` *required* The `txId` returned when the payment was initiated **Example:** `"66aec7de809b7f45c42a49f9"` ## Headers **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` ## Response **status** `string` *required* Status of the API request **Example:** `"success"` **message** `string` *required* Result of the check. See [Messages](#messages). **Example:** `"payment received"` **data** `object` *required* The payment. Additional fields may be present; ignore fields you don't recognise. **data properties** **txId** `string` The payment's transaction ID (same as `_id`) **Example:** `"66aec7de809b7f45c42a49f9"` **\_id** `string` The payment's transaction ID **Example:** `"66aec7de809b7f45c42a49f9"` **status** `string` Payment status after the check: `completed`, `overpayment` or `partial-payment`. A payment with no funds received returns 406 `Payment Not made yet` instead, whatever its stored status. See [Payment status values](/guides/payments#payment-status-values). **Example:** `"completed"` **type** `string` Transaction type. Always `receive` for payments. **Example:** `"receive"` **chain** `string` Chain identifier **Example:** `"tron"` **chainId** `object` The chain **chainId properties** **\_id** `string` Internal ID of the chain record **Example:** `"64f782bb4278fc475eb30e29"` **id** `string` Chain identifier **Example:** `"tron"` **name** `string` Chain display name **Example:** `"Tron"` **icon** `string` Chain icon URL **Example:** `"https://tender-store.s3.amazonaws.com/icons/tron.png"` **coin** `string` Native coin of the chain **Example:** `"TRX"` **chainType** `string` Chain family **Example:** `"tron"` **explorer** `string` Block explorer URL **Example:** `"https://tronscan.org"` **currency** `object` The cryptocurrency **currency properties** **\_id** `string` Internal ID of the currency record **Example:** `"64fc4cda43812c15552311d7"` **id** `string` Currency identifier **Example:** `"usdt"` **name** `string` Currency name **Example:** `"Tether"` **symbol** `string` Currency symbol **Example:** `"USDT"` **icon** `string` Currency icon URL **Example:** `"https://tender-store.s3.amazonaws.com/icons/usdt.png"` **isContract** `boolean` Whether the currency is a token contract **Example:** `true` **walletAddress** `string` The deposit address the customer pays to **Example:** `"TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT"` **contractAddress** `string` Token contract address, or an empty string for a native coin **Example:** `"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"` **amount** `string` The all-in cryptocurrency amount requested **Example:** `"10.15"` **coinAmount** `string` Same as `amount` **Example:** `"10.15"` **usdAmount** `string` USD value of `amount` **Example:** `"10.15"` **amountReceived** `string` Cryptocurrency amount received so far **Example:** `"10.15"` **amountReceivedUSD** `string` USD value of `amountReceived` **Example:** `"10.15"` **balanceRequired** `string` Cryptocurrency amount still owed. `"0"` once the payment is complete. **Example:** `"0"` **balanceRequiredUSD** `string` `balanceRequired` in USD **Example:** `"0"` **fee** `string` Fees charged on this payment, in the cryptocurrency **Example:** `"0.1"` **feeUSD** `string` `fee` in USD **Example:** `"0.1"` **activationFee** `string` Activation fee included in `fee`, if any **Example:** `"0"` **activationFeeUSD** `string` `activationFee` in USD **Example:** `"0"` **feeBreakdown** `object | null` Itemised fees charged to the customer, or `null` for older payments **feeBreakdown properties** **settlement** `string` Fee settlement state **Example:** `"pending"` **charged** `object` Each fee leg as `{ coin, usd }`, where `coin` is a string and `usd` a number: `receive`, `activation`, `autoPayoutSwap`, `autoPayoutWithdrawal` and `gasFreeService`, plus `totalCoin` (string) and `totalUSD` (number). **rate** `string` USD price of one unit of the cryptocurrency at initiation **Example:** `"1"` **isPartialPayment** `boolean` Whether the customer paid less than the requested amount **Example:** `false` **isFulfilled** `boolean` `true` once the payment is fully paid, or once a partial payment has been accepted as final **Example:** `true` **partialPaymentAt** `string | null` When a partial payment was first detected **Example:** `null` **fulfilledAt** `string | null` When a partial payment was accepted as final **Example:** `null` **fulfilledBy** `string | null` Who accepted a partial payment as final: `expiry` (automatically, 24 hours after the partial payment was detected), `merchant` or `admin` **Example:** `null` **agentAmount** `string` The requested amount in the fiat currency the payment was priced in **Example:** `"10.00"` **agentCurrency** `string` The fiat currency the payment was priced in **Example:** `"usd"` **agentRate** `string` Price of one unit of the cryptocurrency in `agentCurrency` at initiation **Example:** `"1"` **agentRateUSD** `string` USD price of one unit of the cryptocurrency at initiation, or the outlet's manual rate **Example:** `"1"` **amountReceivedAgent** `string` Amount received, in `agentCurrency` **Example:** `"10.15"` **balanceRequiredAgent** `string` Amount still owed, in `agentCurrency` **Example:** `"0"` **referenceId** `string` Your `reference`, or an empty string **Example:** `"ORDER-9821"` **agentId** `object` The outlet that received the payment **agentId properties** **\_id** `string` Outlet ID **Example:** `"6538eaaebdec6d1a21978e6a"` **name** `string` Outlet name **Example:** `"Lagos Fashion Store"` **agentId** `string` Outlet reference **Example:** `"vbibtucm9ynq3k2x7p"` **merchantId** `object` Your merchant account **merchantId properties** **\_id** `string` Merchant ID **Example:** `"6538e8f9bdec6d1a21978a64"` **merchantName** `string` Business name **Example:** `"Example Store"` **createdAt** `string` ISO 8601 timestamp when the payment was created **Example:** `"2026-10-15T09:00:00.000Z"` **updatedAt** `string` ISO 8601 timestamp when the payment was last updated **Example:** `"2026-10-15T09:04:12.000Z"` ## Messages **HTTP 202** (`status: "success"`, `data` is the payment): | Message | Meaning | | - | - | | `payment received` | The full amount arrived. `data.status` is `completed`, or `overpayment` if the customer paid well over the amount. | | `partial payment received` | Less than the full amount arrived. `data.status` is `partial-payment`. | | `partial payment fulfilled` | A partial payment has been accepted as final. | | `Transaction already completed` | The payment was already complete. | | `Transaction already Overpaid` | The payment was already marked as overpaid. | | `Transaction payment state already updated` | Another check updated the payment at the same time; `data` is its current state. | **HTTP 406** (`status: "error"`, `data: {}`): | Message | Meaning | | - | - | | `Payment Not made yet` | No funds have arrived yet. Keep polling. This is also returned for an unpaid payment that has been cancelled. | | `Transaction state changed; retry validation` | The payment changed during the check. Retry. | | `Invalid tx id` | The ID is unknown or doesn't belong to your merchant account. | | `Invalid Chain` / `Invalid Currency` / `Invalid Agent` / `Invalid Merchant` | The payment's chain, currency, outlet or merchant can no longer be found. | | `Exchange Price Error` | No price is available for the currency right now. Retry later. | ## Example request ```bash theme={null} curl -X POST https://sandbox-api.tender.cash/v1/api/payment/validate/66aec7de809b7f45c42a49f9 \ -H "x-access-id: your-access-id-here" ``` ## Example response ```json theme={null} { "status": "success", "message": "payment received", "data": { "_id": "66aec7de809b7f45c42a49f9", "txId": "66aec7de809b7f45c42a49f9", "status": "completed", "type": "receive", "chain": "tron", "chainId": { "_id": "64f782bb4278fc475eb30e29", "id": "tron", "name": "Tron", "icon": "https://tender-store.s3.amazonaws.com/icons/tron.png", "coin": "TRX", "chainType": "tron", "explorer": "https://tronscan.org" }, "currency": { "_id": "64fc4cda43812c15552311d7", "id": "usdt", "name": "Tether", "symbol": "USDT", "icon": "https://tender-store.s3.amazonaws.com/icons/usdt.png", "isContract": true }, "walletAddress": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", "contractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "amount": "10.15", "coinAmount": "10.15", "usdAmount": "10.15", "amountReceived": "10.15", "amountReceivedUSD": "10.15", "balanceRequired": "0", "balanceRequiredUSD": "0", "fee": "0.1", "feeUSD": "0.1", "activationFee": "0", "activationFeeUSD": "0", "feeBreakdown": { "settlement": "pending", "charged": { "receive": { "coin": "0.1", "usd": 0.1 }, "activation": { "coin": "0", "usd": 0 }, "autoPayoutSwap": { "coin": "0", "usd": 0 }, "autoPayoutWithdrawal": { "coin": "0", "usd": 0 }, "gasFreeService": { "coin": "0.05", "usd": 0.05 }, "totalCoin": "0.1", "totalUSD": 0.1 } }, "rate": "1", "isPartialPayment": false, "isFulfilled": true, "partialPaymentAt": null, "fulfilledAt": null, "fulfilledBy": null, "agentAmount": "10.00", "agentCurrency": "usd", "agentRate": "1", "agentRateUSD": "1", "amountReceivedAgent": "10.15", "balanceRequiredAgent": "0", "referenceId": "ORDER-9821", "agentId": { "_id": "6538eaaebdec6d1a21978e6a", "name": "Lagos Fashion Store", "agentId": "vbibtucm9ynq3k2x7p" }, "merchantId": { "_id": "6538e8f9bdec6d1a21978a64", "merchantName": "Example Store" }, "createdAt": "2026-10-15T09:00:00.000Z", "updatedAt": "2026-10-15T09:04:12.000Z" } } ``` A payment that hasn't been paid yet returns: ```json theme={null} { "status": "error", "message": "Payment Not made yet", "data": {} } ``` # Get Webhook Logs Source: https://docs.tender.cash/api-reference/endpoint/webhook-logs GET /v1/api/webhook/logs Retrieve paginated webhook delivery logs > [!NOTE] > > **Auth method: Signed (HMAC)** — this endpoint requires the full set of signed > headers: `x-access-id`, `x-request-id`, `x-timestamp`, and an HMAC-SHA256 > `authorization` signature. See [Authentication](/api-reference/authentication#signed-hmac-authentication). Rate limit: 120 requests per minute for all webhook endpoints. See [Rate limits](/api-reference/rate-limits). ## Headers **authorization** `string` *required* Base64-encoded HMAC-SHA256 signature of `{"timeStamp":"","requestId":"","accessId":""}`, signed with your access secret. The request body is not part of the signature. **Example:** `"pV3n0o8kq0lK2mJ8bZ1yq3Yt6hR0cXK9u4mVJ2l9sE0="` **x-timestamp** `string` *required* ISO 8601 timestamp; must be within 5 minutes of server time **Example:** `"2026-10-15T09:45:53.000Z"` **x-request-id** `string` *required* A unique UUID v4 per request **Example:** `"550e8400-e29b-41d4-a716-446655440000"` **x-access-id** `string` *required* Your API access ID provided by Tender **Example:** `"your-access-id-here"` Every delivery attempt is logged, whether it succeeded or failed. Tender doesn't retry failed deliveries, so use these logs to find events your endpoint missed. See [Webhooks](/get-started/webhooks#no-automatic-retries). ## Query Parameters **page** `integer` (default: 1) Page number, starting at 1 **Example:** `1` **limit** `integer` (default: 10) Number of log entries per page **Example:** `10` Logs are not sorted by date. To find the most recent deliveries, request the last page (`totalPages`). ## Response Returns HTTP **`202`**. **status** `string` *required* Always `"success"` **message** `string` *required* **Example:** `"Webhook logs fetched"` **data** `object` *required* **data properties** **webhookLogs** `object[]` Log entries for this page. Additional fields may be present; ignore fields you don't recognise. **log entry** **\_id** `string` ID of the log entry **Example:** `"691069ff947a702cfad9e862"` **event** `string` Event name **Example:** `"transaction_completed"` **eventId** `string` The `eventId` sent in this delivery **Example:** `"b3c1f7a2-5d4e-4f8a-9c2b-6e1d0a7f3b94"` **webhookId** `string` ID of the webhook the event was sent to **Example:** `"691069ff947a702cfad9e861"` **merchantId** `string` Your merchant ID **Example:** `"6538e8f9bdec6d1a21978a64"` **requestBody** `object` The full body that was sent: `{ eventId, event, data }` **responseBody** `object | string | null` The body your endpoint returned, if any. If it was a JSON object with a `data` field, only `data` is stored. **status** `string` `success` (your endpoint returned `2xx`) or `failed` **Example:** `"success"` **error** `string` For failed deliveries, the reason: the `message` field of your endpoint's error response if it had one, otherwise an error such as `"timeout of 15000ms exceeded"` or `"Request failed with status code 500"`. For successful deliveries, `"success"`. **Example:** `"success"` **createdAt** `string` ISO 8601 timestamp of the delivery attempt **Example:** `"2026-10-15T09:04:13.120Z"` **total** `integer` Total number of log entries **Example:** `42` **totalPages** `integer` Total number of pages for the given `limit` **Example:** `5` ```json 202 theme={null} { "status": "success", "message": "Webhook logs fetched", "data": { "webhookLogs": [ { "_id": "691069ff947a702cfad9e862", "merchantId": "6538e8f9bdec6d1a21978a64", "webhookId": "691069ff947a702cfad9e861", "eventId": "6f2e9d14-8c3b-4a57-b0e1-9d4c2a7f5e31", "event": "transaction_cancelled", "requestBody": { "eventId": "6f2e9d14-8c3b-4a57-b0e1-9d4c2a7f5e31", "event": "transaction_cancelled", "data": { "txId": "66aec7de809b7f45c42a49f9", "status": "cancelled", "referenceId": "order-123", "amount": "10.15", "usdAmount": "10.15", "chain": "ethereum", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1", "cancelledAt": "2026-10-15T09:00:00.000Z" } }, "responseBody": { "received": true }, "status": "success", "error": "success", "createdAt": "2026-10-15T09:00:01.204Z", "updatedAt": "2026-10-15T09:00:01.204Z" } ], "total": 42, "totalPages": 5 } } ``` # Errors Source: https://docs.tender.cash/api-reference/errors HTTP status codes, the error envelope and validation rules used by the Tender API. Tender uses conventional HTTP status codes. Codes in the `2xx` range mean the request succeeded. Codes in the `4xx` range mean the request failed because of something in the request (a missing field, bad credentials, a resource that doesn't exist). Codes in the `5xx` range mean something went wrong on Tender's side or with an upstream provider. ## Response envelope Every response, success or error, uses the same envelope. **Success:** ```json theme={null} { "status": "success", "message": "success", "data": { } } ``` `data` is an object, an array or `null`, depending on the endpoint. ## Error envelope ```json theme={null} { "status": "error", "message": "Human-readable description of the failure", "data": null } ``` * `status` is always `"error"`. * `message` describes the failure. For authentication errors it is a code such as `NOT_ALLOWED` or `INVALID_REQUEST_SIGNATURE` (see [Authentication](/api-reference/authentication#authentication-errors)). * `data` is usually `{}` or `null`. Some endpoints put extra context in it, for example `{"txId": "…", "status": "completed"}` when a payment can no longer be cancelled. Always branch on the HTTP status code first, then on `message` if you need detail. Don't parse `message` text for control flow except where an endpoint page documents a specific message. ## HTTP status codes | Code | Name | When Tender returns it | | - | - | - | | 200 | OK | The request succeeded. | | 202 | Accepted | Returned instead of `200` by some read endpoints: `GET /v1/api/webhook`, `GET /v1/api/webhook/logs`, `GET /v1/api/webhook/events`, payment validation, and subwallet transfer and transfer-status endpoints while a transfer is not yet final. Treat any `2xx` as success. | | 400 | Bad Request | Validation failed, a required field or header is missing, an ID is malformed (`"Invalid resource ID format"`), or the action isn't allowed in the resource's current state. | | 401 | Unauthorized | Missing headers, an unknown access ID, an expired timestamp or an invalid signature. See [Authentication](/api-reference/authentication#authentication-errors). | | 403 | Forbidden | Your merchant account is suspended. | | 404 | Not Found | The resource doesn't exist or isn't visible to your account, or the route doesn't exist. | | 406 | Not Acceptable | The requested record isn't available yet: for example, no webhook is registered (`GET /v1/api/webhook`), or a payment couldn't be validated. | | 409 | Conflict | A duplicate record (for example, a subwallet or address reference that already exists), or an [idempotency](/api-reference/idempotency) conflict. | | 429 | Too Many Requests | You exceeded a rate limit. See [Rate limits](/api-reference/rate-limits). | | 500 | Internal Server Error | An unexpected error. The message is generic (`"Something went wrong in the server"`). Retry with backoff; contact support if it persists. | | 503 | Service Unavailable | A dependency needed for this request is temporarily unavailable (for example, the subwallet service, or no payout provider for the selected asset). Retry later. | The API does not return `422`. Validation errors are always `400`. ## Validation errors Request bodies, query strings and path parameters are validated before the request is processed. * A validation failure returns **HTTP 400**. * Only the **first** failing rule is reported, as a plain string in `message`: ```json theme={null} { "status": "error", "message": "currency must be a string", "data": null } ``` * **Unknown body fields are silently ignored.** They are removed before processing and do not cause an error, so a misspelt optional field is simply dropped. Check field names against the endpoint page. * Values are converted to the documented type where possible (for example, `"10"` to `10` for a numeric field). ### String input rules All string values in bodies, query strings and path parameters are cleaned before validation: * leading and trailing whitespace is trimmed; * HTML tags, `javascript:` and inline event-handler patterns such as `onload=` are removed; * object keys that start with `$` or contain `.` are dropped. The agent, conversion, payout, onramp and subwallet endpoints apply stricter rules to most free-text fields: they may not contain the characters `$ \ ( ) < >`, and are limited to 500 characters unless the endpoint page states another limit. A field that breaks these rules fails validation with a 400. ## Working with errors When you receive a `400`: * Check that the body matches the documented schema (field names, types and required fields). * Check path parameters such as `{id}` or `{reference}`; a malformed ID returns `"Invalid resource ID format"`. * Read `message`: it names the first field that failed. When you receive a `401` or `403`, see [Authentication](/api-reference/authentication#authentication-errors). When you receive a `409` on a payout, see [Idempotency](/api-reference/idempotency#errors). When you receive a `429`, wait until the window resets (the `RateLimit-Reset` header gives the seconds remaining) before retrying. For `500` and `503`, retry with exponential backoff. For payouts, retry with the **same** `Idempotency-Key` so the payout can't be created twice. If an error persists with a payload you believe is correct, capture the full response body and contact **[support@tender.cash](mailto:support@tender.cash)**. # Idempotency Source: https://docs.tender.cash/api-reference/idempotency Use the Idempotency-Key header to retry payout requests safely. Payout requests move funds. If a request times out or your connection drops, you can't tell whether the payout was created. An idempotency key lets you retry safely: Tender processes the request at most once and replays the original response to any retry. ## Where it is required The `Idempotency-Key` header is **required** on: | Endpoint | | | - | - | | `POST /v1/api/payout/fiat` | [Fiat payout](/api-reference/endpoint/payouts/fiat) | | `POST /v1/api/payout/crypto` | [Crypto payout](/api-reference/endpoint/payouts/crypto) | A request without the header is rejected with `400` before anything is processed. Other endpoints ignore the header. ## Choosing a key * Generate a new, unique key for each payout you intend to make, for example a UUID v4 or your own payout ID. * Keys can be up to **200 characters**. Leading and trailing whitespace is ignored. * Store the key with your payout record **before** you send the request, so a retry after a crash reuses it. * Reuse the key only to retry the **same** payout with the **same** request body. ```http theme={null} POST /v1/api/payout/fiat Idempotency-Key: 4f9c2a1e-7b3d-4c8e-9a51-2f6d0e8b7c13 x-access-id: x-request-id: x-timestamp: authorization: Content-Type: application/json ``` The `Idempotency-Key` header is separate from `x-request-id`. Each retry still needs a fresh `x-request-id`, `x-timestamp` and signature (see [Authentication](/api-reference/authentication)); only the idempotency key and the body stay the same. ## How it behaves Keys are scoped to your merchant account **and** the endpoint, so the same key used on `/payout/fiat` and `/payout/crypto` refers to two different requests. | Situation | Result | | - | - | | First request with a new key | Processed normally. | | Retry with the same key and the same body, after the first request **succeeded** | The original response is returned again. No new payout is created. | | Retry with the same key while the first request is **still being processed** | `409` `"A request with this Idempotency-Key is already being processed"`. Wait and retry. | | Same key with a **different** body | `409` `"Idempotency-Key was already used with a different request"`. | | Retry with the same key after the first request **failed** (any error response) | Processed again as a new request. A failed request releases its key. | | Same key more than **24 hours** after it was first used | Treated as a new key. | "Same body" means the same fields with the same values, in the same order. Fields the endpoint doesn't recognise are ignored and don't affect the comparison. The simplest approach is to serialise the body once and resend exactly those bytes. ## Errors | HTTP status | `message` | Meaning | | - | - | - | | `400` | `Idempotency-Key header is required` | The header is missing or empty. | | `400` | `Idempotency-Key header is too long` | The key is longer than 200 characters. | | `409` | `A request with this Idempotency-Key is already being processed` | An earlier request with this key hasn't finished. Retry after a short delay with the same key and body. | | `409` | `Idempotency-Key was already used with a different request` | You reused a key for a different payout. Use a new key. | ## Recommended retry pattern 1. Create the key and save it with your payout record. 2. Send the request. 3. On a network error, timeout, `429`, `500` or `503`, retry with the **same** key and body, using exponential backoff. 4. On `409 … already being processed`, wait a few seconds and retry with the same key. 5. On any other `4xx`, fix the request. If you change the body, use a **new** key. 6. Confirm the final state with the payout `GET` endpoints rather than resending the payout. Payout routes share a small [rate limit](/api-reference/rate-limits), so keep retries spaced out. # Rate limits Source: https://docs.tender.cash/api-reference/rate-limits Per-merchant request limits, rate limit headers and how to handle 429 responses. Tender limits how many requests your merchant account can make in a fixed window. Limits are counted **per merchant account**, so all of your API keys and outlets share the same budget. ## How the window works * Each limit uses a **fixed 60-second window**. The window starts with your first request in that group and resets 60 seconds later; it does not slide. * Every endpoint group has its own counter. Requests to `/v1/api/payment` don't use up your `/v1/api/payout` budget. * Some groups have a second, tighter limit on top of the group limit. A request must pass every limit that applies to it. * Requests that are rejected with `429` still count towards the current window. ## Limits | Scope | Limit per 60 s | Notes | | - | - | - | | Every endpoint group (`agent`, `conversions`, `onramp`, `payment`, `payout`, `system`, `transactions`, `wallet`, `webhook`) | 120 | Separate counter per group. | | `GET /v1/api/wallet/balance/{address}` | 30 | In addition to the 120/min `wallet` limit. | | All `/v1/api/payout` routes | 10 | One shared counter for every payout route, including `GET` requests (list, get one, fee). `POST /v1/api/payout/crypto` counts **twice**, so you can make about 5 crypto payouts per minute. | | All `/v1/api/conversions` routes | 10 | One shared counter for every conversion route, including `GET` requests. | | All `/v1/api/onramp` routes | 10 | One shared counter for every onramp route, including quote and status polling. | > [!TIP] > > Payout, conversion and onramp endpoints have a small budget that reads and writes share. > Don't poll their `GET` endpoints in a tight loop; poll every 10 seconds or more, and back off > once a record reaches a final status. ## Rate limit headers Responses from rate-limited endpoints include these headers: | Header | Description | | - | - | | `RateLimit-Limit` | The number of requests allowed in the window. | | `RateLimit-Remaining` | The number of requests left in the current window. | | `RateLimit-Reset` | The number of seconds until the current window resets. | Where more than one limit applies (for example, on payout routes), the headers describe the **tighter** limit. ## When you exceed a limit The API responds with HTTP `429` and the standard error envelope. The message states the limit you hit: ```json theme={null} { "status": "error", "message": "Too many requests. Max 10 per 60s.", "data": {} } ``` To recover: 1. Stop sending requests to that group. 2. Wait for `RateLimit-Reset` seconds. 3. Retry. For payouts, retry with the **same** `Idempotency-Key`; see [Idempotency](/api-reference/idempotency). If your integration needs higher limits, contact **[support@tender.cash](mailto:support@tender.cash)**. # Getting API Credentials Source: https://docs.tender.cash/get-started/api-credentials Generate and manage your Tender API credentials for authentication ## Overview Tender secures API requests with two authentication methods — Signed (HMAC) for most endpoints, and Basic (Access ID) for Payment and System endpoints (see [Authentication](/api-reference/authentication)). You'll need two credentials: 1. **Access ID** - Your public API identifier, sent with every request 2. **Access Secret** - Your private key for signing requests to Signed (HMAC) endpoints > [!WARNING] > > Never share your Access Secret or commit it to version control. Treat it like a password. *** ## Generating API Credentials API credentials belong to an **outlet** (agent). Payments you initiate with a key are attributed to that key's outlet, and requests made with a key that isn't linked to an outlet are rejected. **1. Log in to Dashboard** Navigate to [sandbox-merchant.tender.cash](https://sandbox-merchant.tender.cash) (sandbox) or [merchant.tender.cash](https://merchant.tender.cash) (live) and log in to your account. **2. Navigate to API Settings** * Click on **Settings** in the sidebar * Select **API Credentials** **3. Generate credentials for an outlet** * Choose the outlet the credentials are for * Click **Generate New Credentials** **4. Save Your Credentials** > [!WARNING] > > **Important**: Copy both values and store them securely straight away. * **Access ID**: Copy this value * **Access Secret**: Copy this value * Store them in a secure location (password manager, environment variables, secrets manager) > [!NOTE] > > Each outlet has **one** active set of credentials. Generating new credentials for an outlet > immediately revokes that outlet's previous credentials, so update every system that uses them > straight away. *** ## Test vs Production Credentials **Test Environment** **Test Credentials** are for: * Development and integration * Testing payment flows * Webhook testing * Uses testnet cryptocurrencies ```text theme={null} Base URL: https://sandbox-api.tender.cash ``` **Production Environment** **Production Credentials** are for: * Live transactions * Real cryptocurrency payments * Production webhooks * Uses mainnet cryptocurrencies ```text theme={null} Base URL: https://secureapi.tender.cash ``` > [!WARNING] > > Only use production credentials after thorough testing in the test environment. *** ## Managing Credentials ### Viewing Active Credentials In the dashboard's **API Credentials** settings you can see the active credentials for each outlet. ### Rotating or Revoking Credentials If credentials are compromised, or you want to rotate them: **1. Generate new credentials for the outlet** This revokes the outlet's old credentials immediately. Requests that still use them fail with `401 INVALID_REQUEST_SIGNATURE`. **2. Update your systems** Deploy the new Access ID and Access Secret to every system that calls the API for that outlet. **3. Monitor for issues** Check that all systems are working with the new credentials. To revoke credentials without replacing them, delete them from the dashboard. *** ## Next Steps > **[Authentication Guide](/api-reference/authentication)** > > Learn how to sign API requests with your credentials > **[Quick Start](/quickstart)** > > Make your first API call *** ## Troubleshooting > **401 Unauthorized Error** > > **Possible causes:** > > * A missing signed header (`NOT_ALLOWED`) > * An incorrect Access ID or Secret, or credentials that were replaced by newer ones (`INVALID_REQUEST_SIGNATURE`) > * An invalid HMAC signature, or an `x-timestamp` more than 5 minutes from server time (`INVALID_REQUEST_SIGNATURE`) > * Using sandbox credentials on the production URL (or vice versa) > > **Solution:** Verify your credentials and signature generation. See [Authentication](/api-reference/authentication#authentication-errors). > **403 Forbidden Error** > > Your merchant account is suspended. Contact **[support@tender.cash](mailto:support@tender.cash)**. > **Credentials not working** > > **Check:** > > * Are you using the correct environment (sandbox vs production)? > * Have new credentials been generated for this outlet since you copied these? > * Is the timestamp an ISO 8601 string within 5 minutes of the current time? > * Is the signature generated over the exact header values? # OpenAPI Specification Source: https://docs.tender.cash/get-started/api-specification ## Tender API Specification Prefer to use OpenAPI tooling? You can access our full OpenAPI spec directly or import it to Postman, Insomnia, or other API clients: > **[Download OpenAPI JSON](/openapi.json)** > > Get the complete OpenAPI 3.1.0 specification for Tender API ### What You Can Do With the OpenAPI Spec * **Import to Postman or Insomnia** - Generate a complete API collection with all endpoints * **Generate API Clients** - Use tools like OpenAPI Generator to create SDK code * **API Testing** - Automate testing with tools that support OpenAPI specs * **Documentation** - Generate custom documentation using Swagger UI or similar tools * **Validation** - Validate request/response payloads against the schema ### Using with Postman 1. Open Postman 2. Click **Import** in the top left 3. Choose **Link** and paste: `https://docs.tender.cash/openapi.json` 4. Click **Continue** and then **Import** 5. Your Tender API collection is ready to use! ### Using with OpenAPI Generator Generate client libraries in your preferred language: ```bash theme={null} # Install OpenAPI Generator npm install @openapitools/openapi-generator-cli -g # Generate a Node.js client openapi-generator-cli generate \ -i https://docs.tender.cash/openapi.json \ -g javascript \ -o ./tender-client ``` *** ## Changelog ### 15 October 2026 The reference now matches the API's current behaviour. If you built against earlier versions of these docs, check the following. **Corrections** * **Payment initiation:** the token field is `currency`, not `coin`. * **Webhooks:** Tender sends each webhook once, with **no automatic retries**. Webhooks are **not signed**; authenticate them with the custom `headers` you register. The envelope now documents `eventId`, which is new on every delivery. Register webhooks only with `POST /v1/api/webhook`; there is no `callbackUrl` field on other requests. Each merchant has one webhook, and posting again replaces it. See [Webhooks](/get-started/webhooks) and the full [event catalogue](/get-started/webhook-events). * **Payouts:** `POST /v1/api/payout/fiat` and `POST /v1/api/payout/crypto` require an `Idempotency-Key` header. See [Idempotency](/api-reference/idempotency). * **Validation errors** return HTTP `400` with the first failing message, never `422`. Unknown body fields are ignored. See [Errors](/api-reference/errors). * **Authentication:** `x-timestamp` must be within 5 minutes of server time; only the timestamp, request ID and access ID are signed. The Python signing example is fixed (compact JSON separators and a millisecond UTC timestamp). See [Authentication](/api-reference/authentication). * **Webhook management:** `GET /v1/api/webhook` and `GET /v1/api/webhook/logs` return `202`; `GET /v1/api/webhook` returns `406` when no webhook is registered. Webhook log fields are corrected. * **Rate limits** are now documented. See [Rate limits](/api-reference/rate-limits). **Newly documented endpoints** * `POST /v1/api/payment/cancel/{id}`: cancel a pending payment. * `POST /v1/api/payment/quote/{id}`: quote a payment before initiating it. * `GET /v1/api/agent`: list your agents. * `GET /v1/api/conversions/stats`: conversion statistics. * `POST /v1/api/wallet/transfer/status`: status of a subwallet transfer. * `GET /v1/api/wallet/{walletReference}/transactions`: transactions for a subwallet. * [`GET /v1/api/webhook/events`](/api-reference/endpoint/list-webhook-events): list the webhook event names you can subscribe to. # Create Account Source: https://docs.tender.cash/get-started/create-account Sign up for a Tender account to start accepting cryptocurrency payments ## Getting Started with Tender To start accepting cryptocurrency payments with Tender, you'll need to create a merchant account. Follow these steps to get started: *** ## Step 1: Sign Up Visit the Tender website and click on the "Sign Up" or "Get Started" button. **1. Visit Tender** Go to [https://tender.cash](https://tender.cash) and click "Sign Up" **2. Provide Business Information** Fill in your business details: * Business name * Business email * Country * Business type **3. Create Your Account** Set up your account credentials: * Create a secure password * Verify your email address *** ## Step 2: Complete KYC Verification To comply with regulatory requirements, you'll need to complete Know Your Customer (KYC) verification: > **Individual Account** > > **Required Documents:** > > * Government-issued ID (passport, driver's license, or national ID) > * Proof of address (utility bill or bank statement) > * Selfie for identity verification > **Business Account** > > **Required Documents:** > > * Business registration documents > * Tax identification number > * Director/owner identification > * Proof of business address > * Bank account information > [!NOTE] > > KYC verification typically takes 1-3 business days. You'll receive an email notification once your account is approved. *** ## Step 3: Access Your Dashboard Once your account is verified, you can access the Tender Dashboard: > **Dashboard Features** > > * View transaction history > * Monitor wallet balances > * Manage agents and sub-businesses > * Generate API credentials > * Configure webhooks > * View analytics and reports *** ## Step 4: Configure Your Account Before going live, configure your account settings: **Test Environment** Use the test environment to: * Test API integration * Simulate transactions * Verify webhook delivery * Test payment flows > [!WARNING] > > Test environment uses testnet cryptocurrencies with no real value. **Production Environment** Production environment for: * Real cryptocurrency transactions * Live customer payments * Actual fund settlements > [!WARNING] > > Ensure thorough testing before switching to production. *** ## Next Steps > **[Get API Credentials](/get-started/api-credentials)** > > Generate your API keys to start integration > **[Quick Start Guide](/quickstart)** > > Make your first API call *** ## Need Help? For assistance with account creation, reach out to our support team: * **Email**: [support@tender.cash](mailto:support@tender.cash) * **Documentation**: [docs.tender.cash](https://docs.tender.cash) * **Support Portal**: [support.tender.cash](https://support.tender.cash) # Definitions Source: https://docs.tender.cash/get-started/definitions Below are essential terms you'll encounter while integrating Tender. Understanding these will help ensure smooth implementation, testing, and reconciliation of transactions. ## Authentication & Security #### Access ID A public credential used to identify your account when making API requests. This identifier is passed in the `x-access-id` header with every request. #### Access Secret A private credential used for signing and verifying API requests. You must use your secret to generate HMAC signatures for authentication. Never share this key or commit it to version control. #### Signing Requests Most Tender API endpoints require requests to be signed using HMAC-SHA256 with your Access Secret. The signature covers your access ID, the request ID and the timestamp; it proves the request came from you and limits replay to a 5-minute window. The request body is not signed. Payment and System endpoints use Basic (Access ID) authentication instead and don't require a signature — see [Authentication](/api-reference/authentication) for which method each endpoint group uses. *** ## Environments We have two environments in Tender: | Environment | Details | | - | - | | **Test** | A sandbox environment used during development and testing. It allows developers to simulate payment flows and API interactions using testnet cryptocurrencies. No actual funds are processed. | | **Production** | The live environment where real cryptocurrency transactions occur. It processes actual payments on mainnet blockchains and should only be accessed with secure, verified credentials. | *** ## Payments #### Agent A sub-business or merchant account that can receive cryptocurrency payments independently. Agents have their own wallet addresses and can be managed through the Tender API. #### Payment Initiation The process of creating a new cryptocurrency payment request. Returns a wallet address where the customer can send funds on their chosen blockchain. #### Chain A blockchain network (e.g., Ethereum, Polygon, BSC) where cryptocurrency transactions are processed. Tender supports multiple chains for flexibility. #### Coin/Currency The specific cryptocurrency used for payment (e.g., USDT, USDC, ETH). Each chain may support multiple currencies. #### Fiat Currency Traditional government-issued currency that is not backed by a physical commodity but by the government that issued it (e.g., USD, EUR, NGN, GBP). In Tender's context, fiat currencies are used as the base currency for pricing and conversions. When initiating a payment by fiat currency, you specify the amount in fiat (e.g., \$100 USD), and Tender calculates the equivalent cryptocurrency amount based on current exchange rates. #### Transaction Validation The confirmation of a cryptocurrency payment after it has been broadcast and confirmed on the blockchain. Returns the transaction status and details. *** ## Integration Terms #### Webhook A real-time notification sent to your server when certain events occur (for example, a payment is completed or a subwallet deposit is detected). You register one webhook per merchant account with [`POST /v1/api/webhook`](/api-reference/endpoint/add-webhook), giving a URL, the events to receive and custom headers that Tender sends with every delivery. See [Webhooks](/get-started/webhooks). #### Webhook URL The endpoint on your system where Tender sends webhook events. It is the `url` you register with `POST /v1/api/webhook`; API requests such as payment initiation don't take a callback URL. It must be a public HTTPS URL and respond with a `2xx` status code within 15 seconds. Failed deliveries are not retried. #### Exchange Rate The conversion rate between cryptocurrencies and fiat currencies (e.g., USDT to USD). Tender provides real-time exchange rate data through the API. #### Request ID A unique identifier (UUID v4) for each signed API request, passed in the `x-request-id` header and included in the HMAC signature. Generate a new one for every request. It does not make requests idempotent; payouts use a separate [`Idempotency-Key`](/api-reference/idempotency) header. #### Timestamp The time a signed request is made, as an ISO 8601 string (for example `2026-10-15T09:45:53.000Z`), passed in the `x-timestamp` header and included in the HMAC signature. It must be within 5 minutes of Tender's server time. *** ## Blockchain Terms #### Wallet Address A unique cryptographic address on a blockchain where cryptocurrency can be received. Each agent has wallet addresses for supported chains. #### Confirmation The number of blocks added to the blockchain after a transaction is included. More confirmations mean higher security and finality. #### Gas Fee The transaction fee paid to blockchain validators/miners for processing a transaction. Gas fees vary by network and congestion. #### Testnet A parallel blockchain network used for testing without using real cryptocurrency. Useful for development and integration testing. #### Mainnet The primary blockchain network where real transactions with actual value occur. Used in production environments. # Use Cases Source: https://docs.tender.cash/get-started/use-cases #### Multi-Location Businesses with Cryptocurrency Payments Businesses with multiple locations or franchises can create separate agents for each location, enabling independent cryptocurrency payment tracking and reconciliation per branch. #### E-commerce Platforms Accepting Crypto Online stores can integrate Tender to accept cryptocurrency payments across multiple blockchains, giving customers flexibility to pay with their preferred chain and stablecoin. #### Marketplace Platforms with Multiple Merchants Marketplaces can assign each seller their own agent account with unique wallet addresses, simplifying payment routing and settlement for multi-vendor platforms. #### Cross-Border Payment Solutions Businesses serving international customers can leverage cryptocurrency payments to avoid traditional banking fees, currency conversion costs, and lengthy settlement times. #### Payment Links for Social Commerce Generate cryptocurrency payment requests that can be shared via WhatsApp, Telegram, or social media to collect payments remotely without building a full checkout page. #### White-Label Payment Infrastructure Fintech platforms and payment service providers can build on Tender's infrastructure to offer cryptocurrency payment capabilities to their merchants with custom branding. # Webhook Events Source: https://docs.tender.cash/get-started/webhook-events Every webhook event Tender sends, when it fires, and an example payload Every webhook uses the envelope `{"eventId": "...", "event": "...", "data": {...}}`. This page describes `data` for each event. See [Webhooks](/get-started/webhooks) for registration, delivery and security. > [!NOTE] > > Payloads list a stable public subset of fields. Additional fields may be present; ignore fields you don't recognise. > Monetary amounts are strings unless stated otherwise. ## Supported events Subscribe using these exact names (lowercase, case-sensitive). You can also fetch them from [`GET /v1/api/webhook/events`](/api-reference/endpoint/list-webhook-events). | Event | When it fires | | - | - | | [`transaction_requested`](#transaction_requested) | A payment was initiated. | | [`transaction_completed`](#transaction_completed) | A payment was paid in full. | | [`transaction_overpayment`](#transaction_overpayment) | A payment received more than the amount due. | | [`transaction_partially_paid`](#transaction_partially_paid) | A payment received less than the amount due. May repeat. | | [`transaction_partial_fulfilled`](#transaction_partial_fulfilled) | A partially paid payment was accepted as final. | | [`transaction_cancelled`](#transaction_cancelled) | A pending payment was cancelled. | | [`sub_user_wallet_transaction_detected`](#sub_user_wallet_transaction_detected) | A deposit to a subwallet address was detected. Repeats as confirmations rise. | | [`sub_user_wallet_transfer_confirmed`](#sub_user_wallet_transfer_confirmed) | An outgoing subwallet transfer was confirmed. | | [`sub_user_wallet_transfer_failed`](#sub_user_wallet_transfer_failed) | An outgoing gasless subwallet transfer failed. | | [`offramp_order_completed`](#off-ramp-order-events) | A dashboard off-ramp order completed. | | [`offramp_order_cancelled`](#off-ramp-order-events) | A dashboard off-ramp order was cancelled, declined or expired. | | [`offramp_order_failed`](#off-ramp-order-events) | A dashboard off-ramp order failed. | > [!WARNING] > > There are **no webhooks for payouts, conversions or on-ramp orders** created through the API. > Poll [`GET /v1/api/payout/{id}`](/api-reference/endpoint/payouts/get-one), > [`GET /v1/api/conversions/{id}`](/api-reference/endpoint/conversions/get-one) and > [`GET /v1/api/onramp/{reference}`](/api-reference/endpoint/onramp/status) instead. *** ## Payment events ### transaction\_requested Fires when a payment is initiated through [initiate payment](/api-reference/endpoint/initiate-payment) or [initiate payment by currency](/api-reference/endpoint/initiate-payment-currency). `data` matches the initiate response, with IDs as strings. | Field | Description | | - | - | | `txId` | The payment ID. Use it to validate or cancel the payment. | | `type` | Always `receive`. | | `chain` | Chain ID, for example `tron`. | | `chainId`, `currency` | IDs of the chain and currency records. | | `amount`, `coinAmount` | Total the payer must send, in the payment currency, including fees. | | `usdAmount` | `amount` in USD. | | `requestedAmount`, `agentAmount`, `agentCurrency` | The amount you asked for, and its currency. | | `fee`, `feeUSD`, `activationFee`, `activationFeeUSD` | Fees included in `amount`. | | `rate` | USD price of one unit of the payment currency used for this payment. | | `walletAddress` | The wallet's controlling address. | | `gasFreeAddress` | Present on gasless chains. When present, this is the address the payer must pay. | | `contractAddress` | Token contract address, or empty for native coins. | | `balanceRequired`, `balanceRequiredUSD` | Amount still outstanding (the full amount at this point). | | `referenceId` | The reference you sent at initiation. | | `agentId`, `merchantId` | IDs of the outlet and merchant. | ```json theme={null} { "eventId": "0d9c6a52-3f1e-4b7a-8a6e-2c5f9b1d7e40", "event": "transaction_requested", "data": { "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "chain": "tron", "chainId": "6538e1b2bdec6d1a21978a10", "currency": "6538e1c4bdec6d1a21978a22", "amount": "10.15", "coinAmount": "10.15", "usdAmount": "10.15", "requestedAmount": "10", "agentAmount": "10", "agentCurrency": "USD", "fee": "0.15", "feeUSD": "0.15", "activationFee": "0", "activationFeeUSD": "0", "rate": 1, "walletAddress": "TQ5NmAqXwBvnDgC9bYVh7f3S9oYr4kJx2M", "gasFreeAddress": "TGx8kP3m9ZqLhW2vYcR7nB5tD1sF6aJ4uE", "contractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "balanceRequired": "10.15", "balanceRequiredUSD": "10.15", "referenceId": "order-123", "agentId": "6538eaaebdec6d1a21978e6a", "merchantId": "6538e8f9bdec6d1a21978a64" } } ``` ### Payment status events `transaction_completed`, `transaction_overpayment`, `transaction_partially_paid` and `transaction_partial_fulfilled` all carry the same payment object, which is also what [validate payment](/api-reference/endpoint/validate-payment) returns. #### transaction\_completed Fires once, when the amount received reaches the amount due. `status` is `completed`. #### transaction\_overpayment Fires once, when the payer sends more than the amount due. `status` is `overpayment`. Compare `amountReceived` with `amount` to find the excess. #### transaction\_partially\_paid Fires when a payment receives less than the amount due. `status` is `partial-payment`. The payment is re-checked periodically, so this event **may repeat** while the payment stays partially paid, with updated `amountReceived` and `balanceRequired`. If the balance arrives, you'll receive `transaction_completed` or `transaction_overpayment`. #### transaction\_partial\_fulfilled Fires once, when a partially paid payment is accepted as final and no further funds are expected. This happens automatically 24 hours after the payment became partially paid, or earlier if Tender accepts it at your request. `status` stays `partial-payment`; `isFulfilled` is `true`, and `fulfilledAt` and `fulfilledBy` (`expiry`, `merchant` or `admin`) are set. #### Payment object fields | Field | Description | | - | - | | `txId` (also `_id`) | The payment ID. | | `type` | Always `receive`. | | `status` | `completed`, `overpayment` or `partial-payment`. | | `chain` | Chain ID, for example `ethereum`. | | `chainId` | Chain object: `_id`, `id`, `name`, `coin`, `icon`, `explorer`, `chainType`. | | `currency` | Currency object: `_id`, `id`, `name`, `symbol`, `icon`, `isContract`. | | `walletAddress` | The address the payer paid (the gasless address on gasless chains). | | `contractAddress` | Token contract address, or empty for native coins. | | `amount`, `coinAmount`, `usdAmount` | The amount due, in the payment currency and in USD. | | `amountReceived`, `amountReceivedUSD` | What has been received so far. | | `balanceRequired`, `balanceRequiredUSD` | What is still outstanding. `"0"` once paid in full. | | `agentAmount`, `agentCurrency`, `agentRate`, `amountReceivedAgent`, `balanceRequiredAgent` | The same amounts in the outlet's currency. | | `fee`, `feeUSD`, `activationFee`, `activationFeeUSD` | Fees charged on this payment. | | `feeBreakdown` | `{ settlement, charged: { receive, activation, autoPayoutSwap, autoPayoutWithdrawal, gasFreeService, totalCoin, totalUSD } }`. Each charged leg is `{ coin, usd }`. | | `rate` | USD price of one unit of the payment currency. | | `isPartialPayment`, `partialPaymentAt` | Whether the payment was ever partially paid, and when. | | `isFulfilled`, `fulfilledAt`, `fulfilledBy` | Whether the payment is final (fully paid or a partial accepted as final). | | `referenceId` | The reference you sent at initiation. | | `agentId` | Outlet object: `_id`, `name`, `email`, `phoneNumber`, `country`. | | `merchantId` | Merchant object: `_id`, `merchantName`, `email`. | | `createdAt`, `updatedAt` | ISO 8601 timestamps. | ```json theme={null} { "eventId": "b3c1f7a2-5d4e-4f8a-9c2b-6e1d0a7f3b94", "event": "transaction_completed", "data": { "_id": "66aec7de809b7f45c42a49f9", "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "status": "completed", "chain": "ethereum", "chainId": { "_id": "6538e1b2bdec6d1a21978a11", "id": "ethereum", "name": "Ethereum", "coin": "ETH", "icon": "https://assets.tender.cash/chains/ethereum.png", "explorer": "https://etherscan.io", "chainType": "EVM" }, "currency": { "_id": "6538e1c4bdec6d1a21978a23", "id": "usdt", "name": "Tether USD", "symbol": "USDT", "icon": "https://assets.tender.cash/coins/usdt.png", "isContract": true }, "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1", "contractAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "amount": "10.15", "coinAmount": "10.15", "usdAmount": "10.15", "amountReceived": "10.15", "amountReceivedUSD": "10.15", "balanceRequired": "0", "balanceRequiredUSD": "0", "agentAmount": "10", "agentCurrency": "USD", "agentRate": "1", "amountReceivedAgent": "10", "balanceRequiredAgent": "0", "fee": "0.15", "feeUSD": "0.15", "activationFee": "0", "activationFeeUSD": "0", "feeBreakdown": { "settlement": "pending", "charged": { "receive": { "coin": "0.15", "usd": 0.15 }, "activation": { "coin": "0", "usd": 0 }, "autoPayoutSwap": { "coin": "0", "usd": 0 }, "autoPayoutWithdrawal": { "coin": "0", "usd": 0 }, "gasFreeService": { "coin": "0", "usd": 0 }, "totalCoin": "0.15", "totalUSD": 0.15 } }, "rate": "1", "isPartialPayment": false, "partialPaymentAt": null, "isFulfilled": true, "fulfilledAt": null, "fulfilledBy": null, "referenceId": "order-123", "agentId": { "_id": "6538eaaebdec6d1a21978e6a", "name": "Lagos Store", "email": "store@example.com", "phoneNumber": "+2348012345678", "country": "Nigeria" }, "merchantId": { "_id": "6538e8f9bdec6d1a21978a64", "merchantName": "Example Ltd", "email": "owner@example.com" }, "createdAt": "2026-10-15T09:00:00.000Z", "updatedAt": "2026-10-15T09:04:12.000Z" } } ``` A `transaction_partially_paid` payload has the same shape, for example with `"status": "partial-payment"`, `"isPartialPayment": true`, `"amountReceived": "5"` and `"balanceRequired": "5.15"`. ### transaction\_cancelled Fires when a pending payment is cancelled, for example with [cancel payment](/api-reference/endpoint/cancel-payment). Cancelling an already-cancelled payment doesn't send it again. | Field | Description | | - | - | | `txId` | The payment ID. | | `status` | Always `cancelled`. | | `referenceId` | The reference you sent at initiation, or `""`. | | `amount`, `usdAmount` | The amount that was due. | | `chain` | Chain ID. | | `walletAddress` | The wallet's controlling address. | | `cancelledAt` | ISO 8601 timestamp. | ```json theme={null} { "eventId": "6f2e9d14-8c3b-4a57-b0e1-9d4c2a7f5e31", "event": "transaction_cancelled", "data": { "txId": "66aec7de809b7f45c42a49f9", "status": "cancelled", "referenceId": "order-123", "amount": "10.15", "usdAmount": "10.15", "chain": "ethereum", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1", "cancelledAt": "2026-10-15T09:00:00.000Z" } } ``` *** ## Subwallet events ### sub\_user\_wallet\_transaction\_detected Fires when a deposit to one of your subwallet addresses is detected on-chain, and **again each time its confirmation count changes**. Deduplicate on `depositKey`, and credit the deposit only once `confirmed` is `true`. | Field | Description | | - | - | | `depositKey` | Unique ID of the deposit: `::`. | | `transactionId` | Tender's ID for the deposit record. | | `walletReference`, `addressReference` | The references you gave the wallet and address. `addressReference` may be `null`. | | `addressId` | ID of the receiving address record. | | `chainId` | Chain the deposit arrived on. | | `network` | Network family of the address, for example `EVM`. | | `symbol` | Asset symbol, for example `USDT`. | | `address` | The receiving address. | | `txHash`, `index` | Transaction hash, and the transfer's index within it. | | `amount` | Amount in the asset's **smallest unit**, as a string. Divide by 10^`decimals`. | | `decimals` | Decimals for `amount`. | | `confirmed` | `true` once the deposit has `confirmationsRequired` confirmations. | | `blockNumber`, `confirmations`, `confirmationsRequired` | Confirmation progress. | | `occurredAt` | ISO 8601 timestamp. | ```json theme={null} { "eventId": "a51c3e07-92d4-4b6f-8e1a-7c0d5f2b9e68", "event": "sub_user_wallet_transaction_detected", "data": { "depositKey": "ethereum:0xabc123…def:0", "transactionId": "671e0f2a9b3c4d5e6f708192", "walletReference": "user-8821", "addressReference": "slot-2", "addressId": "671e0a119b3c4d5e6f708150", "chainId": "ethereum", "network": "EVM", "symbol": "USDT", "address": "0x5aE1c2b3D4e5F60718293A4b5C6d7E8f90a1B2c3", "txHash": "0xabc123…def", "index": 0, "amount": "1000000", "decimals": 6, "confirmed": false, "blockNumber": 21034567, "confirmations": 1, "confirmationsRequired": 12, "occurredAt": "2026-10-15T09:00:00Z" } } ``` ### sub\_user\_wallet\_transfer\_confirmed Fires once when an outgoing [subwallet transfer](/api-reference/endpoint/subwallets/transfer) is confirmed. | Field | Description | | - | - | | `transactionId` | Tender's ID for the outgoing transfer. | | `transferKey` | The transaction hash for on-chain transfers, or the operation ID for gasless transfers. | | `txHash` | Transaction hash. On gasless transfers it may be empty if the hash isn't known. | | `walletReference`, `addressReference` | References of the sending wallet and address. | | `network` | Network family, for example `EVM`. | | `type` | `coin` (native coin) or `asset` (token). | | `sender`, `receiver` | Sending and receiving addresses. | | `amount` | Amount sent, as given in the transfer request. | | `asset` | Asset symbol, or `null`. | | `paymentReference` | The `payment_reference` you sent with the transfer. | | `blockNumber` | On-chain transfers only. | | `operationId`, `status` | Gasless transfers only. `status` is the final operation status, for example `succeeded`. | On-chain transfer: ```json theme={null} { "eventId": "3e7b1c90-4f2a-4d8e-a6b5-1c9f0e2d7a43", "event": "sub_user_wallet_transfer_confirmed", "data": { "transactionId": "671e1b4c9b3c4d5e6f7081a0", "txHash": "0x9f8e7d…c2b1", "transferKey": "0x9f8e7d…c2b1", "walletReference": "user-8821", "addressReference": "slot-2", "network": "EVM", "type": "asset", "sender": "0x5aE1c2b3D4e5F60718293A4b5C6d7E8f90a1B2c3", "receiver": "0x8bC9d0E1f2A3b4C5d6E7f8091a2B3c4D5e6F7a8B", "amount": "10", "asset": "USDT", "paymentReference": "withdrawal-5521", "blockNumber": 21034890 } } ``` Gasless transfer: ```json theme={null} { "eventId": "c8a2f0e5-1b7d-4e39-9a64-5d3b2c1e0f87", "event": "sub_user_wallet_transfer_confirmed", "data": { "transactionId": "671e1b4c9b3c4d5e6f7081a1", "txHash": "a1b2c3…e9f0", "transferKey": "op_01J9Z6Q4V7XK3M2N8P5R", "operationId": "op_01J9Z6Q4V7XK3M2N8P5R", "status": "succeeded", "walletReference": "user-8821", "addressReference": "slot-1", "network": "TRON", "type": "asset", "sender": "TGx8kP3m9ZqLhW2vYcR7nB5tD1sF6aJ4uE", "receiver": "TQ5NmAqXwBvnDgC9bYVh7f3S9oYr4kJx2M", "amount": "25", "asset": "USDT", "paymentReference": "withdrawal-5522" } } ``` ### sub\_user\_wallet\_transfer\_failed Fires once when an outgoing **gasless** subwallet transfer ends in failure. The payload matches the gasless `sub_user_wallet_transfer_confirmed` payload, with `"status": "failed"`. `txHash` is usually empty. ```json theme={null} { "eventId": "e1f4a7b2-6c3d-4e58-8b90-2a1d7c6e5f34", "event": "sub_user_wallet_transfer_failed", "data": { "transactionId": "671e1b4c9b3c4d5e6f7081a2", "txHash": "", "transferKey": "op_01J9Z7B2C4D6E8F0G1H3", "operationId": "op_01J9Z7B2C4D6E8F0G1H3", "status": "failed", "walletReference": "user-8821", "addressReference": "slot-1", "network": "TRON", "type": "asset", "sender": "TGx8kP3m9ZqLhW2vYcR7nB5tD1sF6aJ4uE", "receiver": "TQ5NmAqXwBvnDgC9bYVh7f3S9oYr4kJx2M", "amount": "25", "asset": "USDT", "paymentReference": "withdrawal-5523" } } ``` *** ## Off-ramp order events `offramp_order_completed`, `offramp_order_cancelled` and `offramp_order_failed` are sent for **off-ramp orders created in the Tender merchant dashboard** (selling crypto to a bank account through Tender's off-ramp partner). They aren't sent for API payouts. | Event | When it fires | `status` | | - | - | - | | `offramp_order_completed` | The order completed and the fiat was paid out. | `complete` | | `offramp_order_cancelled` | The order was cancelled, declined or expired. | `cancelled`, `declined` or `expired` | | `offramp_order_failed` | The order failed. | `failed` | | Field | Description | | - | - | | `banxaOrderId` | The off-ramp partner's order ID. | | `status` | The order's new status (see the table above). | | `order` | The order record **as it was before this update**. Its `status`, `txHash` and fee fields can be out of date; use the top-level `status`. | | `order._id`, `order.banxaOrderId`, `order.orderType` | Order identifiers. `orderType` is `SELL`. | | `order.crypto`, `order.blockchain`, `order.cryptoAmount` | The crypto sold. | | `order.fiat`, `order.fiatAmount` | The fiat paid out. | | `order.txHash` | Hash of the crypto transfer, once known. | | `order.processingFee`, `order.networkFee` | Fees, once known. | | `order.withdrawalId` | The related withdrawal record, if any. | | `order.createdAt`, `order.updatedAt` | ISO 8601 timestamps. | ```json theme={null} { "eventId": "9b0d2e4f-7a1c-4f63-8e25-c4b6a8d0f1e2", "event": "offramp_order_completed", "data": { "banxaOrderId": "a7c3e9f1b2d4", "status": "complete", "order": { "_id": "671e2c5d9b3c4d5e6f7081b3", "banxaOrderId": "a7c3e9f1b2d4", "orderType": "SELL", "status": "inProgress", "crypto": "USDT", "blockchain": "TRON", "cryptoAmount": "500", "fiat": "USD", "fiatAmount": "492.50", "txHash": "b4c5d6…a9f0", "processingFee": "5.00", "networkFee": "2.50", "withdrawalId": "671e2c4a9b3c4d5e6f7081b2", "createdAt": "2026-10-15T08:30:00.000Z", "updatedAt": "2026-10-15T08:52:10.000Z" } } } ``` *** ## Reserved event names `GET /v1/api/webhook/events` also returns `transaction_reserved`. It is reserved and **not currently sent**; subscribing to it has no effect. # Webhooks Source: https://docs.tender.cash/get-started/webhooks Receive real-time notifications about payments and subwallet activity in your Tender account Webhooks let Tender notify your server when something happens in your account, such as a payment being completed or a deposit arriving at a subwallet address. Tender sends an HTTPS `POST` with a JSON body to the URL you register. For the list of events and their payloads, see [Webhook Events](/get-started/webhook-events). ## Register your webhook Register your endpoint with [`POST /v1/api/webhook`](/api-reference/endpoint/add-webhook). This is the only way to register a webhook. Payment and other API requests don't accept a callback URL. * **One webhook per merchant account.** Calling `POST /v1/api/webhook` again replaces your existing URL, description, event list and headers. * **Subscribe to each event by name.** You only receive events listed in `eventTypes`. Event names are case-sensitive and lowercase (for example `transaction_completed`). Names are not checked when you subscribe, so a misspelt name is accepted but never delivered. Use [`GET /v1/api/webhook/events`](/api-reference/endpoint/list-webhook-events) to get the exact names. * **Use a public HTTPS URL.** The URL must use `https`, and must not point to `localhost` or a private, loopback or link-local address. To see your current registration, call [`GET /v1/api/webhook`](/api-reference/endpoint/fetch-webhook). To stop all deliveries, call [`DELETE /v1/api/webhook/{id}`](/api-reference/endpoint/delete-webhook). *** ## Webhook structure Every webhook has the same envelope: | Field | Type | Description | | - | - | - | | `eventId` | string (UUID) | A new ID for every delivery. | | `event` | string | The event name, for example `transaction_completed`. | | `data` | object | The event payload. Its shape depends on the event. | Example (`data` shortened): ```json theme={null} { "eventId": "b3c1f7a2-5d4e-4f8a-9c2b-6e1d0a7f3b94", "event": "transaction_completed", "data": { "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "status": "completed", "chain": "ethereum", "amount": "10.15", "usdAmount": "10.15", "amountReceived": "10.15", "balanceRequired": "0", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1", "referenceId": "order-123", "createdAt": "2026-10-15T09:00:00.000Z", "updatedAt": "2026-10-15T09:04:12.000Z" } } ``` The request is sent with `Content-Type: application/json` and `Accept: application/json`, plus any custom headers you registered. Payloads may gain new fields over time; ignore fields you don't recognise. *** ## Responding to webhooks * **Return any `2xx` status** to acknowledge the webhook. Any other status, a redirect, a connection error or no response within **15 seconds** counts as a failed delivery. * **Respond quickly.** Acknowledge first, then process the event asynchronously (for example, by putting it on a queue). * **Redirects are not followed.** Register the final URL, not one that redirects. ### No automatic retries Tender sends each webhook **once**. Failed deliveries are **not** retried. Every delivery attempt, successful or failed, is recorded in your [webhook logs](/api-reference/endpoint/webhook-logs). Use webhooks as a fast signal, and don't rely on them alone: * Check the webhook logs for failed deliveries. * For payments, confirm the state with [validate payment](/api-reference/endpoint/validate-payment) when a webhook is missing or you need certainty. * Reconcile periodically against the API. *** ## Deduplicating events You can receive more than one webhook for the same thing. For example, `transaction_partially_paid` can arrive several times while a payment stays partially paid, and `sub_user_wallet_transaction_detected` arrives again each time a deposit's confirmation count changes. `eventId` is new on every delivery, so it **can't** be used to detect duplicates. Deduplicate on the business identifiers instead: | Events | Deduplication key | | - | - | | `transaction_*` | `event` + `data.txId` + `data.status`. For `transaction_partially_paid`, also `data.amountReceived`. | | `sub_user_wallet_transaction_detected` | `data.depositKey`. Update your record as `confirmations` rises and act once `confirmed` is `true`. | | `sub_user_wallet_transfer_confirmed`, `sub_user_wallet_transfer_failed` | `data.transactionId` (or `data.transferKey`). | | `offramp_order_*` | `data.banxaOrderId` + `data.status`. | Make your handler idempotent: processing the same event twice should have no extra effect. *** ## Securing your endpoint Tender webhooks are **not signed**. There is no signature header to verify. Instead, authenticate deliveries with the **custom headers** you register: Tender sends them, unchanged, with every webhook. ### Setting custom headers Register a secret value in `headers` when you create the webhook: ```javascript theme={null} const response = await fetch('https://secureapi.tender.cash/v1/api/webhook', { method: 'POST', headers: signedHeaders(accessId, accessSecret), // see Authentication body: JSON.stringify({ description: 'Production webhook', url: 'https://your-domain.com/webhooks/tender', eventTypes: [ 'transaction_completed', 'transaction_overpayment', 'transaction_partially_paid', 'transaction_cancelled', ], headers: { 'X-Webhook-Secret': 'a-long-random-value', }, }), }); ``` See [Authentication](/api-reference/authentication#signed-request-example) for `signedHeaders`. A few headers can't be overridden and are dropped if you set them: `Host`, `Content-Length`, `Connection`, `Transfer-Encoding`, `Upgrade`, `Proxy-Authorization`, `X-Forwarded-For`, `X-Forwarded-Host` and `X-Forwarded-Proto`. ### Verifying custom headers On your endpoint, reject any request that doesn't carry the expected value: ```javascript theme={null} const crypto = require('crypto'); const express = require('express'); const app = express(); const WEBHOOK_SECRET = process.env.TENDER_WEBHOOK_SECRET; // same value you registered function safeEqual(a, b) { const x = Buffer.from(String(a ?? '')); const y = Buffer.from(String(b ?? '')); return x.length === y.length && crypto.timingSafeEqual(x, y); } app.post('/webhooks/tender', express.json(), (req, res) => { if (!safeEqual(req.headers['x-webhook-secret'], WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Unauthorized' }); } const { eventId, event, data } = req.body; // Acknowledge first, then process asynchronously and idempotently res.status(200).json({ received: true }); enqueue({ eventId, event, data }); }); app.listen(3000); ``` ### Best practices * Use a long, randomly generated secret value, and compare it in constant time. * Keep the value out of source control, and rotate it by calling `POST /v1/api/webhook` with a new value. * Treat the payload as a notification. Before you ship goods or credit a customer, confirm the payment with [validate payment](/api-reference/endpoint/validate-payment). * Serve the endpoint over HTTPS only. *** ## What doesn't send webhooks There are **no webhooks for payouts, conversions or on-ramp orders** created through the API. Poll their `GET` endpoints to track status, within the [rate limits](/api-reference/rate-limits): * Payouts: [`GET /v1/api/payout/{id}`](/api-reference/endpoint/payouts/get-one) * Conversions: [`GET /v1/api/conversions/{id}`](/api-reference/endpoint/conversions/get-one) * On-ramp: [`GET /v1/api/onramp/{reference}`](/api-reference/endpoint/onramp/status) # Conversions Source: https://docs.tender.cash/guides/conversions Swap crypto to another coin/chain or convert to a fiat payout account A conversion moves funds from one form to another without requiring the customer to make a new payment. There are two types: | Type | What it does | | - | - | | `crypto` | Swaps a coin on one chain to a different coin or chain (e.g. USDT/Ethereum → SOL/Solana) | | `fiat` | Converts crypto and sends it to one of your saved payout accounts (a bank account, or a crypto account) | A `crypto` conversion always settles to your own merchant wallet on the destination chain. Tender picks the conversion route automatically. A conversion must be worth more than \$20 USD. *** ## How it works **1. Discover supported swap chains and coins** Call `GET /conversions/swap/chains` and `GET /conversions/swap/coins` to find what source/destination pairs are available. **2. Check the fee** Call `GET /conversions/fee/{chain}/{coin}/{amount}` to get a fee quote and estimated output before committing. For crypto swaps, pass `toChain` and `toCoin` as query params to get a destination-specific estimate. For fiat, pass `toChain=fiat` and the fiat currency as `toCoin`. **3. Create the conversion** Call `POST /conversions` with the source coin, chain, amount, and destination. The conversion is queued immediately and processed asynchronously. **4. Poll for completion** Call `GET /conversions/{id}` until `status` is `completed` or `failed`. There are no conversion webhooks, so polling is the only way to track progress. *** ## Prerequisites > **[Authentication guide](/api-reference/authentication)** > > These endpoints use Signed (HMAC) authentication — an HMAC-SHA256 signature is required on every request. See the authentication guide for code examples. ```javascript theme={null} const BASE = 'https://sandbox-api.tender.cash/v1/api'; // makeHeaders() → { 'x-access-id', 'x-request-id', 'x-timestamp', 'authorization', ... } ``` *** ## Step 1 — Check the fee Always fetch a fee quote before creating a conversion so you can show the cost to the user. ```javascript Node.js theme={null} // Crypto swap fee quote const feeRes = await fetch( `${BASE}/conversions/fee/ethereum/usdt/100?toChain=solana&toCoin=sol`, { headers: makeHeaders() } ); const { data: fee } = await feeRes.json(); /* { from: { chain: "ethereum", currency: "usdt", symbol: "USDT", amount: 100, amountUSD: 100 }, to: { chain: "solana", currency: "sol", symbol: "SOL", amount: 0.6633, amountUSD: 99.5 }, fee: { fee: 0.5, feeUSD: 0.5 }, rate: { value: "0.006667", perSourceUnit: "0.006700", symbol: "SOL", overridden: false } } */ console.log(`You pay ${fee.from.amount} ${fee.from.symbol}, receive ~${fee.to.amount} ${fee.to.symbol}`); ``` ```python Python theme={null} fee_res = requests.get( f"{BASE}/conversions/fee/ethereum/usdt/100", params={ "toChain": "solana", "toCoin": "sol" }, headers=make_headers(), ) fee = fee_res.json()["data"] ``` *** ## Step 2 — Create a crypto swap ```javascript Node.js theme={null} const convRes = await fetch(`${BASE}/conversions`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ type: 'crypto', chain: 'ethereum', coin: 'usdt', amount: '100', toChain: 'solana', toCoin: 'sol', }), }); const { data } = await convRes.json(); const conversionId = data.conversion.conversionId; console.log('Status:', data.conversion.status); // "pending" ``` ```python Python theme={null} conv_res = requests.post( f"{BASE}/conversions", headers=make_headers(), json={ "type": "crypto", "chain": "ethereum", "coin": "usdt", "amount": "100", "toChain": "solana", "toCoin": "sol", }, ) conversion = conv_res.json()["data"]["conversion"] conversion_id = conversion["conversionId"] ``` *** ## Step 2 (alt) — Create a fiat conversion To convert to fiat, supply a `payoutAccountId` instead of `toChain`/`toCoin`. It must be one of your saved, active payout accounts. If it is a crypto payout account, the converted funds are sent to its address instead. ```javascript Node.js theme={null} const convRes = await fetch(`${BASE}/conversions`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ type: 'fiat', chain: 'ethereum', coin: 'usdt', amount: '100', payoutAccountId: '66aec7de809b7f45c42a49f9', }), }); ``` ```python Python theme={null} conv_res = requests.post( f"{BASE}/conversions", headers=make_headers(), json={ "type": "fiat", "chain": "ethereum", "coin": "usdt", "amount": "100", "payoutAccountId": "66aec7de809b7f45c42a49f9", }, ) ``` *** ## Step 3 — Poll for completion ```javascript Node.js theme={null} // Conversion endpoints share 10 requests/minute, so poll every 10s or slower. async function pollConversion(id, intervalMs = 15000) { while (true) { const res = await fetch(`${BASE}/conversions/${id}`, { headers: makeHeaders() }); const { data } = await res.json(); console.log('Status:', data.status, 'Stages:', data.processingStages); if (data.status === 'completed') return data; if (data.status === 'failed') throw new Error(`Conversion failed: ${data.failureReason}`); await new Promise(r => setTimeout(r, intervalMs)); } } ``` ```python Python theme={null} import time # Conversion endpoints share 10 requests/minute, so poll every 10s or slower. def poll_conversion(conversion_id, interval_s=15): while True: res = requests.get(f"{BASE}/conversions/{conversion_id}", headers=make_headers()) data = res.json()["data"] if data["status"] == "completed": return data if data["status"] == "failed": raise RuntimeError(f"Conversion failed: {data.get('failureReason')}") time.sleep(interval_s) ``` *** ## Processing stages `processingStages` shows progress in more detail. Use the top-level `status` to decide when the conversion is finished. | Stage | What happens | Statuses | | - | - | - | | `sendForConversion` | Funds are sent for conversion | `pending`, `initiated`, `executed`, `completed`, `failed` | | `conversionState` | The conversion is executed | `pending`, `executed`, `completed`, `failed` | | `settleCrypto` | Converted funds are settled | `pending`, `executed`, `completed`, `failed` | | `settleFiat` | Fiat is paid out to the bank account (fiat conversions only) | `pending`, `processed`, `completed`, `failed` | To get the on-chain transactions behind the stages, with their hashes, call [List Conversion Internal Transactions](/api-reference/endpoint/conversions/list-internal). The `conversion_send` entry is the send that funds the conversion. *** ## Error handling | Scenario | Action | | - | - | | `Conversion request must be more than $20 USD` | Increase the amount; check `from.amountUSD` from the fee endpoint | | Unsupported swap pair | Verify the pair via `/conversions/swap/chains` and `/conversions/swap/coins` | | `Invalid or inactive payout account` | Use the ID of one of your active payout accounts | | HTTP 500 `Failed to create conversion` | The pair could not be priced; re-check it with the fee endpoint and retry | | `status: "failed"` | Read `failureReason` on the conversion; retry or contact support | | HTTP 429 | Conversion endpoints share 10 requests/minute; slow down. See [Rate limits](/api-reference/rate-limits) | # Bulk Disbursements Source: https://docs.tender.cash/guides/disbursements Pay multiple recipients at once using USDC or XLM on the Stellar network Tender's disbursement feature lets you send funds to many recipients in a single action. Each recipient receives an invitation to claim their payment directly into their Tender loyalty wallet. ![Disbursement flow demo](https://mintcdn.com/bombatahabeebgmailcom/ksrs05kub9T7GP95/images/sdp-disbursement.gif?s=43ea339e2192c27c64f5e1c444eb4a7b) *** ## How it works **1. Prepare your recipient list** Collect each recipient's contact (email or phone), a unique ID, the amount, and their date of birth for verification. **2. Submit the disbursement** Go to **Wallet → Actions → Disburse** in your merchant dashboard, fill in the details, and upload a CSV or enter recipients manually. **3. Tender transfers funds on-chain** Tender debits your Stellar platform balance and transfers the total to the Stellar Disbursement Platform distribution account on-chain. **4. Recipients claim their funds** Each recipient receives an invitation via email or SMS. They click the link, verify their identity, and funds are credited to their Tender loyalty wallet. *** ## Prerequisites > **KYC approved** > > Your merchant account must be fully KYC verified before you can disburse. > **Stellar balance** > > You must hold the asset you want to disburse (USDC or XLM) in your Tender platform wallet. > The full total is debited at submission time. *** ## Supported assets | Asset | Network | Notes | | - | - | - | | USDC | Stellar | Settled to recipient's Tender loyalty wallet | | XLM | Stellar | Native asset — settled to recipient's Tender loyalty wallet | *** ## CSV format Upload a `.csv` file with the following columns: ```csv theme={null} contact,id,amount,verification alice@example.com,cust-001,10.00,1990-05-20 bob@example.com,cust-002,25.50,1985-11-03 +2348012345678,cust-003,5.00,1992-08-14 ``` | Column | Type | Description | | - | - | - | | `contact` | string | Recipient email address or phone number (E.164 format for phone) | | `id` | string | Your internal ID for this recipient — must be unique within the disbursement | | `amount` | number | Amount to send (e.g. `10.00`) | | `verification` | string | Recipient's date of birth in `YYYY-MM-DD` — must match exactly at claim time | > [!WARNING] > > The `verification` date of birth must match exactly what the recipient enters when they claim > their funds. A mismatch will block the payment. ### Rules * Each `id` must be unique within the disbursement * Minimum amount: `0.0000001` (Stellar precision) * Maximum recipients per disbursement: **10,000** * Disbursement `name` must be globally unique — plan a naming convention such as `payroll-2026-06` *** ## Disbursement statuses ### Disbursement-level | Status | Meaning | | - | - | | `STARTED` | Accepted — payments are being queued | | `IN_PROGRESS` | Some recipients paid, others pending | | `COMPLETED` | All recipients paid | | `PAUSED` | Manually paused | ### Per-recipient payment status | Status | Meaning | | - | - | | `READY` | Queued, waiting for recipient to register and verify | | `PENDING` | Submitted to the Stellar network | | `SUCCESS` | Funds landed in recipient's Tender loyalty wallet | | `FAILED` | Payment failed — will retry automatically | *** ## Troubleshooting > **"Insufficient balance"** > > Your Stellar platform balance is below the disbursement total. Top up via your wallet page > and retry. > **"Disbursement already exists"** > > A disbursement with that exact name was already submitted. Use a different, unique name. > **"Receiver verification mismatch"** > > The date of birth in your CSV doesn't match what the recipient entered at registration. > Confirm the correct date and resubmit with an updated record. > **Recipient hasn't received anything** > > Check whether they completed registration — they need to click the link in their email or SMS > invitation. Unregistered recipients stay in `READY` state until they do. > **"No USDC balance available on Stellar"** > > Your platform wallet has no Stellar USDC. Either switch to XLM or deposit USDC to your > Tender Stellar wallet first. *** ## FAQ > **Where do recipients receive their funds?** > > Funds are credited to the recipient's Tender loyalty wallet. Recipients without an account > will be guided through a short setup flow when they click their invitation link. > **Can I cancel a disbursement?** > > Disbursements can be paused before all payments are processed. Already-sent payments cannot > be reversed — Stellar transactions are final. > **Is there a limit on the number of recipients?** > > Up to 10,000 recipients per disbursement. For larger campaigns, split across multiple > disbursements. > **What happens if a recipient doesn't claim?** > > The payment stays in `READY` state indefinitely until the recipient completes their > registration and identity verification via the invitation link. # Fiat Payouts Source: https://docs.tender.cash/guides/fiat-payouts Pay out from your merchant balance to a Nigerian bank account, or as cash through MoneyGram A fiat payout debits crypto from your Tender merchant balance and delivers fiat to a recipient. There are two methods, both on the same endpoint, `POST /v1/api/payout/fiat`: **Internal** — sends fiat directly to a Nigerian bank account using the recipient's bank code and account number. Tender chooses the payout provider for each payout; you can't select one. **MoneyGram** — starts a MoneyGram payout. You get back a `url`; your user opens it to complete the payout with MoneyGram. Every request needs an `Idempotency-Key` header, so a retried request never pays out twice. See [Idempotency](/api-reference/idempotency). *** ## Prerequisites > **[Authentication guide](/api-reference/authentication)** > > These endpoints use Signed (HMAC) authentication — an HMAC-SHA256 signature is required on every request. See the authentication guide for code examples. ```javascript theme={null} const BASE = 'https://sandbox-api.tender.cash/v1/api'; // makeHeaders() → { 'x-access-id', 'x-request-id', 'x-timestamp', 'authorization', ... } ``` *** ## Internal bank transfer **1. Submit the payout** Call `POST /payout/fiat` with an `Idempotency-Key` header, `method: "internal"`, the `asset`, the `amount`, and the recipient's `bank_code` and `account_number`. **2. Confirm initiation** A successful response returns the payout record with `status: "pending"` and its `id`. **3. Poll for completion** Call `GET /payout/{id}` until `status` is `completed` or `failed`. There are no payout webhooks. * `asset` can be any coin you hold, for example `usdc` or `usdt`. Your balance of that asset across all chains must cover `amount`; Tender pays out from the chain where you hold the most of it. * The amount must be at least 20 units of the asset and worth at least \$20 USD. * The bank code must be a supported Nigerian bank. * An optional `reference` is stored on the payout as `withdrawalReference`, so you can match it to your own records. ```javascript Node.js theme={null} const res = await fetch(`${BASE}/payout/fiat`, { method: 'POST', headers: { ...makeHeaders(), 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ method: 'internal', asset: 'usdc', amount: '100', bank_code: '090267', account_number: '0123456789', reference: 'user-8821', }), }); const { data } = await res.json(); // data → { id, status: "pending", amount, asset, usdValue, bankName, bankCode, accountNumber, createdAt } ``` ```python Python theme={null} import uuid res = requests.post( f"{BASE}/payout/fiat", headers={**make_headers(), "Idempotency-Key": str(uuid.uuid4())}, json={ "method": "internal", "asset": "usdc", "amount": "100", "bank_code": "090267", "account_number": "0123456789", "reference": "user-8821", }, ) payout_id = res.json()["data"]["id"] ``` > **[Payout Fiat API reference](/api-reference/endpoint/payouts/fiat)** > > Full parameter list, errors and interactive playground for both methods. *** ## MoneyGram payout **1. Submit the payout** Call `POST /payout/fiat` with an `Idempotency-Key` header, `method: "moneygram"`, the `asset` and the `amount` (at least 15 units). Add any recipient details you already have. **2. Send your user to MoneyGram** The response contains `url`, `transactionId` and `id`. Open `url` to let your user complete the payout. **3. Poll for completion** Call `GET /payout/{id}`. Each call refreshes the status from MoneyGram: `providerStatus` holds MoneyGram's raw status and `status` becomes `completed`, `failed` or `processing`. The recipient details — `first_name`, `last_name`, `dob` (or `date_of_birth`), `mobile_number`, `address`, `city`, `postal_code`, `state_or_province` and `country` — are all **optional**. Any you send are pre-filled in the MoneyGram form so your user doesn't have to type them again. Funds come from your merchant Stellar wallet. To pay out from one of your Stellar subwallet addresses instead, pass its `address_reference`. ```javascript Node.js theme={null} const res = await fetch(`${BASE}/payout/fiat`, { method: 'POST', headers: { ...makeHeaders(), 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ method: 'moneygram', asset: 'usdc', amount: '100', reference: 'user-8821', first_name: 'John', last_name: 'Doe', mobile_number: '+2348012345678', country: 'NG', }), }); const { data } = await res.json(); // Open data.url to let your user complete the payout; poll GET /payout/{data.id} ``` ```python Python theme={null} import uuid res = requests.post( f"{BASE}/payout/fiat", headers={**make_headers(), "Idempotency-Key": str(uuid.uuid4())}, json={ "method": "moneygram", "asset": "usdc", "amount": "100", "reference": "user-8821", "first_name": "John", "last_name": "Doe", "mobile_number": "+2348012345678", "country": "NG", }, ) data = res.json()["data"] # {"url": ..., "transactionId": ..., "id": ...} ``` ### Identifying the end user with `reference` MoneyGram payouts accept an optional `reference` field — a stable identifier for the end user the payout is for. It is sent to MoneyGram for reconciliation, and it also determines which MoneyGram consumer the payout is attributed to. > [!WARNING] > > When you serve many end users through one Tender merchant account, pass a `reference` so each user maps to a **distinct MoneyGram consumer** — each with its own KYC record and its own transaction-limit window. Without a `reference`, every payout collapses into a single consumer for your merchant account, so all your users share one KYC identity and one aggregate limit. The value must be **stable per end user**: reuse the same `reference` for the same person on every payout, and use a different `reference` for a different person. A per-transaction value (a new order or invoice id each time) would create a new MoneyGram consumer on every payout — re-triggering KYC and fragmenting limits. Omit `reference` (or set it to your own merchant id) to attribute the payout to your merchant account itself. *** ## Retries and idempotency * Retrying with the **same** key and body returns the original response; no second payout is made. * Reusing a key with a **different** body, or while the first request is still running, returns `409`. * If the first request failed, the key is released and you can retry with it. * Keys are kept for 24 hours. *** ## Payout status values | Status | Meaning | | - | - | | `pending` | Queued, not yet processed | | `processing` | Being handled by the payout provider or MoneyGram | | `completed` | Successfully delivered | | `failed` | The payout did not go through | An `internal` payout runs as a conversion, so its crypto leaves your balance in an on-chain send. To get that transaction and its hash, call [List Payout Internal Transactions](/api-reference/endpoint/payouts/list-internal) with the payout ID. MoneyGram payouts have no internal transactions. *** ## Error handling | Scenario | Action | | - | - | | Insufficient balance | Ensure your balance of the asset covers the amount before submitting | | `Bank code "…" is not supported` | Verify the bank code against the list of supported Nigerian banks | | Below the minimum | `internal`: at least 20 units and \$20; `moneygram`: at least 15 units | | HTTP 503 `No withdrawal provider available for the selected asset` | MoneyGram doesn't support this asset right now; try another asset or later | | HTTP 409 | Retry the same body later, or use a new key for a new payout | | `status: "failed"` | Create a new payout with a new key, or contact support | # Payments Source: https://docs.tender.cash/guides/payments Accept cryptocurrency payments from customers on any supported chain Tender generates a unique deposit 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 fiat `amount`, the `chain` and the crypto `currency`. Tender returns a deposit `walletAddress`, the all-in crypto `amount` to collect, and a `txId` to track the payment. **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 checks the address on-chain automatically and sends [webhook events](/get-started/webhook-events) as the payment progresses. **4. Validate the payment** Call `POST /payment/validate/{id}` to check the payment on demand and read its current status. *** ## Prerequisites > **[Authentication guide](/api-reference/authentication)** > > Payment endpoints use Basic (Access ID) authentication — only your `x-access-id` header is > required. See the authentication guide for details on both auth methods. ```javascript theme={null} const BASE = 'https://sandbox-api.tender.cash/v1/api'; // makeHeaders() → { 'x-access-id': ACCESS_ID, 'Content-Type': 'application/json' } ``` *** ## Pricing: what `amount` means `amount` is a **fiat** price, not a crypto amount: * On `POST /payment/initiate`, it is in your outlet's default fiat currency (USD if none is set). * On `POST /payment/initiate/{currency}`, it is in the fiat currency named in the path, such as `ngn`. Tender converts it to the chosen cryptocurrency and adds every fee on top (the receive fee, any activation fee, any auto-payout fees and, on Tron, the gasless network service fee). The response's `amount` is the all-in crypto total the customer must send. To show the customer that total before creating a payment, call [`POST /payment/quote/{currency}`](/api-reference/endpoint/quote-payment). Its `totalPayable` matches what initiation charges. *** ## Step 1 — Initiate a payment ```javascript Node.js theme={null} const res = await fetch(`${BASE}/payment/initiate`, { method: 'POST', headers: makeHeaders(), body: JSON.stringify({ amount: '10.00', // fiat, in the outlet's default currency chain: 'tron', currency: 'usdt', reference: 'ORDER-9821', // your own order ID, optional meta: { customerEmail: 'customer@example.com' }, // optional }), }); const { data: payment } = await res.json(); /* { txId: "66aec7de809b7f45c42a49f9", walletAddress: "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", amount: "10.15", // all-in USDT the customer must send requestedAmount: "10.00", // the fiat amount you asked for usdAmount: "10.15", fee: 0.1, chain: "tron", referenceId: "ORDER-9821", ... } */ const { txId, walletAddress, amount } = payment; ``` ```python Python theme={null} res = requests.post( f"{BASE}/payment/initiate", headers=make_headers(), json={ "amount": "10.00", # fiat, in the outlet's default currency "chain": "tron", "currency": "usdt", "reference": "ORDER-9821", "meta": {"customerEmail": "customer@example.com"}, # optional }, ) payment = res.json()["data"] tx_id = payment["txId"] wallet_address = payment["walletAddress"] amount = payment["amount"] ``` A `reference` can be used by only one payment at a time. Reusing the reference of a payment that isn't cancelled returns `409`. See [Initiate Payment](/api-reference/endpoint/initiate-payment) for the full response and error list. *** ## Step 2 — Show payment details to your customer Display the deposit address, the network and the exact `amount` from the response. ``` Please send: 10.15 USDT Network: Tron (TRC-20) To: TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT Do not send from an exchange. Use a self-custody wallet. ``` > [!WARNING] > > The `walletAddress` is unique to this transaction. Never reuse addresses across payments. *** ## Step 3 — Poll for payment status `POST /payment/validate/{id}` returns HTTP **202** with the payment once funds have arrived. Until then it returns HTTP **406** with the message `Payment Not made yet`. A 406 is not a failure: keep polling until your own checkout deadline. ```javascript Node.js theme={null} async function pollPayment(txId, { intervalMs = 8000, timeoutMs = 30 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const res = await fetch(`${BASE}/payment/validate/${txId}`, { method: 'POST', headers: makeHeaders(), }); const body = await res.json(); if (res.status === 202) { const { status, amountReceived, balanceRequired } = body.data; console.log('Status:', status, '| Received:', amountReceived); if (status === 'completed' || status === 'overpayment') return body.data; if (status === 'partial-payment') { console.log('Still owed:', balanceRequired); // keep polling for a top-up } } else if (res.status !== 406) { throw new Error(`Validate failed: ${res.status} ${body.message}`); } // 406 (e.g. "Payment Not made yet") → keep polling await new Promise(r => setTimeout(r, intervalMs)); } // Customer didn't pay in time: cancel the request await fetch(`${BASE}/payment/cancel/${txId}`, { method: 'POST', headers: makeHeaders() }); return null; } const result = await pollPayment(txId); ``` ```python Python theme={null} import time def poll_payment(tx_id, interval_s=8, timeout_s=30 * 60): deadline = time.time() + timeout_s while time.time() < deadline: res = requests.post(f"{BASE}/payment/validate/{tx_id}", headers=make_headers()) body = res.json() if res.status_code == 202: data = body["data"] print(f"Status: {data['status']} | Received: {data['amountReceived']}") if data["status"] in ("completed", "overpayment"): return data if data["status"] == "partial-payment": print(f"Still owed: {data['balanceRequired']}") # keep polling for a top-up elif res.status_code != 406: raise RuntimeError(f"Validate failed: {res.status_code} {body['message']}") # 406 (e.g. "Payment Not made yet") → keep polling time.sleep(interval_s) # Customer didn't pay in time: cancel the request requests.post(f"{BASE}/payment/cancel/{tx_id}", headers=make_headers()) return None ``` > [!TIP] > > Use webhooks instead of polling for production. Register a webhook and Tender pushes > `transaction_completed`, `transaction_partially_paid`, `transaction_overpayment` and the other > payment events as they happen. See [Webhooks](/get-started/webhooks). *** ## Payment status values | Status | Meaning | | - | - | | `pending` | Address generated; awaiting the customer's transfer | | `reserved` | No payment arrived within about 2 hours. Tender keeps checking the address, less often; a payment that arrives still completes. | | `partial-payment` | Some funds arrived, but less than the full amount | | `completed` | The full amount arrived | | `overpayment` | The customer paid more than the amount (by about \$3 or more) | | `cancelled` | The request was cancelled, by you or automatically after it went unpaid | `completed` and `overpayment` are final. A payment can only be cancelled through the API while it is `pending`. *** ## Internal transactions Some payments are settled in more than one step. A payment can be bridged to another chain, or converted as it settles, and each step is its own on-chain transaction. These transactions don't appear in your payment list, because they belong to a payment you already see. To get them, with hashes and explorer links, call [List Payment Internal Transactions](/api-reference/endpoint/transactions/list-internal) with the payment's `txId`: | `role` | Meaning | | - | - | | `bridge_deposit` | Funds sent into a bridge to move the payment to another chain | | `bridge_settlement` | Funds received on the destination chain when the bridge completes | | `conversion_send` | The on-chain send that funds a conversion | | `conversion_settlement` | The transaction that credits the result of a conversion | Most payments have none and return an empty list. *** ## Unpaid payments Payments don't fail. A request with no funds received moves from `pending` to `reserved` after about 2 hours, and Tender cancels it automatically once it has been unpaid for at least a day. The automatic cancellation doesn't send a webhook. If your checkout has its own time limit, cancel the request yourself when it passes with [`POST /payment/cancel/{id}`](/api-reference/endpoint/cancel-payment). Cancelling sends the `transaction_cancelled` webhook and frees the payment's `reference` for reuse. *** ## Partial payments and overpayments If the customer sends less than the required `amount` (by \$0.10 or more), the payment moves to `partial-payment`. A smaller shortfall is treated as `completed`. While a payment is `partial-payment`: * `balanceRequired` shows what is still owed. The customer can send the rest to the same address. * Tender keeps checking for 24 hours from the first partial payment. After that it accepts the partial amount as final: `isFulfilled` becomes `true`, `fulfilledBy` is `expiry`, and Tender sends the `transaction_partial_fulfilled` webhook. The status stays `partial-payment`. If the customer sends noticeably more than required (about \$3 or more over), the payment moves to `overpayment`. *** ## Error handling | Scenario | Action | | - | - | | Validate returns 406 `Payment Not made yet` | Keep polling; no funds have arrived yet | | Customer sends too little | Status is `partial-payment`; ask for `balanceRequired`, or wait for the partial to be accepted as final | | Customer never pays | Cancel the request when your checkout times out, then initiate a new one if needed | | Wrong network | Funds sent on the wrong chain can't be recovered automatically — always display the network clearly | | Initiate returns 409 | A payment with the same `reference` already exists; validate or cancel it instead | # Payouts Source: https://docs.tender.cash/guides/payouts Send crypto from your merchant balance to a wallet address or saved payout account A payout sends crypto from your Tender merchant balance to an external destination — either a raw wallet address or a saved payout account. Payouts are processed asynchronously and do not require an OTP or password. Every payout request needs an `Idempotency-Key` header so that a retry never sends funds twice. To pay out fiat to a bank account or through MoneyGram, see [Fiat payouts](/guides/fiat-payouts). *** ## How it works **1. Check the fee** Call `GET /payout/fee/{chain}/{coin}/{amount}` to see the fee and net amount before committing. **2. Submit the payout** Call `POST /payout/crypto` with an `Idempotency-Key` header, the coin, chain, amount, and either a wallet `address` or a saved `payoutAccountId`. **3. Poll for completion** Call `GET /payout/{id}` until `status` is `completed` or `failed`. There are no payout webhooks, so polling is the only way to track progress. *** ## Prerequisites > **[Authentication guide](/api-reference/authentication)** > > These endpoints use Signed (HMAC) authentication — an HMAC-SHA256 signature is required on every request. See the authentication guide for code examples. ```javascript theme={null} const BASE = 'https://sandbox-api.tender.cash/v1/api'; // makeHeaders() → { 'x-access-id', 'x-request-id', 'x-timestamp', 'authorization', ... } ``` *** ## Step 1 — Check the fee ```javascript Node.js theme={null} const feeRes = await fetch( `${BASE}/payout/fee/tron/usdt/50`, { headers: makeHeaders() } ); const { data: fee } = await feeRes.json(); /* { amount: 50, // debited from your balance amountUSD: 50, fee: 0.5, feeUSD: 0.5, amountDeductedFee: 49.5, // what the recipient receives amountAfterFee: 50.5, // amount + fee (gross), not the amount delivered currency: "usdt", chain: "tron", estimatedArrival: "1-3 minutes", ... } */ ``` ```python Python theme={null} fee_res = requests.get( f"{BASE}/payout/fee/tron/usdt/50", headers=make_headers(), ) fee = fee_res.json()["data"] ``` *** ## Step 2 — Submit a payout to a wallet address Generate a new `Idempotency-Key` for each payout, and store it with your own record before you send the request. If the request times out, retry with the **same** key and body: you get the original response back instead of a second payout. See [Idempotency](/api-reference/idempotency). The amount must be worth at least \$20 USD. The fee is taken out of `amount`, so the recipient receives `amount` − fee. ```javascript Node.js theme={null} const payoutRes = await fetch(`${BASE}/payout/crypto`, { method: 'POST', headers: { ...makeHeaders(), 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ coin: 'usdt', chain: 'tron', amount: '50', address: 'TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT', }), }); const { data: payout } = await payoutRes.json(); const payoutId = payout._id; console.log('Status:', payout.status); // "pending" ``` ```python Python theme={null} import uuid payout_res = requests.post( f"{BASE}/payout/crypto", headers={**make_headers(), "Idempotency-Key": str(uuid.uuid4())}, json={ "coin": "usdt", "chain": "tron", "amount": "50", "address": "TQn9Y2khDD9JHTfVE5oB2h8BKWWM4LxKLT", }, ) payout = payout_res.json()["data"] payout_id = payout["_id"] ``` *** ## Step 2 (alt) — Payout to a saved payout account The saved account must be one of your active crypto payout accounts, on the same chain and coin as the payout. If you send both `address` and `payoutAccountId`, the saved account's address is used. ```javascript Node.js theme={null} const payoutRes = await fetch(`${BASE}/payout/crypto`, { method: 'POST', headers: { ...makeHeaders(), 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ coin: 'usdt', chain: 'tron', amount: '50', payoutAccountId: '66aec7de809b7f45c42a49f9', }), }); ``` ```python Python theme={null} payout_res = requests.post( f"{BASE}/payout/crypto", headers={**make_headers(), "Idempotency-Key": str(uuid.uuid4())}, json={ "coin": "usdt", "chain": "tron", "amount": "50", "payoutAccountId": "66aec7de809b7f45c42a49f9", }, ) ``` *** ## Step 3 — Poll for completion ```javascript Node.js theme={null} // Payout endpoints share 10 requests/minute, so poll every 10s or slower. async function pollPayout(id, intervalMs = 15000) { while (true) { const res = await fetch(`${BASE}/payout/${id}`, { headers: makeHeaders() }); const { data } = await res.json(); console.log('Status:', data.status); if (data.status === 'completed') return data; if (data.status === 'failed') throw new Error('Payout failed'); await new Promise(r => setTimeout(r, intervalMs)); } } const result = await pollPayout(payoutId); ``` ```python Python theme={null} import time # Payout endpoints share 10 requests/minute, so poll every 10s or slower. def poll_payout(payout_id, interval_s=15): while True: res = requests.get(f"{BASE}/payout/{payout_id}", headers=make_headers()) data = res.json()["data"] print(f"Status: {data['status']}") if data["status"] == "completed": return data if data["status"] == "failed": raise RuntimeError("Payout failed") time.sleep(interval_s) ``` > [!NOTE] > > Tender doesn't send webhooks for payouts. Poll `GET /payout/{id}` (or list recent payouts with > `GET /payout`) to track status. *** ## Payout status values | Status | Meaning | | - | - | | `pending` | Queued, not yet sent | | `processing` | Handed to the network or payout provider | | `completed` | Delivered; `txId` holds the on-chain transaction hash | | `failed` | The payout did not go through | *** ## Error handling | Scenario | Action | | - | - | | Insufficient balance | Check your balance on that chain before submitting | | `Minimum withdrawal amount is $20 USD` | Increase the amount; check `amountUSD` from the fee endpoint | | Neither `address` nor `payoutAccountId` provided | Send at least one; omitting both returns 400 | | `Idempotency-Key header is required` | Add the header to every `POST /payout/crypto` request | | HTTP 409 on retry | You reused a key with a different body, or the first request is still running. Retry the same body later, or use a new key for a new payout | | HTTP 429 | Payout endpoints share 10 requests/minute, and each `POST /payout/crypto` counts twice. See [Rate limits](/api-reference/rate-limits) | | `status: "failed"` | Check your balance and the destination, then create a new payout with a new key, or contact support | # Subwallets Source: https://docs.tender.cash/guides/subwallets Create and manage crypto wallets for your end-users — deposits, addresses, and transfers, all under your merchant account > [!WARNING] > > This feature is currently only available in the sandbox environment. Tender's subwallet offering lets you provision and manage a full wallet infrastructure for each of your end-users. You create a wallet per user, assign blockchain addresses, watch those addresses for incoming deposits, and initiate outbound transfers — all via your merchant API credentials, without your users ever touching Tender directly. *** ## How it works **1. Create a wallet** Provision a wallet tied to a unique reference you supply — typically your own user ID. Tender returns one address per supported network. **2. Add addresses** Create additional address slots on specific networks for an existing wallet. Each slot has an `address_reference` you choose. **3. Watch for deposits** Register addresses with Tender's deposit monitor. When a deposit arrives, Tender sends a `sub_user_wallet_transaction_detected` webhook to your server. **4. Initiate transfers** Send funds from an address slot to any destination. Tender picks a native coin, token or gasless token transfer from the currency and network, and sends `sub_user_wallet_transfer_confirmed` (or `sub_user_wallet_transfer_failed` for a failed gasless transfer) when it settles. *** ## Prerequisites > **[Authentication guide](/api-reference/authentication)** > > These endpoints use Signed (HMAC) authentication — an HMAC-SHA256 signature is required on every request. See the authentication guide for code examples. All subwallet endpoints share a limit of 120 requests per minute per merchant; `GET /wallet/balance/:address` is further limited to 30 per minute. See [Rate limits](/api-reference/rate-limits). *** ## Step 1 — Create a wallet Call `POST /wallet` with a `reference` for the end-user. References are **globally unique** across Tender, so prefix them with something specific to your platform. Tender provisions the wallet and returns one address per supported network (`EVM`, `TRON`, `BTC`), each with its own generated `address_reference` and, where the network supports it, a `gasless_address`. Store the `reference`: it is the `wallet_reference` (and the transfer's `merchant_reference`) in every later call for that user. > **[Create Wallet](/api-reference/endpoint/subwallets/create-wallet)** > > Full parameters and response schema for `POST /wallet` *** ## Step 2 — Add addresses to the wallet Call `POST /wallet/address` with the `wallet_reference`, an `address_reference` of your choice (also globally unique), and the `networks` you want addresses on (`EVM`, `TRON`, `BTC`). All addresses from one call share that `address_reference`. > **[Create Address](/api-reference/endpoint/subwallets/create-address)** > > Full parameters and response schema for `POST /wallet/address` > [!NOTE] > > On networks where token transfers run gasless, tokens are spent from the slot's > `gasless_address`, not its `address`. Ask users to deposit tokens to the `gasless_address` > when one is present. *** ## Step 3 — Watch for deposits Call `POST /wallet/filter` with the address and the network types to monitor (`EVM`, `TRON`, `BTC`). Once registered, Tender watches that address on-chain and sends a `sub_user_wallet_transaction_detected` webhook whenever a deposit is detected. The same deposit can be reported more than once as confirmations increase. Every webhook uses the envelope `{"eventId": "", "event": "", "data": {…}}`. **`sub_user_wallet_transaction_detected` data fields:** | Field | Description | | - | - | | `depositKey` | Unique key for the deposit (`chainId:txHash:index`). Use it to deduplicate. | | `transactionId` | Tender's ID for the deposit record | | `walletReference` | The wallet this deposit belongs to | | `addressReference` | The address slot's reference | | `addressId` | Tender's ID for the receiving address | | `chainId` | The chain the deposit arrived on (for example `ethereum`) | | `network` | Network type of the receiving address (for example `EVM`) | | `symbol` | Token symbol (for example `ETH`, `USDT`) | | `address` | The receiving address | | `txHash` | On-chain transaction hash | | `index` | Position of the transfer within the transaction | | `amount` | Amount in the token's smallest unit (for example wei for ETH) | | `decimals` | Decimal places — divide `amount` by `10 ** decimals` for the human value | | `confirmed` | `false` on first detection; `true` once fully confirmed | | `blockNumber` | Block the transaction was included in | | `confirmations` | Current confirmation count | | `confirmationsRequired` | Confirmations needed to consider the deposit settled | | `occurredAt` | ISO 8601 timestamp of the event | ```json theme={null} { "eventId": "2b9c3f0e-5d1a-4e7b-9a62-0c8d4f1e7a35", "event": "sub_user_wallet_transaction_detected", "data": { "depositKey": "ethereum:0xabc...:0", "transactionId": "6710a1f2c3d4e5f601234567", "walletReference": "acme-user-8821", "addressReference": "acme-user-8821-slot-2", "addressId": "6710a0e1b2c3d4e5f6012345", "chainId": "ethereum", "network": "EVM", "symbol": "USDT", "address": "0x7d8e9f...", "txHash": "0xabc...", "index": 0, "amount": "1000000", "decimals": 6, "confirmed": false, "blockNumber": 21034567, "confirmations": 1, "confirmationsRequired": 12, "occurredAt": "2026-10-15T09:00:00Z" } } ``` > [!WARNING] > > Treat a deposit as settled only when `confirmed` is `true` or > `confirmations >= confirmationsRequired`. > **[Add Deposit Filter](/api-reference/endpoint/subwallets/add-filter)** > > Full parameters and response schema for `POST /wallet/filter` *** ## Step 4 — Initiate a transfer Call `POST /wallet/transfer` with: * `chain` and `currency` (for example `tron` and `usdt`); * `receiver` and `amount` (human-readable units); * `merchant_reference`: the **wallet reference** to send from; * `address_reference`: the address slot to send from; * `payment_reference`: your own reference for this transfer, echoed in webhooks and the transaction list. `sender` is optional; the source address is resolved from the two references. **Supported chains:** `ethereum`, `tron`, `bitcoin` The response depends on the transfer type: * **Coin or on-chain token transfer**: `200` with a `tx_id`. Tender sends `sub_user_wallet_transfer_confirmed` once the transaction confirms. * **Gasless token transfer**: `202` with an `operation_id`. Poll [`POST /wallet/transfer/status`](/api-reference/endpoint/subwallets/transfer-status) or wait for `sub_user_wallet_transfer_confirmed` / `sub_user_wallet_transfer_failed`. > **[Transfer Funds](/api-reference/endpoint/subwallets/transfer)** > > Full parameters and response schema for `POST /wallet/transfer` **`sub_user_wallet_transfer_confirmed` data fields (on-chain transfers):** | Field | Description | | - | - | | `transactionId` | Tender's ID for the transfer record | | `txHash` | On-chain transaction hash | | `transferKey` | Unique key for this transfer; equals `txHash` | | `walletReference` | Wallet the transfer was sent from | | `addressReference` | Address slot the transfer was sent from | | `network` | Network type, returned as-is (for example `tron`) | | `type` | `coin` or `asset` | | `sender` / `receiver` | Source and destination addresses | | `amount` | Amount in human-readable units, as a string | | `asset` | Currency symbol (for example `USDT`) | | `paymentReference` | The `payment_reference` you sent | | `blockNumber` | Block the transaction was included in | For **gasless** transfers, `sub_user_wallet_transfer_confirmed` and `sub_user_wallet_transfer_failed` carry the same fields without `blockNumber`, plus: | Field | Description | | - | - | | `operationId` | The `operation_id` returned by the transfer call | | `transferKey` | Equals `operationId` | | `status` | Final operation status, as returned by the gasless network (`succeeded` or `succeed` on success; `failed` on failure) | | `txHash` | On-chain hash, if one was produced | ```json theme={null} { "eventId": "9f4e1c2a-7b3d-4a8e-b5c6-1d2e3f4a5b6c", "event": "sub_user_wallet_transfer_confirmed", "data": { "transactionId": "6710b3a4c5d6e7f801234567", "txHash": "7c1e9a...", "transferKey": "f3b0c2d1e4a5...", "operationId": "f3b0c2d1e4a5...", "status": "succeed", "walletReference": "acme-user-8821", "addressReference": "acme-user-8821-slot-2", "network": "tron", "type": "asset", "sender": "TLq9...", "receiver": "TABCef...", "amount": "50", "asset": "USDT", "paymentReference": "payout-9922" } } ``` *** ## Utility operations | Operation | Endpoint | What it does | | - | - | - | | List wallets | [`GET /wallet`](/api-reference/endpoint/subwallets/list-wallets) | Paginated list of all wallets under your merchant account | | List addresses | [`GET /wallet/:walletReference/addresses`](/api-reference/endpoint/subwallets/list-addresses) | Addresses belonging to a specific wallet | | List transactions | [`GET /wallet/:walletReference/transactions`](/api-reference/endpoint/subwallets/list-transactions) | Deposits and transfers of a wallet, newest first | | Get balance | [`GET /wallet/balance/:address`](/api-reference/endpoint/subwallets/get-balance) | Native, USDT and USDC balances of an address | | Transfer status | [`POST /wallet/transfer/status`](/api-reference/endpoint/subwallets/transfer-status) | Status of a gasless transfer by `operation_id` | | Validate address | [`POST /wallet/validate`](/api-reference/endpoint/subwallets/validate-address) | Check whether an address is valid on a network | | Check watched | [`GET /wallet/watch/:address`](/api-reference/endpoint/subwallets/check-watched) | Check whether an address is registered with the deposit monitor | *** ## Webhook events | Event | Sent when | | - | - | | `sub_user_wallet_transaction_detected` | A deposit is detected on a watched address, and again as it gains confirmations | | `sub_user_wallet_transfer_confirmed` | An outbound transfer confirms on-chain, or a gasless transfer succeeds | | `sub_user_wallet_transfer_failed` | A gasless transfer fails | Webhooks are delivered once, without retries. Use `depositKey` and `transferKey` to deduplicate, and use [List Wallet Transactions](/api-reference/endpoint/subwallets/list-transactions) to reconcile anything you missed. See [Webhooks](/get-started/webhooks) for how to configure your endpoint. *** ## When the wallet service is unavailable Any subwallet endpoint that talks to the blockchain can return `503` with `Wallet service is temporarily unavailable. Please try again.` Retry with backoff. For `POST /wallet` and `POST /wallet/transfer`, a `503` doesn't tell you whether the action happened: check [List Wallets](/api-reference/endpoint/subwallets/list-wallets) or [List Wallet Transactions](/api-reference/endpoint/subwallets/list-transactions) before retrying. # Home Source: https://docs.tender.cash/index Start accepting cryptocurrency payments and managing agents on your platform **Prerequisites**: Before you begin, make sure to create an account and complete onboarding. Start accepting cryptocurrency payments online through multiple blockchain networks with Tender's robust payment infrastructure. > **[Online Payments](/quickstart)** > > Accept cryptocurrency payments on your website, app, or digital platform > **[Agent Management](/api-reference/endpoint/agents/create)** > > Create and manage agents (sub-businesses) under your merchant account *** ## Quick Links > **[Create Account](/get-started/create-account)** > > Sign up for a Tender account and get started > **[Get API Credentials](/get-started/api-credentials)** > > Obtain your API keys from the dashboard > **[Authentication](/api-reference/authentication)** > > Learn how to authenticate API requests with HMAC signatures > **[Webhooks](/get-started/webhooks)** > > Configure webhooks for real-time event notifications *** ## API Reference Explore our comprehensive API documentation: > **[Payment APIs](/api-reference/endpoint/initiate-payment)** > > Initiate and validate cryptocurrency payments > **[Agent APIs](/api-reference/endpoint/agents/create)** > > Create and manage agent accounts > **[System APIs](/api-reference/endpoint/fetch-chains)** > > Get supported chains, currencies, and exchange rates > **[Webhook APIs](/api-reference/endpoint/add-webhook)** > > Configure and manage webhook endpoints # On-ramp Source: https://docs.tender.cash/onramp/overview Let customers pay with local fiat and receive cryptocurrency directly to their wallet 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 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: * Converts the fiat to the target coin on the target chain * Delivers the coin to the `targetAddress` on the `targetChain` **5. Poll for completion** Call `GET /onramp/{reference}` to track progress. There are no on-ramp webhooks; the `stages` object shows where the pipeline is at any moment. *** ## Pipeline stages After payment is confirmed, the on-ramp moves through these stages in order: | Stage | What happens | | - | - | | `payment` | The customer's bank transfer is received | | `payout`, `swap` | The fiat is converted to the target coin on the target chain | | `swapWithdraw` | Internal transfer step *(usually skipped)* | | `cryptoSend` | The coin is sent to the customer's `targetAddress` | Each stage has a `status` of `pending`, `in_progress`, `completed`, `failed`, or `skipped`. *** ## Overall status values | Status | Meaning | | - | - | | `pending_payment` | Waiting for the customer's bank transfer | | `processing` | Payment confirmed; pipeline is running | | `pending` | A step failed and will be retried automatically; keep polling | | `crypto_sent` | Crypto sent to `targetAddress`. This is the final success status. | | `failed` | The on-ramp failed and won't be retried | *** ## 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 60 seconds after it is created (shown as `expiresAt`) * **Single-use** — consumed the moment it is passed to `/onramp/initiate`, even if it has expired * **Yours only** — another merchant can't redeem it Attempting to initiate with an expired or already-used quote returns a `400` error. ### Minimum amount The fiat amount must be worth at least **\$15 USD** at the quoted rate. Smaller amounts are rejected at the quote step. ### Rate limits All on-ramp endpoints, including status polling, share a limit of **10 requests per minute** per merchant. Cache the chains and coins lists, and poll status about every 30 seconds. See [Rate limits](/api-reference/rate-limits). ### 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](/api-reference/authentication)** > > Onramp endpoints use Signed (HMAC) authentication — an HMAC-SHA256 signature is required on every request. 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: ```javascript theme={null} 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. ```javascript Node.js theme={null} // 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: "...", id: "usdt", name: "Tether USD", symbol: "USDT", ... }] // Use coin.symbol as targetCurrency and chain.id as targetChain. ``` ```python Python theme={null} import requests chains_res = requests.get(f"{BASE}/onramp/chains", headers=make_headers()) chains = chains_res.json()["data"] coins_res = requests.get(f"{BASE}/onramp/coins?chain=tron", headers=make_headers()) coins = coins_res.json()["data"] ``` *** ### Step 2 — Get a quote Once the customer has chosen a target currency and entered their fiat amount, fetch a live quote. ```javascript Node.js theme={null} 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', // the coin's symbol targetChain: 'tron', // the chain's id }), }); const { data: quote } = await quoteRes.json(); console.log(quote); /* { quoteId: "b7f2a1c3-9d4e-4b2f-a8c0-1e2d3f4a5b6c", fiatCurrency: "NGN", fiatAmount: 50000, targetCurrency: "USDT", targetChain: "tron", swapCurrency: "USDT", swapChain: "tron", rate: 1600, // NGN per 1 USD estimatedCryptoAmount: "31.250000", // 50000 / 1600 = $31.25 → 31.25 USDT at $1.00 expiresAt: "2026-10-15T10:01:00.000Z" } */ ``` ```python Python theme={null} quote_res = requests.post( f"{BASE}/onramp/quote", headers=make_headers(), json={ "fiatAmount": 50000, "fiatCurrency": "NGN", "targetCurrency": "USDT", "targetChain": "tron", }, ) quote = quote_res.json()["data"] ``` > [!WARNING] > > 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: ≈ 31.25 USDT Network: Tron (TRC-20) Rate expires: in 58 seconds ``` > [!TIP] > > If the customer takes longer than 60 seconds to confirm, fetch a new quote before initiating. *** ### Step 4 — Initiate the on-ramp After the customer confirms, call `/onramp/initiate` with the `quoteId` and their wallet address. ```javascript Node.js theme={null} 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' }, // optional; not returned by the status endpoint }), }); 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: "2026-10-15T11:00:20.000Z", amount: { value: 50000, currency: "NGN" }, targetChain: "tron", targetAddress: "TRDFGhjkytywooiueonuoo", targetCurrency: "USDT" } */ ``` ```python Python theme={null} initiate_res = requests.post( f"{BASE}/onramp/initiate", headers=make_headers(), json={ "quoteId": quote["quoteId"], "targetAddress": "TRDFGhjkytywooiueonuoo", "customer": { "email": "customer@example.com", "name": "Ada Obi", }, }, ) onramp = initiate_res.json()["data"] reference = onramp["reference"] ``` **Error cases to handle:** | Error message | Cause | Fix | | - | - | - | | `"Quote not found or already used"` | `quoteId` was already consumed or never existed | Fetch a fresh quote | | `"Quote has expired"` | `expiresAt` passed before `/initiate` was called | Fetch a fresh quote | | Validation message, e.g. `"quoteId must be a UUID"` | A field is missing or malformed | Fix 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` (success) or `failed`. A status of `pending` means a step is being retried; keep polling. Polling counts against the on-ramp limit of 10 requests per minute, so poll about every 30 seconds. ```javascript Node.js theme={null} async function pollOnramp(reference, intervalMs = 30000) { 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') { console.log('On-ramp complete!', data.stages.cryptoSend.data?.txRef); return data; } if (data.status === 'failed') { throw new Error(`On-ramp ${reference} failed`); } await new Promise(r => setTimeout(r, intervalMs)); } } const result = await pollOnramp(onramp.reference); ``` ```python Python theme={null} import time def poll_onramp(reference, interval_s=30): while True: res = requests.get(f"{BASE}/onramp/{reference}", headers=make_headers()) data = res.json()["data"] print(f"Status: {data['status']}") if data["status"] == "crypto_sent": print("On-ramp complete!") return data if data["status"] == "failed": raise RuntimeError(f"On-ramp {reference} failed") time.sleep(interval_s) result = poll_onramp(reference) ``` > [!NOTE] > > On-ramps don't send webhooks. Polling the status endpoint is the only way to track progress. *** ### Inspecting stage progress The `stages` object in the status response shows the exact state of each pipeline step. ```json theme={null} { "stages": { "payment": { "status": "completed", "attempts": 1, "completedAt": "2026-10-15T10:04:55Z" }, "payout": { "status": "completed", "attempts": 1, "completedAt": "2026-10-15T10:05:10Z" }, "swap": { "status": "in_progress", "attempts": 1, "startedAt": "2026-10-15T10:05:11Z" }, "swapWithdraw": { "status": "skipped", "attempts": 0 }, "cryptoSend": { "status": "pending", "attempts": 0 } } } ``` *** ### Error handling | Scenario | Recommended action | | - | - | | `status: "failed"` | Tell the customer the on-ramp failed and contact Tender support with the `reference` if they paid | | `status: "pending"` | A step is being retried automatically; keep polling | | `status: "pending_payment"` after 1 hour | The virtual account has expired; start a new on-ramp | | `429` while polling | Slow down; the on-ramp limit is 10 requests per minute | | Network error polling | Retry with exponential backoff; reading status has no side effects | # Quick Start Source: https://docs.tender.cash/quickstart Make your first cryptocurrency payment in minutes ## Get Started in Three Steps Get up and running with Tender's API and accept your first cryptocurrency payment. *** ### Step 1: Get Your API Credentials > **Generate credentials from dashboard** > > 1. Log in to your [Tender Merchant Dashboard](https://sandbox-merchant.tender.cash) (sandbox) or [merchant.tender.cash](https://merchant.tender.cash) (live) > > 2. Navigate to **Settings** → **API Credentials** > > 3. Click **Generate New Credentials** > > 4. Select **Test Environment** for development > > 5. Save your **Access ID** and **Access Secret** securely > > > [!TIP] > > > > Use the test environment while integrating. It uses testnet cryptocurrencies with no real value. > **Store credentials securely** > > Never hardcode credentials. Use environment variables: > > ```bash .env theme={null} > TENDER_ACCESS_ID=your_access_id_here > TENDER_ACCESS_SECRET=your_access_secret_here > TENDER_BASE_URL=https://sandbox-api.tender.cash > ``` *** ### Step 2: Make Your First API Call Choose your preferred language and make a signed request to create an online outlet (agent): ```javascript Node.js theme={null} import crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; // Your credentials const accessId = process.env.TENDER_ACCESS_ID; const accessSecret = process.env.TENDER_ACCESS_SECRET; // Generate signature const requestId = uuidv4(); const timeStamp = new Date().toISOString(); const payload = { timeStamp, requestId, accessId }; const signature = crypto .createHmac('sha256', accessSecret) .update(JSON.stringify(payload)) .digest('base64'); // Make API request const response = await fetch('https://sandbox-api.tender.cash/v1/api/agent/create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-access-id': accessId, 'x-request-id': requestId, 'x-timestamp': timeStamp, 'authorization': signature }, body: JSON.stringify({ type: 'online', name: 'Lagos Fashion Store', email: 'lagosfashion@store.com', location: 'Lagos', country: 'Nigeria', currency: 'NGN', outletIntent: 'API integration' }) }); const data = await response.json(); console.log('Agent created:', data); ``` ```python Python theme={null} import hmac import hashlib import base64 import json import uuid from datetime import datetime, timezone import requests import os # Your credentials access_id = os.getenv('TENDER_ACCESS_ID') access_secret = os.getenv('TENDER_ACCESS_SECRET') # Generate signature request_id = str(uuid.uuid4()) timestamp = datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z') payload = { "timeStamp": timestamp, "requestId": request_id, "accessId": access_id } # Compact JSON, keys in this order, to match the server's JSON.stringify message = json.dumps(payload, separators=(',', ':')) signature = base64.b64encode( hmac.new( access_secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).digest() ).decode('utf-8') # Make API request response = requests.post( 'https://sandbox-api.tender.cash/v1/api/agent/create', headers={ 'Content-Type': 'application/json', 'x-access-id': access_id, 'x-request-id': request_id, 'x-timestamp': timestamp, 'authorization': signature }, json={ 'type': 'online', 'name': 'Lagos Fashion Store', 'email': 'lagosfashion@store.com', 'location': 'Lagos', 'country': 'Nigeria', 'currency': 'NGN', 'outletIntent': 'API integration' } ) data = response.json() print('Agent created:', data) ``` ```php PHP theme={null} $timeStamp, 'requestId' => $requestId, 'accessId' => $accessId ]; $message = json_encode($payload, JSON_UNESCAPED_SLASHES); $signature = base64_encode(hash_hmac('sha256', $message, $accessSecret, true)); // Make API request $ch = curl_init('https://sandbox-api.tender.cash/v1/api/agent/create'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'type' => 'online', 'name' => 'Lagos Fashion Store', 'email' => 'lagosfashion@store.com', 'location' => 'Lagos', 'country' => 'Nigeria', 'currency' => 'NGN', 'outletIntent' => 'API integration' ])); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-access-id: ' . $accessId, 'x-request-id: ' . $requestId, 'x-timestamp: ' . $timeStamp, 'authorization: ' . $signature ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); echo 'Agent created: ' . print_r($data, true); ?> ``` **Expected Response:** ```json theme={null} { "status": "success", "message": "success", "data": { "id": "66f33b1fa9446a6b6b8f32cd", "name": "Lagos Fashion Store", "email": "lagosfashion@store.com", "country": "Nigeria", "location": "Lagos", "type": "online", "active": true, "reference": "vbibtucm9ynq3k2x7p", "merchantId": "6538e8f9bdec6d1a21978a64", "currency": "NGN", "totalSales": "0", "defaultFiatCurrency": { "currency": "NGN", "rate": "1500", "useSystemRate": true }, "createdAt": "2026-10-15T09:45:53.000Z" } } ``` > [!NOTE] > > `outletIntent` is required for `online` and `whatsapp` outlets, and any `phoneNumber` must be in > E.164 format (for example `+2348012345678`). Point-of-sale outlets are created from the > dashboard, not the API. See [Create Outlet](/api-reference/endpoint/agents/create). *** ### Step 3: Initiate Your First Payment Now let's create a cryptocurrency payment. Payment endpoints use [Basic (Access ID) authentication](/api-reference/authentication#basic-access-id-authentication), so only your `x-access-id` header is required — no signature: ```javascript theme={null} const paymentResponse = await fetch('https://sandbox-api.tender.cash/v1/api/payment/initiate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-access-id': accessId }, body: JSON.stringify({ amount: "10.00", // fiat, in your outlet's default currency (USD if none is set) chain: "ethereum", currency: "usdc", reference: "ORDER-1001" // optional, your own order ID }) }); const payment = await paymentResponse.json(); console.log('Payment initiated:', payment); ``` **Expected Response:** ```json theme={null} { "status": "success", "message": "success", "data": { "txId": "66aec7de809b7f45c42a49f9", "type": "receive", "chain": "ethereum", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "requestedAmount": "10.00", "amount": "10.1", "usdAmount": "10.10", "fee": 0.1, "feeUSD": 0.1, "referenceId": "ORDER-1001", "agentId": "6538eaaebdec6d1a21978e6a" } } ``` The response is abridged; see [Initiate Payment](/api-reference/endpoint/initiate-payment) for every field. `amount` is the all-in crypto total (your price plus fees) the customer must send. A new payment always starts as `pending`. > [!TIP] > > The `walletAddress` is where your customer should send exactly `amount` of the cryptocurrency. Track the payment with [webhooks](/get-started/webhooks) or [Validate Payment](/api-reference/endpoint/validate-payment), which returns HTTP 406 `Payment Not made yet` until funds arrive. *** ## Next Steps Now that you've made your first requests, explore more features: > **[Authentication](/api-reference/authentication)** > > Deep dive into HMAC signature authentication > **[Payment APIs](/api-reference/endpoint/initiate-payment)** > > Explore all payment endpoints and options > **[Configure Webhooks](/get-started/webhooks)** > > Set up real-time event notifications > **[Supported Chains](/api-reference/endpoint/fetch-chains)** > > View all supported blockchains and currencies *** ## Testing Your Integration Before going to production: **1. Test all payment flows** Test with different blockchains and currencies in the test environment **2. Set up webhooks** Configure and test webhook delivery for transaction events **3. Handle errors** Implement proper error handling for failed transactions **4. Security review** Ensure API credentials are stored securely and never exposed **5. Go live** Switch to production credentials and base URL *** ## Need Help? > **Contact Support** > > * **Email**: [support@tender.cash](mailto:support@tender.cash) > * **Documentation**: [docs.tender.cash](https://docs.tender.cash) > * **API Reference**: Browse complete API documentation # Donation SDK Source: https://docs.tender.cash/sdk/donation Collect crypto donations in your React application with the Tender donation widget ## Overview `@tender-cash/donation-sdk` is a React widget for collecting cryptocurrency donations. It renders a donation form — campaign header with fundraising progress, amount entry with a fiat/crypto toggle, preset amounts, asset and network pickers, and a fee breakdown — followed by a deposit screen with a QR code, wallet address and countdown. The widget mounts inside a Shadow DOM root, so its styles never leak into (or inherit from) your page. > **[@tender-cash/donation-sdk](https://www.npmjs.com/package/@tender-cash/donation-sdk)** > > Donation widget package for React *** ## Installation ```bash npm theme={null} npm install @tender-cash/donation-sdk ``` ```bash yarn theme={null} yarn add @tender-cash/donation-sdk ``` ```bash pnpm theme={null} pnpm add @tender-cash/donation-sdk ``` > [!NOTE] > > React 16.8 or later is required — `react` and `react-dom` are peer dependencies. *** ## Quickstart The widget fetches its campaign from the Tender API using your `accessId`, so the header fills itself in — no campaign props required. ```tsx theme={null} import { TenderDonationSdk, onFinishResponse } from '@tender-cash/donation-sdk'; function DonateComponent() { const handleEventResponse = (response: onFinishResponse) => { console.log('SDK Response:', response); }; return ( console.log('closed')} /> ); } ``` > [!NOTE] > > The Tender API does not store a fundraising target, so pass `campaign={{ goal }}` yourself to render the progress ring. *** ## Opening the widget imperatively Hold a ref and call `openDonation` when the donor clicks your own button. With a ref attached, the component renders nothing until then. ```tsx theme={null} import { useRef } from 'react'; import { TenderDonationSdk, TenderDonationRef } from '@tender-cash/donation-sdk'; function DonateButton() { const donationRef = useRef(null); return ( <> ); } ``` *** ## API Reference ### Component Props (`TenderDonationProps`) #### Required Props | Prop | Type | Description | | - | - | - | | `accessId` | `string` | Your Tender merchant Access ID. | | `fiatCurrency` | `string` | Currency the donation is denominated in, e.g. `"USD"`. | | `env` | `"sandbox"` \| `"test"` \| `"live"` \| `"local"` | Which Tender API to talk to. | #### Optional Props | Prop | Type | Description | | - | - | - | | `campaign` | `DonationCampaign` | Overrides for the fetched campaign. Pass `goal` here to show the progress ring. | | `autoOpen` | `boolean` | Opens on mount instead of showing the inline donate button. Defaults to `true` unless a `ref` is attached. | | `amount` | `number` | Pre-fills the amount field. The donor can still change it. | | `presetAmounts` | `number[]` | Preset chips under the amount input. Defaults to `[25, 50, 100, 250]`. | | `minAmount` | `number` | Minimum accepted donation. Defaults to `1`. | | `maxAmount` | `number` | Maximum accepted donation. | | `donor` | `Donor` | Pre-filled donor name / email / anonymous flag. | | `collectDonorDetails` | `boolean` | Shows name and email fields on the form. Defaults to `false`. | | `referenceId` | `string` | Your reference for this donation. Falls back to `campaign.id`. | | `donationExpirySeconds` | `number` | Deposit countdown length. Defaults to `1800` (30 minutes). | | `confirmationInterval` | `number` | Status polling interval in ms. Defaults to `5000`. | | `meta` | `DonationMeta` | Extra metadata forwarded to the Tender API. | | `theme` | `"light"` \| `"dark"` | Defaults to `"light"`. | | `onEventResponse` | `(data: onFinishResponse) => void` | Status callback. | | `closeModal` | `() => void` | Called when the donor closes the widget. | | `apiBaseUrl` | `string` | Overrides the API base URL for the chosen `env`. | | `apiRequest` | `TenderApiRequest` | Routes every API call through your backend. | ### `DonationCampaign` Every field is optional — each one overrides what the API returned. | Field | Type | Description | | - | - | - | | `id` | `string` | Donation reference. Defaults to the agent reference. | | `name` | `string` | Header name. Defaults to the merchant name. | | `logo` | `string` | Logo URL. Falls back to initials derived from `name`. | | `goal` | `number` | Fundraising target. **Not returned by the API** — pass it to show the ring. | | `raised` | `number` | Amount raised. Defaults to the agent's completed transaction value. | | `description` | `string` | Line rendered under the campaign name. | ### `Donor` | Field | Type | Description | | - | - | - | | `name` | `string` | Donor name. | | `email` | `string` | Donor email. | | `anonymous` | `boolean` | Whether the donation is made anonymously. | ### Ref Methods (`TenderDonationRef`) | Method | Description | | - | - | | `openDonation(params?)` | Opens the widget on the donation form. `params` may carry `referenceId`, `amount`, `campaign`, `donor`, `donationExpirySeconds` and `meta`. | | `dismiss()` | Closes the widget. | *** ## Campaign resolution `GET /system/campaign` resolves your `accessId` to its agent and returns the merchant profile plus that agent's transaction stats: | Widget field | Source | | - | - | | `name` | Merchant's `merchantName`, falling back to the agent name. | | `logo` | Merchant's `logo`, falling back to `avatar`. | | `description` | Merchant's `merchantDescription`. | | `raised` | Summed USD value of the agent's completed transactions. | | `donationCount` | Number of payment requests for the agent, in any status. | | `currency` | Currency of `raised`. Always `usd`. | | `reference` | The agent reference, used as the donation reference. | Anything passed in `campaign` overrides the fetched value. The **goal** is the exception — the API does not store a fundraising target, so pass `campaign={{ goal }}` yourself. The progress ring only renders when a goal is available; without one the widget shows the raised amount alone. If the campaign request fails, the widget carries on with whatever `campaign` props were supplied — a missing header never blocks a donation. *** ## Amount entry The amount field accepts either fiat or crypto. The toggle beside it swaps the denomination and converts the current value using the rate Tender returns for the selected asset, so the donation stays worth the same either way. When the API prices an asset, the form also shows the rate and a `Total send` line — the donation plus the network fee for the chosen chain. *** ## Donation status events `onEventResponse` fires as the donation progresses: | `status` | Meaning | | - | - | | `completed` | Full amount received. | | `partial-payment` | Some funds received; a balance is outstanding. | | `overpayment` | More than the entered amount was received. | | `pending` | Awaiting funds. | | `cancelled` | The donor cancelled, or the window expired. | | `failed` / `error` | The donation could not be processed. | ### Callback Data (`onFinishResponse`) ```typescript theme={null} interface onFinishResponse { status: "partial-payment" | "completed" | "overpayment" | "pending" | "error" | "cancelled" | "failed"; message: string; data: IDonationData | undefined; } ``` `data` carries the transaction id, wallet address, chain, asset, amounts, and — when known — the campaign id and donor details. ```typescript theme={null} interface IDonationData { id?: string; amount?: number; coinAmount?: number; coin?: string; chain?: string; address?: string; amountPaid?: string; balance?: string; excess?: string; status?: DonationStatusProps; campaignId?: string; donorName?: string; donorEmail?: string; anonymous?: boolean; } ``` *** ## Routing calls through your backend Pass `apiRequest` to keep credentials server-side. The SDK then calls your function instead of the Tender API directly, so request signing and secrets never reach the browser. ```tsx theme={null} { const response = await fetch(`/api/tender${path}`, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined }); const payload = await response.json(); return payload.data; }} campaign={{ name: 'Riverbend Relief Foundation' }} /> ``` ```typescript TenderApiRequest theme={null} type TenderApiRequest = (options: { path: string; method?: string; body?: unknown; }) => Promise; ``` *** ## Exports * `TenderDonationSdk` — the widget component * `TenderDonationProps` — component props * `TenderDonationRef` — imperative handle type * `DonationCampaign`, `Donor`, `DonationMeta`, `StartDonationParams` * `IDonationData`, `DonationStatusProps`, `onFinishResponse` * `TenderEnvironments`, `TenderApiRequest` *** ## Features * Shadow DOM style isolation * Automatic campaign resolution from your Access ID * Fundraising progress ring * Fiat/crypto amount toggle with live rates * Preset donation amounts and min/max limits * Optional donor detail capture and anonymous donations * Auto-open or programmatic control with refs * Optional backend proxy to keep credentials server-side * Full TypeScript support *** ## Next Steps > **[API Reference](/api-reference/introduction)** > > Explore the full API documentation > **[Webhooks](/get-started/webhooks)** > > Set up webhook notifications > **[GitHub](https://github.com/tender-cash)** > > View source repositories > **[Support](mailto:support@tender.cash)** > > Contact our support team # JavaScript SDK Source: https://docs.tender.cash/sdk/javascript Integrate Tender cryptocurrency payments in your JavaScript and React applications ## Overview This page contains full documentation for both JavaScript SDK tracks: * **Latest SDK**: `@tender-cash/js-sdk` * **Legacy SDK (Old Version)**: `@tender-cash/agent-sdk-react` **Latest SDK** ## Package > **[@tender-cash/js-sdk](https://www.npmjs.com/package/@tender-cash/js-sdk)** > > Current JavaScript SDK package *** ## Installation ```bash npm theme={null} npm install @tender-cash/js-sdk ``` ```bash yarn theme={null} yarn add @tender-cash/js-sdk ``` ```bash pnpm theme={null} pnpm add @tender-cash/js-sdk ``` *** ## Usage in React Use `TenderSdk` as the primary component. `TenderAgentSdk` is still exported as a backward-compatible alias. ```tsx theme={null} import { TenderSdk, onFinishResponse } from '@tender-cash/js-sdk'; function PaymentComponent() { const handleEventResponse = (response: onFinishResponse) => { console.log('SDK Response:', response); }; return ( ); } ``` > [!NOTE] > > When `referenceId` and `amount` are provided, the modal auto-opens on component mount. *** ## API Reference ### Component Props (`TenderAgentProps`) Applies to both `TenderSdk` and `TenderAgentSdk`. #### Required Props | Prop | Type | Description | | - | - | - | | `accessId` | `string` | Your Tender merchant Access ID. | | `fiatCurrency` | `string` | Fiat code, e.g. `"USD"`, `"EUR"`, `"NGN"`. | | `env` | `"test"` \| `"live"` \| `"local"` | Target environment. | #### Optional Props | Prop | Type | Description | | - | - | - | | `onEventResponse` | `(data: onFinishResponse) => void` | Called when payment state changes. | | `referenceId` | `string` | Payment reference. Required for auto-open mode. | | `amount` | `number` | Payment amount in fiat. Required for auto-open mode. | | `paymentExpirySeconds` | `number` | Payment expiration in seconds. | | `theme` | `"light"` \| `"dark"` | Modal theme. | | `closeModal` | `() => void` | Callback fired when modal closes. | *** ## Ref Usage ```tsx theme={null} import { useRef } from 'react'; import { TenderSdk, TenderRef } from '@tender-cash/js-sdk'; function PaymentComponent() { const tenderRef = useRef(null); const openPayment = () => { tenderRef.current?.initiatePayment({ amount: 150, referenceId: `order-${Date.now()}`, paymentExpirySeconds: 1800 }); }; const closePayment = () => { tenderRef.current?.dismiss(); }; return ( <> ); } ``` ### Ref Methods (`TenderRef`) | Method | Description | | - | - | | `initiatePayment` | Opens modal and starts payment with provided parameters. | | `dismiss` | Closes the modal. | *** ## Callback Data (`onFinishResponse`) ```typescript theme={null} interface onFinishResponse { status: "partial-payment" | "completed" | "overpayment" | "pending" | "error" | "cancelled"; message: string; data: IPaymentData | undefined; } ``` *** ## Features * Shadow DOM style isolation * Auto-open mode from props * Programmatic control with refs * TypeScript support * Works across desktop and mobile **Legacy SDK (Old Version)** > [!WARNING] > > This is the old SDK version. New integrations should use `@tender-cash/js-sdk`. ## Package > **[@tender-cash/agent-sdk-react](https://www.npmjs.com/package/@tender-cash/agent-sdk-react)** > > Legacy package (previous SDK version) *** ## Installation ```bash npm theme={null} npm install @tender-cash/agent-sdk-react ``` ```bash yarn theme={null} yarn add @tender-cash/agent-sdk-react ``` ```bash pnpm theme={null} pnpm add @tender-cash/agent-sdk-react ``` *** ## Usage in React ```jsx theme={null} import { TenderAgentSdk, onFinishResponse } from '@tender-cash/agent-sdk-react'; function PaymentComponent() { const handleEventResponse = (response: onFinishResponse) => { console.log('SDK Response:', response); }; return ( ); } ``` *** ## API Reference ### Component Props (`TenderAgentProps`) #### Required Props | Prop | Type | Description | | - | - | - | | `fiatCurrency` | `string` | Fiat currency code, e.g. `"USD"` or `"EUR"`. | | `accessId` | `string` | Your Tender merchant Access ID. | | `env` | `"test"` \| `"live"` \| `"local"` | Target environment. | #### Optional Props | Prop | Type | Description | | - | - | - | | `onEventResponse` | `(data: onFinishResponse) => void` | Called when payment status updates. | | `referenceId` | `string` | Payment reference. Required for auto-open mode. | | `amount` | `number` | Payment amount in fiat. Required for auto-open mode. | | `paymentExpirySeconds` | `number` | Payment expiration in seconds. | | `theme` | `"light"` \| `"dark"` | Modal theme. | *** ## Ref Usage ```jsx theme={null} import { useRef } from 'react'; import { TenderAgentSdk, TenderAgentRef } from '@tender-cash/agent-sdk-react'; function PaymentComponent() { const tenderRef = useRef(null); const handleOpenPayment = () => { tenderRef.current?.initiatePayment({ amount: 150.00, referenceId: "unique-payment-reference-123", paymentExpirySeconds: 1800 }); }; const handleCloseModal = () => { tenderRef.current?.closeModal(); }; return ( <> ); } ``` ### Ref Methods (`TenderAgentRef`) | Method | Description | | - | - | | `initiatePayment` | Opens modal and starts payment with provided parameters. | | `dismiss` | Closes the modal. | | `closeModal` | Closes the modal from outside the widget. | *** ## Callback Data (`onFinishResponse`) ```typescript theme={null} interface onFinishResponse { status: "partial-payment" | "completed" | "overpayment" | "pending" | "error" | "cancelled"; message: string; data: IPaymentData | undefined; } ``` *** ## Features * Shadow DOM style isolation * Auto-open modal flow * TypeScript support * Responsive payment UI *** ## Next Steps > **[API Reference](/api-reference/introduction)** > > Explore the full API documentation > **[Webhooks](/get-started/webhooks)** > > Set up webhook notifications > **[GitHub](https://github.com/tender-cash)** > > View source repositories > **[Support](mailto:support@tender.cash)** > > Contact our support team # Introduction Source: https://docs.tender.cash/sdk/overview Official Tender SDKs for JavaScript, React and donation applications ## Overview Tender provides official SDKs to help you integrate cryptocurrency payments and donations into your applications quickly and easily. *** ## Available SDKs > **[JavaScript SDK (Current)](/sdk/javascript)** > > `@tender-cash/js-sdk` for JavaScript and React applications > **[Donation SDK](/sdk/donation)** > > `@tender-cash/donation-sdk` — crypto donation widget for React > **[JavaScript SDK (Old Version)](https://www.npmjs.com/package/@tender-cash/agent-sdk-react)** > > Legacy package: `@tender-cash/agent-sdk-react` *** ## Features The Tender JavaScript SDK includes: * **Shadow DOM Isolation** - Prevents CSS conflicts with your application styles * **TypeScript Support** - Full TypeScript definitions for type safety * **Error Handling** - Comprehensive error handling with detailed messages * **Loading States** - Built-in loading state management * **Theme Customization** - Support for light and dark themes * **Auto-Open Modal** - Streamlined payment flow with automatic modal opening * **Framework Agnostic** - Works with React, Vue, Angular, or vanilla JavaScript * **Responsive Design** - Optimized for both desktop and mobile devices The Tender Donation SDK adds: * **Campaign Resolution** - Fetches campaign name, logo and amount raised from your Access ID * **Progress Ring** - Fundraising progress against a goal you supply * **Fiat/Crypto Toggle** - Enter the donation in either denomination with live rates * **Preset Amounts** - Configurable donation chips with min/max limits * **Donor Details** - Optional name, email and anonymous donations * **Backend Proxy** - Route API calls through your server to keep credentials off the client *** ## Key Features | Feature | JavaScript SDK | Donation SDK | | - | - | - | | TypeScript Support | ✅ | ✅ | | Payment Initiation | ✅ | ✅ | | Payment Validation | ✅ | ✅ | | Blockchain Selection | ✅ | ✅ | | Loading States | ✅ | ✅ | | Error Handling | ✅ | ✅ | | Theme Support | ✅ | ✅ | | Shadow DOM | ✅ | ✅ | | Mobile Responsive | ✅ | ✅ | | React Integration | ✅ | ✅ | | Vanilla JS Support | ✅ | ❌ | | Campaign Progress Ring | ❌ | ✅ | | Fiat/Crypto Amount Toggle | ❌ | ✅ | | Donor Detail Capture | ❌ | ✅ | | Backend API Proxy | ❌ | ✅ | *** ## Installation ```bash JavaScript SDK theme={null} npm install @tender-cash/js-sdk ``` ```bash Donation SDK theme={null} npm install @tender-cash/donation-sdk ``` *** ## Need Help? > **[API Reference](/api-reference/introduction)** > > View the full API documentation > **[Quickstart Guide](/quickstart)** > > Get started with Tender API > **[GitHub](https://github.com/tender-cash)** > > View source code and contribute > **[Support](mailto:support@tender.cash)** > > Contact our support team # WordPress (WooCommerce) Plugin Source: https://docs.tender.cash/sdk/wordpress Accept Tender cryptocurrency payments in WooCommerce using the official Tender JavaScript SDK. ## Overview The Tender WooCommerce plugin is a WordPress payment gateway that embeds the **Tender JavaScript/React SDK** directly into your WooCommerce checkout flow. Instead of custom HTML forms, the plugin uses the `@tender-cash/agent-sdk-react` package to render the Tender payment experience, including support for: * **Crypto payments** in your WooCommerce store * **Completed, partial, and overpayment** handling * **Test and live environments** * **WooCommerce Blocks** checkout compatibility * **Automatic WooCommerce order status updates** You can install the plugin from the provided ZIP file: * **Download**: [`woo-tender-sdk.zip`](https://tender-store.s3.us-east-1.amazonaws.com/woo-tender-sdk.zip) *** ## Installation Follow these steps to install the Tender WooCommerce plugin: 1. **Download** the plugin ZIP: [`woo-tender-sdk.zip`](https://tender-store.s3.us-east-1.amazonaws.com/woo-tender-sdk.zip) 2. In your WordPress admin, go to **Plugins → Add New → Upload Plugin** 3. Upload the ZIP file and click **Install Now** 4. Click **Activate** to enable the plugin 5. Go to **WooCommerce → Settings → Payments** 6. Enable **"Tender SDK Payment"** and click **Manage** to configure it Alternatively, you can upload it manually: 1. Extract the ZIP 2. Upload the `woocommerce-payment-sdk-gateway` folder to `/wp-content/plugins/` 3. Activate the plugin in **Plugins** *** ## Configuration In **WooCommerce → Settings → Payments → Tender SDK Payment**, configure the gateway with your Tender credentials. ### Required settings * **Access ID**: Your Tender merchant Access ID (from the Tender dashboard) * **Access Secret**: Your Tender merchant Access Secret * **Agent ID**: Your Tender Agent ID * **Environment**: `Test` (staging) or `Live` (production) ### Optional settings * **Title**: Name shown to customers at checkout (default: "Pay with Crypto") * **Description**: Description shown on the checkout page Make sure the credentials you use match the selected environment (test vs live). *** ## How It Works The WooCommerce gateway wraps the Tender React SDK and handles order status updates for you. 1. Customer selects **Tender SDK Payment** at checkout 2. After placing the order, they are redirected to the **Tender payment page** 3. The Tender React SDK loads and renders the payment UI 4. Customer chooses a cryptocurrency and completes payment 5. The SDK sends the payment status back to WordPress via secure AJAX 6. WooCommerce order status is updated automatically: * **Completed** – Order marked as paid * **Overpayment** – Order completed with a note about the excess amount * **Partial payment** – Order placed **on hold** pending full payment * **Error / Cancelled** – Customer is redirected back to checkout *** ## SDK Integration Details Under the hood, the plugin uses the **Tender Agent SDK React** library: * **Package**: `@tender-cash/agent-sdk-react` * **Load method**: ES module from CDN (`unpkg.com`) Example of the SDK props used by the plugin: ```jsx theme={null} ``` The `onEventResponse` callback is responsible for sending the payment result back to WordPress, which then updates the WooCommerce order. For a deeper dive into the SDK itself, see the **JavaScript SDK** docs at `/sdk/javascript`. *** ## File Structure (Plugin) The main plugin files are organized as follows: ```text theme={null} woocommerce-payment-sdk-gateway/ ├── woocommerce-tender-sdk-gateway.php # Main plugin bootstrap file ├── class-woocommerce-tender-sdk-gateway.php # WooCommerce gateway implementation ├── templates/ │ └── tender-sdk-payment.php # Payment page template that mounts the SDK ├── blocks/ │ ├── class-tender-sdk-payment-block.php # WooCommerce Blocks integration │ └── build/ │ ├── index.js # Block script │ └── index.asset.php # Asset dependencies ├── languages/ # Translation files └── README.md # Plugin README ``` Key WordPress/WooCommerce integration points: * `woocommerce_payment_gateways` – registers the gateway * `woocommerce_blocks_payment_method_type_registration` – registers the checkout block * `wp_ajax_tender_sdk_update_order_status` – handles payment status updates *** ## Requirements To use the plugin in production, you need: * **WordPress** 5.0 or higher * **WooCommerce** 3.0 or higher * **PHP** 7.4 or higher * A modern browser with JavaScript enabled * Valid Tender **Access ID**, **Access Secret**, and **Agent ID** *** ## Security Considerations The WooCommerce plugin follows WordPress security best practices: * All AJAX requests use **WordPress nonces** * Tender access credentials are stored securely in **WordPress options** * Order IDs and payment data are validated before processing * Payment details are sanitized before being stored For additional security recommendations, see the **Best Practices** section in the JavaScript SDK docs. *** ## Support & Next Steps If you encounter issues: * **WooCommerce / WordPress integration**: Contact your internal development team * **Tender SDK behavior**: Refer to the **JavaScript SDK** docs or Tender support You can continue exploring: > **[JavaScript SDK](/sdk/javascript)** > > Learn more about the underlying Tender React SDK. > **[API Reference](/api-reference/introduction)** > > Explore the full Tender API.