API Documentation
Integrate Trend Digital's regulated crypto-to-fiat infrastructure. Create invoices, send customer payouts, run KYC compliance checks, and receive real-time webhooks. All requests are authenticated with a single API key.
Environments
Use Sandbox for development and testing. All transactions use testnet tokens, so no real funds are involved. Once your integration is working, switch the base URL and API key to Production.
| Sandbox | Production | |
|---|---|---|
| Base URL | https://qa.trend.digital | https://api.trend.digital |
| Merchant Portal | merchant-qa.trend.digital | merchant.trend.digital |
| Network | Testnets (no real funds) | Mainnets |
Supported Currencies
Contract addresses and token decimals for each supported stablecoin, by environment. Always send and validate amounts against the correct contract for the environment you are calling.
Sandbox
| Asset | Network | Contract address |
|---|---|---|
USDT | Ethereum (Sepolia testnet) | 0xb60B42C095d08776a6ecc9a246180FE37AbA02A5 |
USDT | Tron (Shasta testnet) | TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs |
USDC | Ethereum (Sepolia testnet) | 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 |
Production
| Asset | Network | Contract address |
|---|---|---|
USDT | Ethereum | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
USDT | Tron | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
USDC | Ethereum | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
Authentication
All API requests require an API key sent via the x-api-key header. The API key identifies your merchant account automatically. No other authentication headers are needed.
curl https://qa.trend.digital/invoices \
-H "x-api-key: YOUR_API_KEY"Getting Your API Key
- Log in to the Merchant Portal (Sandbox or Production).
- Go to Settings > API Keys.
- Click Create API Key.
- Copy the key immediately, it is only displayed once.
If you lose your key, generate a new one from the Merchant Portal. The previous key is revoked immediately.
| Field | Type | Description |
|---|---|---|
| Scope | - | One key per merchant account |
| Expiration | - | Keys do not expire |
| Revocation | - | Generating a new key immediately revokes the previous one |
| Storage | - | Keys are hashed server-side, we cannot retrieve your key after creation |
Required Headers
| Field | Type | Description |
|---|---|---|
| x-api-key | Your API key | Every request |
| Content-Type | application/json | POST and PATCH requests |
Example Requests
curl -X POST https://qa.trend.digital/invoices \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"email": "customer@example.com",
"currency": "USDT",
"network": "ETHEREUM",
"amount": "100",
"desc": "Payment for services",
"invoiceType": "NORMAL_INVOICE"
}'curl -X POST https://api.trend.digital/invoices \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"email": "customer@example.com",
"currency": "USDT",
"network": "ETHEREUM",
"amount": "100",
"desc": "Payment for services",
"invoiceType": "NORMAL_INVOICE"
}'Security Best Practices
- Never expose your API key in client-side code, browsers, mobile apps, or public repositories.
- Store the key in environment variables or a secrets manager.
- Use your Sandbox key for development and testing.
- Use your Production key only in your server-side backend.
- If you suspect a key has been compromised, regenerate it immediately from the Merchant Portal.
Invoices
Create and manage payment invoices. When a customer pays an invoice, the funds are settled in your chosen cryptocurrency.
The Invoice Object
| Field | Type | Description |
|---|---|---|
| id | string | Unique invoice identifier |
| merchantId | string | Merchant account identifier |
| string | Customer email address | |
| name | string | null | Customer name |
| mobile | string | null | Customer phone number |
| address | string | null | Customer address |
| state | string | null | Customer state/province |
| country | string | null | Customer country |
| amount | number | Invoice amount (e.g. 100 = 100 USDT) |
| desc | string | Invoice description |
| reference | string | null | Your internal reference ID |
| discount | number | null | Discount value applied |
| discountType | string | null | PERCENTAGE or FLAT |
| items | array | null | Line items (each has id, name, qty, price) |
| dueDate | string | null | Due date (ISO 8601) |
| invoiceType | string | NORMAL_INVOICE or OPEN_INVOICE |
| status | string | Current status |
| validTill | string | null | Expiration date (ISO 8601) |
| merchantFee | number | null | Custom merchant fee value |
| merchantFeeType | string | null | PERCENTAGE or FLAT |
| paymentUrl | string | Payment page URL for the customer |
| createdAt | string | Creation timestamp (ISO 8601) |
| updatedAt | string | Last update timestamp (ISO 8601) |
Invoice Statuses & Types
| Status | Description |
|---|---|
| UNPAID | Invoice created, awaiting payment |
| PAID | Customer sent the exact amount |
| OVERPAID | Customer sent more than the invoice amount |
| UNDERPAID | Customer sent less than the invoice amount |
| EXPIRED | Invoice exceeded its validity period without full payment |
| CANCELLED | Invoice cancelled via API |
| Status | Description |
|---|---|
| NORMAL_INVOICE | Fixed amount, customer must pay the exact amount |
| OPEN_INVOICE | Open amount, customer chooses how much to pay |
Create Invoice
Creates a new payment invoice in UNPAID status.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Customer email address | |
| name | string | No | Customer name |
| mobile | string | No | Customer phone number |
| address | string | No | Customer address |
| state | string | No | Customer state/province |
| country | string | No | Customer country |
| currency | string | Yes | Settlement currency symbol (e.g. USDT, USDC) |
| network | string | No | Blockchain network (e.g. ETHEREUM, TRON). If omitted, the default network for the currency is used |
| amount | string | Yes | Invoice amount as a string (e.g. "100" for 100 USDT) |
| desc | string | Yes | Description shown to the customer |
| invoiceType | string | Yes | NORMAL_INVOICE or OPEN_INVOICE |
| reference | string | No | Your internal reference ID |
| dueDate | string | No | Due date (ISO 8601). Defaults to your configured invoice duration |
| discount | number | No | Discount value |
| discountType | string | No | PERCENTAGE or FLAT. Required if discount is provided |
| items | array | No | Line items. Each item: id (number), name (string), qty (number), price (number) |
| validTill | string | No | Expiration date (ISO 8601) |
| merchantFee | number | No | Custom merchant fee override |
| merchantFeeType | string | No | PERCENTAGE or FLAT. Required if merchantFee is provided |
curl -X POST https://qa.trend.digital/invoices \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"email": "customer@example.com",
"name": "John Doe",
"currency": "USDT",
"network": "ETHEREUM",
"amount": "100",
"desc": "Payment for Order #1234",
"invoiceType": "NORMAL_INVOICE",
"reference": "ORD-1234"
}'{
"id": "inv_a1b2c3d4e5f6",
"merchantId": "mrc_x1y2z3",
"email": "customer@example.com",
"name": "John Doe",
"mobile": null,
"address": null,
"state": null,
"country": null,
"amount": 100,
"desc": "Payment for Order #1234",
"reference": "ORD-1234",
"discount": null,
"discountType": null,
"items": null,
"dueDate": null,
"invoiceType": "NORMAL_INVOICE",
"status": "UNPAID",
"validTill": null,
"merchantFee": null,
"merchantFeeType": null,
"paymentUrl": "https://merchant.trend.digital/payment?invoiceId=inv_a1b2c3d4e5f6",
"createdAt": "2025-02-20T10:30:00.000Z",
"updatedAt": "2025-02-20T10:30:00.000Z"
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | Validation failed | Missing or invalid fields | Check the request body against the required fields table |
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
List Invoices
Returns a list of invoices for your merchant account.
Query Parameters
| Field | Type | Description |
|---|---|---|
| page | integer | Page number (default 1) |
| limit | integer | Items per page (default 10) |
| sort | string | Sort order (e.g. createdAt.desc) |
| filter[status] | string | UNPAID, PAID, OVERPAID, UNDERPAID, EXPIRED, CANCELLED |
| filter[email] | string | Filter by customer email |
curl "https://qa.trend.digital/invoices?page=1&limit=10&filter[status]=PAID&sort=createdAt.desc" \
-H "x-api-key: YOUR_API_KEY"{
"data": [
{
"id": "inv_a1b2c3d4e5f6",
"merchantId": "mrc_x1y2z3",
"email": "customer@example.com",
"name": "John Doe",
"mobile": null,
"address": null,
"state": null,
"country": null,
"amount": 100,
"desc": "Payment for Order #1234",
"reference": "ORD-1234",
"discount": null,
"discountType": null,
"items": null,
"dueDate": null,
"invoiceType": "NORMAL_INVOICE",
"status": "PAID",
"validTill": null,
"merchantFee": null,
"merchantFeeType": null,
"paymentUrl": "https://merchant.trend.digital/payment?invoiceId=inv_a1b2c3d4e5f6",
"createdAt": "2025-02-20T10:30:00.000Z",
"updatedAt": "2025-02-20T12:15:00.000Z"
}
],
"total": 1
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Get Invoice Details
Retrieves full invoice details including settlement currency, individual on-chain payments, and refund history.
curl "https://qa.trend.digital/invoices/inv_a1b2c3d4e5f6/details" \
-H "x-api-key: YOUR_API_KEY"{
"id": "inv_a1b2c3d4e5f6",
"merchantId": "mrc_x1y2z3",
"email": "customer@example.com",
"name": "John Doe",
"mobile": null,
"address": null,
"state": null,
"country": null,
"amount": 100,
"desc": "Payment for Order #1234",
"reference": "ORD-1234",
"discount": null,
"discountType": null,
"items": null,
"dueDate": null,
"invoiceType": "NORMAL_INVOICE",
"status": "PAID",
"validTill": null,
"merchantFee": null,
"merchantFeeType": null,
"paymentUrl": "https://merchant.trend.digital/payment?invoiceId=inv_a1b2c3d4e5f6",
"receiveMerchantCurrency": {
"currency": "USDT",
"network": "ETHEREUM"
},
"transaction": {
"lockedAmount": 100,
"settlementAmount": 100,
"incomingCurrency": {
"currency": "ETH",
"network": "ETHEREUM"
},
"payments": [
{
"id": "pay_x1y2z3",
"expectedAmt": 0.05,
"paidAmt": 0.05,
"txHash": "0xabc123def456789...",
"status": "CONFIRMED",
"paidAt": "2025-02-20T12:00:00.000Z"
}
]
},
"refunds": null,
"createdAt": "2025-02-20T10:30:00.000Z",
"updatedAt": "2025-02-20T12:15:00.000Z"
}transaction.payments[]: on-chain payments
| Field | Type | Description |
|---|---|---|
| id | string | Payment identifier |
| expectedAmt | number | Expected payment amount |
| paidAmt | number | Actual amount paid |
| txHash | string | On-chain transaction hash |
| status | string | Payment status (e.g. CONFIRMED) |
| paidAt | string | Payment timestamp (ISO 8601) |
refunds[]: refund records (or null)
| Field | Type | Description |
|---|---|---|
| id | string | Refund identifier |
| reqAmount | number | Requested refund amount |
| refundType | string | PARTIAL or FULL |
| destAddress | string | Destination wallet for refund |
| customerEmail | string | Customer email |
| remarks | string | null | Refund reason |
| refundStatus | string | PENDING, COMPLETED, or FAILED |
| txHash | string | null | On-chain transaction hash (when completed) |
| processedAt | string | null | Processing timestamp (ISO 8601) |
| createdAt | string | Creation timestamp (ISO 8601) |
Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | You do not have access to this invoice | Invoice belongs to a different merchant | You can only access your own invoices |
| 404 | Invoice does not exist | No invoice found with this ID | Check the invoice ID |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Cancel Invoice
Cancels an unpaid invoice. Only invoices in UNPAID status can be cancelled.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | Must be CANCELLED |
curl -X PATCH "https://qa.trend.digital/invoices/inv_a1b2c3d4e5f6" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"status": "CANCELLED"}'{
"id": "inv_a1b2c3d4e5f6",
"merchantId": "mrc_x1y2z3",
"email": "customer@example.com",
"name": "John Doe",
"mobile": null,
"address": null,
"state": null,
"country": null,
"amount": 100,
"desc": "Payment for Order #1234",
"reference": "ORD-1234",
"discount": null,
"discountType": null,
"items": null,
"dueDate": null,
"invoiceType": "NORMAL_INVOICE",
"status": "CANCELLED",
"validTill": null,
"merchantFee": null,
"merchantFeeType": null,
"paymentUrl": "https://merchant.trend.digital/payment?invoiceId=inv_a1b2c3d4e5f6",
"createdAt": "2025-02-20T10:30:00.000Z",
"updatedAt": "2025-02-20T14:00:00.000Z"
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | You do not have access to this invoice | Invoice belongs to a different merchant | You can only cancel your own invoices |
| 404 | Invoice does not exist | No invoice found with this ID | Check the invoice ID |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
OTC Trading
Trade crypto against fiat (and back) at a locked, quoted price. The OTC flow is a two-step, quote-then-execute model: request a live quote, then execute against that quote's single-use token before it expires. Pairs are identified by plain currency codes, so you never handle internal identifiers.
All OTC endpoints use the same authentication as the rest of the API: a single x-api-key header. The tenant is resolved from your key, so no other identifying header is required. Base URLs are the same as Environments.
The OTC Trade Object
| Field | Type | Description |
|---|---|---|
| id | string | Unique trade identifier |
| from | string | Currency code being sold from (e.g. USDT) |
| fromNetwork | string | null | Network of the from currency (e.g. TRON, ETHEREUM); null for fiat |
| to | string | Currency code being received (e.g. USD) |
| toNetwork | string | null | Network of the to currency; null for fiat |
| requestedAmount | string | Amount of the from currency submitted for the trade |
| lockedQuoteRaw | string | Full-precision price locked at quote time |
| executedQuoteRaw | string | Full-precision price the trade actually filled at |
| side | string | SELL or BUY |
| otcTradeStatus | string | Trade status (see below) |
| otcSettlementStatus | string | Settlement status (see below) |
| filledSettlementAmtRaw | string | Full-precision settled amount in the to currency |
| txHash | string | null | On-chain transaction hash, when applicable |
| bankRef | string | null | Bank reference, when applicable |
| failedReason | string | null | Reason the trade failed, if otcTradeStatus is FAILED |
| executedAt | string | null | ISO 8601 timestamp of execution |
| createdAt | string | ISO 8601 timestamp the trade was created |
| updatedAt | string | ISO 8601 timestamp the trade was last updated |
| feeFlat | number | Flat fee applied to the trade |
| feePercentage | number | Percentage fee applied to the trade |
Trade & Settlement Statuses
Trade statuses (otcTradeStatus)
| Status | Description |
|---|---|
| PENDING | Trade recorded, execution in progress |
| EXECUTED | Trade filled successfully |
| FAILED | Trade did not fill; see failedReason |
| CANCELLED | Trade was cancelled |
Settlement statuses (otcSettlementStatus)
| Status | Description |
|---|---|
| PENDING | Awaiting settlement |
| SETTLED | Fully settled |
| PARTIALLY_SETTLED | Partially settled |
| PARTIALLY_FAILED | Settlement partially failed |
| FAILED | Settlement failed |
List Available Pairs
Returns the currency pairs currently enabled for OTC trading. Use this as your discovery menu: the from, fromNetwork, to, and toNetwork values map directly onto the quote and create request bodies. When the same currency exists on more than one network (for example USDT on both TRON and ETHEREUM), each network appears as its own entry, and you must pass the matching fromNetwork in later calls to disambiguate.
curl -X GET https://qa.trend.digital/otc-trades/pairs \
-H "x-api-key: YOUR_API_KEY"[
{ "from": "USDT", "fromNetwork": "ETHEREUM", "to": "GBP", "toNetwork": null },
{ "from": "USDT", "fromNetwork": "TRON", "to": "USD", "toNetwork": null },
{ "from": "USDC", "fromNetwork": "ETHEREUM", "to": "EUR", "toNetwork": null }
]Get a Quote
Requests a live, locked price for a pair. The response includes a single-use quoteToken that you pass to the create endpoint to execute the trade. A quote is valid for 20 seconds and can be used to execute one trade only.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Yes | Currency code to trade from (e.g. USDT) |
| to | string | Yes | Currency code to receive (e.g. USD) |
| fromNetwork | string | No | Required when from exists on more than one network |
| toNetwork | string | No | Required when to exists on more than one network |
| amount | number | Yes | Amount of the from currency to trade |
| side | string | Yes | SELL or BUY |
curl -X POST https://qa.trend.digital/otc-trades/public/quote \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"from": "USDT",
"fromNetwork": "TRON",
"to": "USD",
"amount": 3,
"side": "SELL"
}'{
"price": 0.99884,
"displayPrice": 0.99,
"expiresAt": 1785943388618,
"quoteToken": "1785943388618.b58b4b10-677c-44b7-b034-ffe7de9735da.f069f638cc378deef613d442ff65fde69d1f9b8afa2b1041a548632ede4cb104",
"total": 2.9965,
"currency": "USD",
"feeFlat": "0",
"feePercentage": "5",
"feeCurrency": "USD"
}Response Fields
| Field | Type | Description |
|---|---|---|
| price | number | Full-precision locked price. Pass this back to the create endpoint unchanged |
| displayPrice | number | Rounded price for display purposes |
| expiresAt | number | Unix epoch (ms) when the quote and its token expire |
| quoteToken | string | Single-use token that authorizes execution of this quote |
| total | number | Total in the to currency for the requested amount |
| currency | string | Currency of total |
| feeFlat | string | Flat fee. Only present when fee visibility is enabled for your account |
| feePercentage | string | Percentage fee. Only present when fee visibility is enabled for your account |
| feeCurrency | string | Currency the fee is charged in. Only present when fee visibility is enabled for your account |
Note: The fee fields (feeFlat, feePercentage, feeCurrency) are only returned if fee visibility is enabled for your account. If it is not, they are omitted from the response.
Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | This currency pair is not available for OTC trading | The from/to/network combination is not an enabled pair, or a network is needed to disambiguate | Use a combination returned by GET /otc-trades/pairs, including fromNetwork where required |
Execute a Trade
Executes a trade against a quote. Send the same pair and amount you quoted, along with the exact price, displayPrice, and quoteToken from the quote response. Must be called within 20 seconds of the quote.
The quoteToken is single-use: once a trade executes against it, the same token cannot execute a second trade.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Yes | Same value used in the quote |
| to | string | Yes | Same value used in the quote |
| fromNetwork | string | No | Same value used in the quote |
| toNetwork | string | No | Same value used in the quote |
| amount | number | Yes | Same value used in the quote |
| side | string | Yes | Same value used in the quote |
| price | number | Yes | The exact price from the quote response |
| displayPrice | number | Yes | The exact displayPrice from the quote response |
| quoteToken | string | Yes | The quoteToken from the quote response |
curl -X POST https://qa.trend.digital/otc-trades/public/create \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"from": "USDT",
"fromNetwork": "TRON",
"to": "USD",
"amount": 3,
"side": "SELL",
"price": 0.99884,
"displayPrice": 0.99,
"quoteToken": "1785943388618.b58b4b10-677c-44b7-b034-ffe7de9735da.f069f638cc378deef613d442ff65fde69d1f9b8afa2b1041a548632ede4cb104"
}'Trade ExecutedErrors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | Invalid quote token | The token was not issued by this environment, is malformed, or has been altered | Request a fresh quote from the same environment and use its token unchanged |
| 400 | Quote expired, please request a new quote | More than 20 seconds passed since the quote was issued | Request a new quote and execute within 20 seconds |
| 400 | LP order failed | The liquidity provider rejected or could not fill the order | Request a new quote and retry |
| 409 | Duplicate | The quoteToken was already used to execute a trade | Do not reuse a token; each quote authorizes a single trade |
List Trades
Returns your OTC trades, most recent first, with pagination.
Query Parameters
| Field | Type | Description |
|---|---|---|
| page | number | Page number (default 1) |
| limit | number | Page size (default 20) |
curl -X GET "https://qa.trend.digital/otc-trades/public?page=1&limit=20" \
-H "x-api-key: YOUR_API_KEY"{
"data": [
{
"id": "ie66geg6ratq8ptaufth8v0w",
"from": "USDT",
"fromNetwork": "ETHEREUM",
"to": "EUR",
"toNetwork": null,
"requestedAmount": "10.00",
"lockedQuoteRaw": "0.86999",
"executedQuoteRaw": "0.87002",
"side": "SELL",
"otcTradeStatus": "EXECUTED",
"otcSettlementStatus": "SETTLED",
"filledSettlementAmtRaw": "8.7002",
"txHash": null,
"bankRef": null,
"failedReason": null,
"executedAt": "2026-07-09T05:18:06.400Z",
"createdAt": "2026-07-09T05:18:06.109Z",
"updatedAt": "2026-07-09T05:18:06.109Z",
"feeFlat": 0,
"feePercentage": 0
}
],
"total": 107
}Retrieve a Trade
Returns a single OTC trade by its id.
curl -X GET https://qa.trend.digital/otc-trades/public/ie66geg6ratq8ptaufth8v0w \
-H "x-api-key: YOUR_API_KEY"{
"id": "ie66geg6ratq8ptaufth8v0w",
"from": "USDT",
"fromNetwork": "ETHEREUM",
"to": "EUR",
"toNetwork": null,
"requestedAmount": "10.00",
"lockedQuoteRaw": "0.86999",
"executedQuoteRaw": "0.87002",
"side": "SELL",
"otcTradeStatus": "EXECUTED",
"otcSettlementStatus": "SETTLED",
"filledSettlementAmtRaw": "8.7002",
"txHash": null,
"bankRef": null,
"failedReason": null,
"executedAt": "2026-07-09T05:18:06.400Z",
"createdAt": "2026-07-09T05:18:06.109Z",
"updatedAt": "2026-07-09T05:18:06.109Z",
"feeFlat": 0,
"feePercentage": 0
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 404 | Not found | No trade with that id exists for your account | Check the id returned by the create or list endpoints |
Quote Lifecycle Notes
- Two-step flow. Always call
POST /otc-trades/public/quoteimmediately beforePOST /otc-trades/public/create. Show the quote to your user, then execute promptly. - 20-second validity. A quote and its token expire 20 seconds after issue. After that, execution returns
Quote expired. - Single-use tokens. A
quoteTokenauthorizes exactly one trade. Reusing it does not create a second trade. - Environment-bound tokens. A token issued by Sandbox cannot be used against Production, and vice versa. Always quote and execute against the same base URL.
- Pass the quote back unchanged. Send the exact
priceanddisplayPricefrom the quote to the create endpoint; do not round or substitute them.
Customer Payouts
Send crypto payouts directly to your customers' wallet addresses.
The Customer Payout Object
| Field | Type | Description |
|---|---|---|
| id | string | Unique payout identifier |
| rail | string | Payout rail (CRYPTO) |
| customerReference | string | Your internal customer identifier |
| string | Customer email address | |
| amount | number | Payout amount (e.g. 50 = 50 USDT) |
| currency | string | Cryptocurrency symbol (e.g. USDT) |
| network | string | Blockchain network (e.g. ETHEREUM) |
| destAddress | string | Destination wallet address |
| status | string | Payout status |
| txHash | string | null | On-chain transaction hash (when submitted) |
| note | string | null | Optional note |
| kycStatus | string | Customer KYC status. NOT_REQUIRED if below compliance limit |
Payout Statuses & Fees
| Status | Description |
|---|---|
| PENDING | KYC verification required before processing |
| PROCESSING | Transaction submitted to the blockchain |
| COMPLETED | Transaction confirmed on-chain |
| FAILED | Transaction failed on the blockchain |
Fee Structure
Fees are deducted from the payout amount:
Amount received by customer = Payout Amount - Platform Fee - Network Fee- Platform Fee: Flat amount + percentage (configured per merchant).
- Network Fee: Estimated gas/network fee at time of submission.
If the amount after fees is zero or negative, the payout is rejected.
Create Customer Payout
Creates a new crypto payout. If the amount exceeds your compliance limit, KYC verification is triggered automatically.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| rail | string | Yes | Must be CRYPTO |
| customerReference | string | Yes | Your internal customer identifier |
| string | Yes | Customer email address | |
| amount | number | Yes | Payout amount (e.g. 50 for 50 USDT) |
| currency | string | Yes | Cryptocurrency symbol (e.g. USDT, USDC) |
| network | string | Yes | Blockchain network (e.g. ETHEREUM, TRON) |
| destAddress | string | Yes | Destination wallet address |
| note | string | No | Optional note |
curl -X POST https://qa.trend.digital/customer-payouts \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"rail": "CRYPTO",
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"amount": 50,
"currency": "USDT",
"network": "ETHEREUM",
"destAddress": "0x1234567890abcdef1234567890abcdef12345678",
"note": "Withdrawal request"
}'Response 201 Created: Processing (within compliance limit)
{
"rail": "CRYPTO",
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"amount": 50,
"currency": "USDT",
"destAddress": "0x1234567890abcdef1234567890abcdef12345678",
"status": "PROCESSING",
"txHash": null,
"note": "Withdrawal request"
}Response 201 Created: KYC Required (exceeds limit)
"KYC email has been sent.""Please ask the user to complete the KYC!"Once KYC is approved, call Complete Customer Payout to process the transaction.
Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | Validation failed | Missing or invalid fields | Check the request body against the required fields table |
| 400 | Insufficient amount to be paid after deducting fees and network charges | Amount too low to cover fees | Increase the payout amount |
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
List Customer Payouts
Returns a list of all customer payouts for your merchant account.
curl "https://qa.trend.digital/customer-payouts?page=1&limit=10" \
-H "x-api-key: YOUR_API_KEY"{
"data": [
{
"id": "cpay_x1y2z3a4b5",
"rail": "CRYPTO",
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"amount": 50,
"currency": "USDT",
"network": "ETHEREUM",
"destAddress": "0x1234567890abcdef1234567890abcdef12345678",
"status": "COMPLETED",
"txHash": "0xabc123def456...",
"note": "Withdrawal request",
"kycStatus": "NOT_REQUIRED"
}
],
"total": 1
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Get Customer Payout
Retrieves a single customer payout.
curl "https://qa.trend.digital/customer-payouts/cpay_x1y2z3a4b5" \
-H "x-api-key: YOUR_API_KEY"{
"rail": "CRYPTO",
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"amount": 50,
"currency": "USDT",
"destAddress": "0x1234567890abcdef1234567890abcdef12345678",
"status": "COMPLETED",
"txHash": "0xabc123def456...",
"note": "Withdrawal request"
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | You do not have access to this customer payout | Payout belongs to a different merchant | You can only access your own payouts |
| 404 | Customer Payout does not exist | No payout found with this ID | Check the payout ID |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Complete Customer Payout
Completes a payout that was pending KYC verification. Re-estimates network fees and submits the transaction to the blockchain.
Prerequisites: payout must be in PENDING status, and the customer's KYC must be APPROVED.
curl -X PATCH "https://qa.trend.digital/customer-payouts/cpay_x1y2z3a4b5/complete" \
-H "x-api-key: YOUR_API_KEY"Response 201 Created. Empty response body on success. The payout status transitions to PROCESSING.
Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | You do not have access to this customer payout | Payout belongs to a different merchant | You can only complete your own payouts |
| 404 | Customer Payout does not exist | No payout found with this ID | Check the payout ID |
| 404 | KYC does not exist | Customer KYC not yet approved | Wait for KYC approval, then retry |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
KYC Flow
When a payout exceeds your compliance limit:
- Create Payout: API returns
"KYC email has been sent.". Payout is saved asPENDING. - Customer receives email: contains a link to complete identity verification.
- Customer completes KYC: submits identity documents through the verification portal.
- Webhook notification: you receive a
customer_payout.kyc_updatedwebhook when the status changes. - Complete Payout: once KYC is
APPROVED, call Complete Customer Payout. - Payout confirmed: you receive a
customer_payout.completedwebhook when the transaction is confirmed on-chain.
Customer Compliance
Check KYC (Know Your Customer) verification status for your customers. KYC is required when payout amounts exceed your compliance limit.
The Customer Compliance Object
| Field | Type | Description |
|---|---|---|
| customerReference | string | Your internal customer identifier |
| string | Customer email address | |
| status | string | KYC verification status |
| verifiedAt | string | null | Verification completion timestamp (ISO 8601). null if not yet verified |
| Status | Description |
|---|---|
| PENDING | KYC initiated, awaiting customer submission |
| UNDER_REVIEW | Documents submitted, under review |
| APPROVED | KYC verified, payouts can be processed |
| REJECTED | KYC rejected, customer must re-submit |
| INCOMPLETE | Additional information required |
List Customer Compliance Records
Returns a list of all KYC records for your merchant account.
curl "https://qa.trend.digital/customers?page=1&limit=10" \
-H "x-api-key: YOUR_API_KEY"{
"data": [
{
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"status": "APPROVED",
"verifiedAt": "2025-02-20T12:00:00.000Z"
}
],
"total": 1
}Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Get Customer Compliance by Email
Looks up a specific customer's KYC status by email.
curl "https://qa.trend.digital/customers/email?email=customer@example.com" \
-H "x-api-key: YOUR_API_KEY"{
"customerReference": "CUST_REF_123",
"email": "customer@example.com",
"status": "APPROVED",
"verifiedAt": "2025-02-20T12:00:00.000Z"
}Returns null if the customer has not initiated KYC verification.
Errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Missing API key or authorization token | No x-api-key header | Add the x-api-key header |
| 403 | Invalid! api key | API key is incorrect or revoked | Verify your API key or generate a new one |
| 404 | Customer compliance record not found | No KYC record for this email | Customer hasn't triggered a KYC check yet |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry after a short delay |
Webhooks
Receive real-time notifications when events happen in your account. Webhooks are HTTP POST requests sent to your configured endpoint.
Setup
- Log in to the Merchant Portal (Sandbox or Production).
- Go to Settings.
- Enter your Webhook URL (must be HTTPS).
- A Webhook Secret is auto-generated for signature verification.
- Save your settings.
All webhook payloads use this envelope:
{
"event": "event.name",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": { ... }
}Invoice Events
| Status | Description |
|---|---|
| invoice.created | A new invoice has been created |
| invoice.paid | Customer payment matches the invoice amount |
| invoice.overpaid | Customer sent more than the invoice amount |
| invoice.underpaid | Customer sent less than the invoice amount |
| invoice.expired | Invoice exceeded its validity period |
{
"event": "invoice.paid",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"invoiceId": "inv_a1b2c3d4e5f6",
"status": "PAID",
"amount": "100",
"currency": "USDT"
}
}{
"event": "invoice.expired",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"invoiceId": "inv_a1b2c3d4e5f6",
"status": "EXPIRED",
"amount": "100",
"currency": "USDT"
}
}Customer Payout Events
| Status | Description |
|---|---|
| customer_payout.created | Payout created and being processed |
| customer_payout.processing | Transaction submitted to the blockchain |
| customer_payout.completed | Transaction confirmed on-chain |
| customer_payout.failed | Transaction failed |
| customer_payout.kyc_required | KYC initiated for this payout |
| customer_payout.kyc_updated | Customer KYC status changed |
{
"event": "customer_payout.created",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"payoutId": "cpay_x1y2z3a4b5",
"status": "PROCESSING",
"amount": "50",
"currency": "USDT",
"destAddress": "0x1234...5678",
"customerReference": "CUST_REF_123"
}
}{
"event": "customer_payout.completed",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"payoutId": "cpay_x1y2z3a4b5",
"status": "COMPLETED",
"txHash": "0xabc123def456...",
"email": "customer@example.com",
"customerReference": "CUST_REF_123"
}
}{
"event": "customer_payout.failed",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"payoutId": "cpay_x1y2z3a4b5",
"status": "FAILED",
"amount": "50",
"currency": "USDT",
"destAddress": "0x1234...5678",
"customerReference": "CUST_REF_123"
}
}{
"event": "customer_payout.kyc_required",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"email": "customer@example.com",
"customerReference": "CUST_REF_123",
"amount": "5000",
"currency": "USDT"
}
}{
"event": "customer_payout.kyc_updated",
"timestamp": "2025-02-20T10:30:00.000Z",
"data": {
"email": "customer@example.com",
"customerReference": "CUST_REF_123",
"kycStatus": "APPROVED"
}
}OTC Trade Events
| Status | Description |
|---|---|
| otc_trade.executed | An OTC trade was executed successfully |
When a trade executes, Trend Digital sends an otc_trade.executed webhook to your configured endpoint. Payloads follow the standard envelope.
{
"event": "otc_trade.executed",
"timestamp": "2026-02-20T10:30:00.000Z",
"data": {
"tradeId": "ie66geg6ratq8ptaufth8v0w",
"status": "EXECUTED",
"side": "SELL",
"pair": "USDT/EUR",
"amount": "10",
"executedPrice": "0.87002"
}
}| Field | Type | Description |
|---|---|---|
| tradeId | string | The executed trade's identifier |
| status | string | Always EXECUTED for this event |
| side | string | SELL or BUY |
| pair | string | The traded pair, formatted FROM/TO |
| amount | string | Amount of the from currency traded |
| executedPrice | string | null | Price the trade filled at |
Signature Verification
Every webhook includes an x-webhook-signature header (HMAC-SHA256). Always verify the signature before processing events.
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = JSON.stringify(req.body);
if (!verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
switch (event) {
case 'invoice.paid': break;
case 'customer_payout.completed': break;
case 'customer_payout.failed': break;
case 'customer_payout.kyc_updated': break;
}
res.status(200).send('OK');
});import hmac
import hashlib
def verify_webhook(raw_body: str, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('x-webhook-signature')
raw_body = request.get_data(as_text=True)
if not verify_webhook(raw_body, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.json
match event['event']:
case 'invoice.paid':
pass
case 'customer_payout.completed':
pass
case 'customer_payout.failed':
pass
return 'OK', 200Retry Policy & Best Practices
If your endpoint does not respond with 2xx within 10 seconds, the webhook is retried:
| Status | Description |
|---|---|
| 1st | Immediate |
| 2nd | After 2 seconds |
| 3rd | After 10 seconds |
After 3 failed attempts, the webhook is dropped.
- Respond quickly: return 200 within 10 seconds. Process events asynchronously if needed.
- Verify signatures: always validate
x-webhook-signaturebefore processing. - Handle duplicates: use
invoiceIdorpayoutIdto deduplicate. Retries may deliver the same event more than once. - Use HTTPS: required in production.
- Log events: store all received webhooks for debugging.
Errors
The API returns errors in a consistent JSON format.
Error Response Format
{
"message": "A human-readable error description",
"errors": "Error type"
}For validation errors, errors is an array:
{
"message": "Validation failed",
"errors": [
"email must be an email",
"currency should not be empty",
"amount must be a string"
]
}HTTP Status Codes
| Status | Description |
|---|---|
| 200 | OK, request succeeded |
| 201 | Created: resource created successfully |
| 400 | Bad Request, invalid request body or parameters |
| 401 | Unauthorized, missing or invalid API key |
| 403 | Forbidden, valid API key but no access to the resource |
| 404 | Not Found, resource does not exist |
| 429 | Too Many Requests, rate limit exceeded |
| 500 | Internal Server Error, something went wrong on our end |
Common Errors
Missing API Key
{
"message": "Missing API key or authorization token",
"errors": "Missing API key or authorization token"
}Fix: Add the x-api-key header to your request.
Invalid API Key
{
"message": "Invalid! api key",
"errors": "Invalid! api key"
}Fix: Verify your API key. If you regenerated it from the Merchant Portal, the previous key is revoked.
Validation Error
{
"message": "Validation failed",
"errors": ["email must be an email", "amount must be a string"]
}Fix: Check the request body against the endpoint's required fields. Amounts must be strings in requests.
Resource Not Found
{
"message": "Invoice does not exist",
"errors": "Not Found"
}Fix: Verify the resource ID in your request.
Access Denied
{
"message": "You do not have access to this invoice",
"errors": "Forbidden"
}Fix: You can only access resources under your own merchant account.
Insufficient Payout Amount
{
"message": "Insufficient amount to be paid after deducting fees and network charges",
"errors": "Bad Request"
}Fix: Increase the payout amount to cover platform + network fees.
KYC Not Approved
{
"message": "KYC does not exist",
"errors": "Not Found"
}Fix: The customer's KYC must be APPROVED before completing a payout. Check via Get Customer Compliance by Email.
Internal Server Error
{
"message": "Internal Server Error",
"errors": ["Internal Server Error"]
}Fix: Retry after a short delay. If it persists, contact support with the timestamp and endpoint details.
Rate Limiting
Requests are limited to 100 per minute per API key. If you hit rate limits consistently, consider using webhooks instead of polling.
{
"message": "ThrottlerException: Too Many Requests",
"errors": "Too Many Requests"
}Need help integrating?
Our team can walk you through sandbox setup, webhooks, and going live. Contact us to get started.