Developers

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.

Chapter 1

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.

SandboxProduction
Base URLhttps://qa.trend.digitalhttps://api.trend.digital
Merchant Portalmerchant-qa.trend.digitalmerchant.trend.digital
NetworkTestnets (no real funds)Mainnets
Chapter 2

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

AssetNetworkContract address
USDTEthereum (Sepolia testnet)0xb60B42C095d08776a6ecc9a246180FE37AbA02A5
USDTTron (Shasta testnet)TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs
USDCEthereum (Sepolia testnet)0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238

Production

AssetNetworkContract address
USDTEthereum0xdAC17F958D2ee523a2206206994597C13D831ec7
USDTTronTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
USDCEthereum0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Chapter 3

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.

Request
curl https://qa.trend.digital/invoices \
  -H "x-api-key: YOUR_API_KEY"

Getting Your API Key

  1. Log in to the Merchant Portal (Sandbox or Production).
  2. Go to Settings > API Keys.
  3. Click Create API Key.
  4. 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.

FieldTypeDescription
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

FieldTypeDescription
x-api-keyYour API keyEvery request
Content-Typeapplication/jsonPOST and PATCH requests

Example Requests

Sandbox
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"
  }'
Production
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.
Chapter 4

Invoices

Create and manage payment invoices. When a customer pays an invoice, the funds are settled in your chosen cryptocurrency.

The Invoice Object

FieldTypeDescription
idstringUnique invoice identifier
merchantIdstringMerchant account identifier
emailstringCustomer email address
namestring | nullCustomer name
mobilestring | nullCustomer phone number
addressstring | nullCustomer address
statestring | nullCustomer state/province
countrystring | nullCustomer country
amountnumberInvoice amount (e.g. 100 = 100 USDT)
descstringInvoice description
referencestring | nullYour internal reference ID
discountnumber | nullDiscount value applied
discountTypestring | nullPERCENTAGE or FLAT
itemsarray | nullLine items (each has id, name, qty, price)
dueDatestring | nullDue date (ISO 8601)
invoiceTypestringNORMAL_INVOICE or OPEN_INVOICE
statusstringCurrent status
validTillstring | nullExpiration date (ISO 8601)
merchantFeenumber | nullCustom merchant fee value
merchantFeeTypestring | nullPERCENTAGE or FLAT
paymentUrlstringPayment page URL for the customer
createdAtstringCreation timestamp (ISO 8601)
updatedAtstringLast update timestamp (ISO 8601)

Invoice Statuses & Types

StatusDescription
UNPAIDInvoice created, awaiting payment
PAIDCustomer sent the exact amount
OVERPAIDCustomer sent more than the invoice amount
UNDERPAIDCustomer sent less than the invoice amount
EXPIREDInvoice exceeded its validity period without full payment
CANCELLEDInvoice cancelled via API
StatusDescription
NORMAL_INVOICEFixed amount, customer must pay the exact amount
OPEN_INVOICEOpen amount, customer chooses how much to pay

Create Invoice

POST/invoices

Creates a new payment invoice in UNPAID status.

Request Body

FieldTypeRequiredDescription
emailstringYesCustomer email address
namestringNoCustomer name
mobilestringNoCustomer phone number
addressstringNoCustomer address
statestringNoCustomer state/province
countrystringNoCustomer country
currencystringYesSettlement currency symbol (e.g. USDT, USDC)
networkstringNoBlockchain network (e.g. ETHEREUM, TRON). If omitted, the default network for the currency is used
amountstringYesInvoice amount as a string (e.g. "100" for 100 USDT)
descstringYesDescription shown to the customer
invoiceTypestringYesNORMAL_INVOICE or OPEN_INVOICE
referencestringNoYour internal reference ID
dueDatestringNoDue date (ISO 8601). Defaults to your configured invoice duration
discountnumberNoDiscount value
discountTypestringNoPERCENTAGE or FLAT. Required if discount is provided
itemsarrayNoLine items. Each item: id (number), name (string), qty (number), price (number)
validTillstringNoExpiration date (ISO 8601)
merchantFeenumberNoCustom merchant fee override
merchantFeeTypestringNoPERCENTAGE or FLAT. Required if merchantFee is provided
Request
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"
  }'
Response 201 Created
{
  "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

StatusErrorCauseFix
400Validation failedMissing or invalid fieldsCheck the request body against the required fields table
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
429Too Many RequestsRate limit exceededWait and retry after a short delay

List Invoices

GET/invoices

Returns a list of invoices for your merchant account.

Query Parameters

FieldTypeDescription
pageintegerPage number (default 1)
limitintegerItems per page (default 10)
sortstringSort order (e.g. createdAt.desc)
filter[status]stringUNPAID, PAID, OVERPAID, UNDERPAID, EXPIRED, CANCELLED
filter[email]stringFilter by customer email
Request
curl "https://qa.trend.digital/invoices?page=1&limit=10&filter[status]=PAID&sort=createdAt.desc" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
429Too Many RequestsRate limit exceededWait and retry after a short delay

Get Invoice Details

GET/invoices/:id/details

Retrieves full invoice details including settlement currency, individual on-chain payments, and refund history.

Request
curl "https://qa.trend.digital/invoices/inv_a1b2c3d4e5f6/details" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

FieldTypeDescription
idstringPayment identifier
expectedAmtnumberExpected payment amount
paidAmtnumberActual amount paid
txHashstringOn-chain transaction hash
statusstringPayment status (e.g. CONFIRMED)
paidAtstringPayment timestamp (ISO 8601)

refunds[]: refund records (or null)

FieldTypeDescription
idstringRefund identifier
reqAmountnumberRequested refund amount
refundTypestringPARTIAL or FULL
destAddressstringDestination wallet for refund
customerEmailstringCustomer email
remarksstring | nullRefund reason
refundStatusstringPENDING, COMPLETED, or FAILED
txHashstring | nullOn-chain transaction hash (when completed)
processedAtstring | nullProcessing timestamp (ISO 8601)
createdAtstringCreation timestamp (ISO 8601)

Errors

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403You do not have access to this invoiceInvoice belongs to a different merchantYou can only access your own invoices
404Invoice does not existNo invoice found with this IDCheck the invoice ID
429Too Many RequestsRate limit exceededWait and retry after a short delay

Cancel Invoice

PATCH/invoices/:id

Cancels an unpaid invoice. Only invoices in UNPAID status can be cancelled.

Request Body

FieldTypeRequiredDescription
statusstringYesMust be CANCELLED
Request
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"}'
Response 200 OK
{
  "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

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403You do not have access to this invoiceInvoice belongs to a different merchantYou can only cancel your own invoices
404Invoice does not existNo invoice found with this IDCheck the invoice ID
429Too Many RequestsRate limit exceededWait and retry after a short delay
Chapter 5

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

FieldTypeDescription
idstringUnique trade identifier
fromstringCurrency code being sold from (e.g. USDT)
fromNetworkstring | nullNetwork of the from currency (e.g. TRON, ETHEREUM); null for fiat
tostringCurrency code being received (e.g. USD)
toNetworkstring | nullNetwork of the to currency; null for fiat
requestedAmountstringAmount of the from currency submitted for the trade
lockedQuoteRawstringFull-precision price locked at quote time
executedQuoteRawstringFull-precision price the trade actually filled at
sidestringSELL or BUY
otcTradeStatusstringTrade status (see below)
otcSettlementStatusstringSettlement status (see below)
filledSettlementAmtRawstringFull-precision settled amount in the to currency
txHashstring | nullOn-chain transaction hash, when applicable
bankRefstring | nullBank reference, when applicable
failedReasonstring | nullReason the trade failed, if otcTradeStatus is FAILED
executedAtstring | nullISO 8601 timestamp of execution
createdAtstringISO 8601 timestamp the trade was created
updatedAtstringISO 8601 timestamp the trade was last updated
feeFlatnumberFlat fee applied to the trade
feePercentagenumberPercentage fee applied to the trade

Trade & Settlement Statuses

Trade statuses (otcTradeStatus)

StatusDescription
PENDINGTrade recorded, execution in progress
EXECUTEDTrade filled successfully
FAILEDTrade did not fill; see failedReason
CANCELLEDTrade was cancelled

Settlement statuses (otcSettlementStatus)

StatusDescription
PENDINGAwaiting settlement
SETTLEDFully settled
PARTIALLY_SETTLEDPartially settled
PARTIALLY_FAILEDSettlement partially failed
FAILEDSettlement failed

List Available Pairs

GET/otc-trades/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.

Request
curl -X GET https://qa.trend.digital/otc-trades/pairs \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
[
  { "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

POST/otc-trades/public/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

FieldTypeRequiredDescription
fromstringYesCurrency code to trade from (e.g. USDT)
tostringYesCurrency code to receive (e.g. USD)
fromNetworkstringNoRequired when from exists on more than one network
toNetworkstringNoRequired when to exists on more than one network
amountnumberYesAmount of the from currency to trade
sidestringYesSELL or BUY
Request
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"
  }'
Response 200 OK
{
  "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

FieldTypeDescription
pricenumberFull-precision locked price. Pass this back to the create endpoint unchanged
displayPricenumberRounded price for display purposes
expiresAtnumberUnix epoch (ms) when the quote and its token expire
quoteTokenstringSingle-use token that authorizes execution of this quote
totalnumberTotal in the to currency for the requested amount
currencystringCurrency of total
feeFlatstringFlat fee. Only present when fee visibility is enabled for your account
feePercentagestringPercentage fee. Only present when fee visibility is enabled for your account
feeCurrencystringCurrency 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

StatusErrorCauseFix
400This currency pair is not available for OTC tradingThe from/to/network combination is not an enabled pair, or a network is needed to disambiguateUse a combination returned by GET /otc-trades/pairs, including fromNetwork where required

Execute a Trade

POST/otc-trades/public/create

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

FieldTypeRequiredDescription
fromstringYesSame value used in the quote
tostringYesSame value used in the quote
fromNetworkstringNoSame value used in the quote
toNetworkstringNoSame value used in the quote
amountnumberYesSame value used in the quote
sidestringYesSame value used in the quote
pricenumberYesThe exact price from the quote response
displayPricenumberYesThe exact displayPrice from the quote response
quoteTokenstringYesThe quoteToken from the quote response
Request
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"
  }'
Response 200 OK
Trade Executed

Errors

StatusErrorCauseFix
400Invalid quote tokenThe token was not issued by this environment, is malformed, or has been alteredRequest a fresh quote from the same environment and use its token unchanged
400Quote expired, please request a new quoteMore than 20 seconds passed since the quote was issuedRequest a new quote and execute within 20 seconds
400LP order failedThe liquidity provider rejected or could not fill the orderRequest a new quote and retry
409DuplicateThe quoteToken was already used to execute a tradeDo not reuse a token; each quote authorizes a single trade

List Trades

GET/otc-trades/public

Returns your OTC trades, most recent first, with pagination.

Query Parameters

FieldTypeDescription
pagenumberPage number (default 1)
limitnumberPage size (default 20)
Request
curl -X GET "https://qa.trend.digital/otc-trades/public?page=1&limit=20" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

GET/otc-trades/public/:id

Returns a single OTC trade by its id.

Request
curl -X GET https://qa.trend.digital/otc-trades/public/ie66geg6ratq8ptaufth8v0w \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

StatusErrorCauseFix
404Not foundNo trade with that id exists for your accountCheck the id returned by the create or list endpoints

Quote Lifecycle Notes

  • Two-step flow. Always call POST /otc-trades/public/quote immediately before POST /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 quoteToken authorizes 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 price and displayPrice from the quote to the create endpoint; do not round or substitute them.
Chapter 6

Customer Payouts

Send crypto payouts directly to your customers' wallet addresses.

The Customer Payout Object

FieldTypeDescription
idstringUnique payout identifier
railstringPayout rail (CRYPTO)
customerReferencestringYour internal customer identifier
emailstringCustomer email address
amountnumberPayout amount (e.g. 50 = 50 USDT)
currencystringCryptocurrency symbol (e.g. USDT)
networkstringBlockchain network (e.g. ETHEREUM)
destAddressstringDestination wallet address
statusstringPayout status
txHashstring | nullOn-chain transaction hash (when submitted)
notestring | nullOptional note
kycStatusstringCustomer KYC status. NOT_REQUIRED if below compliance limit

Payout Statuses & Fees

StatusDescription
PENDINGKYC verification required before processing
PROCESSINGTransaction submitted to the blockchain
COMPLETEDTransaction confirmed on-chain
FAILEDTransaction failed on the blockchain

Fee Structure

Fees are deducted from the payout amount:

Formula
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

POST/customer-payouts

Creates a new crypto payout. If the amount exceeds your compliance limit, KYC verification is triggered automatically.

Request Body

FieldTypeRequiredDescription
railstringYesMust be CRYPTO
customerReferencestringYesYour internal customer identifier
emailstringYesCustomer email address
amountnumberYesPayout amount (e.g. 50 for 50 USDT)
currencystringYesCryptocurrency symbol (e.g. USDT, USDC)
networkstringYesBlockchain network (e.g. ETHEREUM, TRON)
destAddressstringYesDestination wallet address
notestringNoOptional note
Request
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)

Response 201 Created
{
  "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)

Response (new KYC)
"KYC email has been sent."
Response (KYC already pending)
"Please ask the user to complete the KYC!"

Once KYC is approved, call Complete Customer Payout to process the transaction.

Errors

StatusErrorCauseFix
400Validation failedMissing or invalid fieldsCheck the request body against the required fields table
400Insufficient amount to be paid after deducting fees and network chargesAmount too low to cover feesIncrease the payout amount
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
429Too Many RequestsRate limit exceededWait and retry after a short delay

List Customer Payouts

GET/customer-payouts

Returns a list of all customer payouts for your merchant account.

Request
curl "https://qa.trend.digital/customer-payouts?page=1&limit=10" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
429Too Many RequestsRate limit exceededWait and retry after a short delay

Get Customer Payout

GET/customer-payouts/:id

Retrieves a single customer payout.

Request
curl "https://qa.trend.digital/customer-payouts/cpay_x1y2z3a4b5" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "rail": "CRYPTO",
  "customerReference": "CUST_REF_123",
  "email": "customer@example.com",
  "amount": 50,
  "currency": "USDT",
  "destAddress": "0x1234567890abcdef1234567890abcdef12345678",
  "status": "COMPLETED",
  "txHash": "0xabc123def456...",
  "note": "Withdrawal request"
}

Errors

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403You do not have access to this customer payoutPayout belongs to a different merchantYou can only access your own payouts
404Customer Payout does not existNo payout found with this IDCheck the payout ID
429Too Many RequestsRate limit exceededWait and retry after a short delay

Complete Customer Payout

PATCH/customer-payouts/:id/complete

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.

Request
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

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403You do not have access to this customer payoutPayout belongs to a different merchantYou can only complete your own payouts
404Customer Payout does not existNo payout found with this IDCheck the payout ID
404KYC does not existCustomer KYC not yet approvedWait for KYC approval, then retry
429Too Many RequestsRate limit exceededWait and retry after a short delay

KYC Flow

When a payout exceeds your compliance limit:

  1. Create Payout: API returns "KYC email has been sent.". Payout is saved as PENDING.
  2. Customer receives email: contains a link to complete identity verification.
  3. Customer completes KYC: submits identity documents through the verification portal.
  4. Webhook notification: you receive a customer_payout.kyc_updated webhook when the status changes.
  5. Complete Payout: once KYC is APPROVED, call Complete Customer Payout.
  6. Payout confirmed: you receive a customer_payout.completed webhook when the transaction is confirmed on-chain.
Chapter 7

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

FieldTypeDescription
customerReferencestringYour internal customer identifier
emailstringCustomer email address
statusstringKYC verification status
verifiedAtstring | nullVerification completion timestamp (ISO 8601). null if not yet verified
StatusDescription
PENDINGKYC initiated, awaiting customer submission
UNDER_REVIEWDocuments submitted, under review
APPROVEDKYC verified, payouts can be processed
REJECTEDKYC rejected, customer must re-submit
INCOMPLETEAdditional information required

List Customer Compliance Records

GET/customers

Returns a list of all KYC records for your merchant account.

Request
curl "https://qa.trend.digital/customers?page=1&limit=10" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "data": [
    {
      "customerReference": "CUST_REF_123",
      "email": "customer@example.com",
      "status": "APPROVED",
      "verifiedAt": "2025-02-20T12:00:00.000Z"
    }
  ],
  "total": 1
}

Errors

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
429Too Many RequestsRate limit exceededWait and retry after a short delay

Get Customer Compliance by Email

GET/customers/email?email=customer@example.com

Looks up a specific customer's KYC status by email.

Request
curl "https://qa.trend.digital/customers/email?email=customer@example.com" \
  -H "x-api-key: YOUR_API_KEY"
Response 200 OK
{
  "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

StatusErrorCauseFix
401Missing API key or authorization tokenNo x-api-key headerAdd the x-api-key header
403Invalid! api keyAPI key is incorrect or revokedVerify your API key or generate a new one
404Customer compliance record not foundNo KYC record for this emailCustomer hasn't triggered a KYC check yet
429Too Many RequestsRate limit exceededWait and retry after a short delay
Chapter 8

Webhooks

Receive real-time notifications when events happen in your account. Webhooks are HTTP POST requests sent to your configured endpoint.

Setup

  1. Log in to the Merchant Portal (Sandbox or Production).
  2. Go to Settings.
  3. Enter your Webhook URL (must be HTTPS).
  4. A Webhook Secret is auto-generated for signature verification.
  5. Save your settings.

All webhook payloads use this envelope:

Envelope
{
  "event": "event.name",
  "timestamp": "2025-02-20T10:30:00.000Z",
  "data": { ... }
}

Invoice Events

StatusDescription
invoice.createdA new invoice has been created
invoice.paidCustomer payment matches the invoice amount
invoice.overpaidCustomer sent more than the invoice amount
invoice.underpaidCustomer sent less than the invoice amount
invoice.expiredInvoice exceeded its validity period
invoice.paid / invoice.overpaid / invoice.underpaid
{
  "event": "invoice.paid",
  "timestamp": "2025-02-20T10:30:00.000Z",
  "data": {
    "invoiceId": "inv_a1b2c3d4e5f6",
    "status": "PAID",
    "amount": "100",
    "currency": "USDT"
  }
}
invoice.expired
{
  "event": "invoice.expired",
  "timestamp": "2025-02-20T10:30:00.000Z",
  "data": {
    "invoiceId": "inv_a1b2c3d4e5f6",
    "status": "EXPIRED",
    "amount": "100",
    "currency": "USDT"
  }
}

Customer Payout Events

StatusDescription
customer_payout.createdPayout created and being processed
customer_payout.processingTransaction submitted to the blockchain
customer_payout.completedTransaction confirmed on-chain
customer_payout.failedTransaction failed
customer_payout.kyc_requiredKYC initiated for this payout
customer_payout.kyc_updatedCustomer KYC status changed
customer_payout.created / customer_payout.processing
{
  "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"
  }
}
customer_payout.completed
{
  "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"
  }
}
customer_payout.failed
{
  "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"
  }
}
customer_payout.kyc_required
{
  "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"
  }
}
customer_payout.kyc_updated
{
  "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

StatusDescription
otc_trade.executedAn 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.

otc_trade.executed
{
  "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"
  }
}
FieldTypeDescription
tradeIdstringThe executed trade's identifier
statusstringAlways EXECUTED for this event
sidestringSELL or BUY
pairstringThe traded pair, formatted FROM/TO
amountstringAmount of the from currency traded
executedPricestring | nullPrice the trade filled at

Signature Verification

Every webhook includes an x-webhook-signature header (HMAC-SHA256). Always verify the signature before processing events.

Node.js
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');
});
Python
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', 200

Retry Policy & Best Practices

If your endpoint does not respond with 2xx within 10 seconds, the webhook is retried:

StatusDescription
1stImmediate
2ndAfter 2 seconds
3rdAfter 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-signature before processing.
  • Handle duplicates: use invoiceId or payoutId to deduplicate. Retries may deliver the same event more than once.
  • Use HTTPS: required in production.
  • Log events: store all received webhooks for debugging.
Chapter 9

Errors

The API returns errors in a consistent JSON format.

Error Response Format

Standard error
{
  "message": "A human-readable error description",
  "errors": "Error type"
}

For validation errors, errors is an array:

Validation error
{
  "message": "Validation failed",
  "errors": [
    "email must be an email",
    "currency should not be empty",
    "amount must be a string"
  ]
}

HTTP Status Codes

StatusDescription
200OK, request succeeded
201Created: resource created successfully
400Bad Request, invalid request body or parameters
401Unauthorized, missing or invalid API key
403Forbidden, valid API key but no access to the resource
404Not Found, resource does not exist
429Too Many Requests, rate limit exceeded
500Internal Server Error, something went wrong on our end

Common Errors

Missing API Key

401 Unauthorized
{
  "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

403 Forbidden
{
  "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

400 Bad Request
{
  "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

404 Not Found
{
  "message": "Invoice does not exist",
  "errors": "Not Found"
}

Fix: Verify the resource ID in your request.

Access Denied

403 Forbidden
{
  "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

400 Bad Request
{
  "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

404 Not Found
{
  "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

500 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.

429 Too Many Requests
{
  "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.