DocsIntroduction

ZyndPay API Reference

API v1 · USDT TRC20 on TRON

The ZyndPay API is a RESTful payment gateway for accepting USDT on the TRON blockchain. Resource-oriented URLs, JSON bodies, and a consistent response envelope make it easy to integrate in any environment.

All amounts are in USDT (TRC20). The minimum payment is 1 USDT. Merchant fee: 1% per transaction. Customers pay a $1.00 network fee on external payments. Withdrawal fee: 1% (min $1.50). Payout fee: 1% (min $1.50, max $5).

Base URLhttps://api.zyndpay.io/v1
json
{
  "success": true,
  "data": { ... },
  "meta": { "page": 1, "limit": 20, "total": 100 }
}

Authentication

Include your API key in the X-Api-Key header on every request. Alternatively, pass it as a Bearer token in the Authorization header.

bash
curl https://api.zyndpay.io/v1/payments \
  -H "X-Api-Key: zyp_live_sk_..."
ParameterTypeRequiredDescription
live_sk_*stringrequiredLive secret key — full API access. Never expose client-side.
live_pk_*stringoptionalLive publishable key — limited read access, safe for browsers.
test_sk_*stringrequiredSandbox secret key — identical to live but no real transactions.
test_pk_*stringoptionalSandbox publishable key.
rk_*stringoptionalRestricted key — scoped to specific operations only.
Warning
docs.authWarning

Quick Start

Accept your first USDT payment in under 5 minutes. Select your language below — each example shows install, create a pay-in, handle the webhook, and check status.

typescript
// 1. Install
// npm install @zyndpay/sdk

// 2. Initialize
import { ZyndPay } from '@zyndpay/sdk';

const zyndpay = new ZyndPay({
  apiKey: process.env.ZYNDPAY_API_KEY!,
  webhookSecret: process.env.ZYNDPAY_WEBHOOK_SECRET,
});

// 3. Create a pay-in and get a payment URL
const payin = await zyndpay.payins.create({
  amount: '100',
  externalRef: 'order_' + Date.now(), // your order ID
  successUrl: 'https://yoursite.com/success',
  cancelUrl: 'https://yoursite.com/cancel',
});
console.log('Redirect customer to:', payin.paymentUrl);
// Or show payin.address if you want to display the TRON address directly

// 4. Handle the webhook (Express.js)
import express from 'express';
const app = express();

app.post('/webhooks/zyndpay', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-zyndpay-signature'] as string;

  let event;
  try {
    event = zyndpay.webhooks.verify(req.body, signature);
  } catch {
    return res.status(400).send('Invalid signature');
  }

  switch (event.event) {
    case 'payin.confirmed':
      // Payment received — fulfill the order
      console.log('Paid:', event.data.externalRef, event.data.amount, 'USDT');
      break;
    case 'payin.expired':
      console.log('Expired:', event.data.externalRef);
      break;
    case 'payin.underpaid':
      console.log('Underpaid:', event.data.externalRef,
        'got', event.data.amount, 'expected', event.data.amountRequested);
      break;
  }

  res.json({ received: true });
});

// 5. Poll / check status programmatically
const tx = await zyndpay.payins.get(payin.transactionId);
console.log('Status:', tx.status); // "CONFIRMED"
Tip
Not ready for real funds? Use a zyp_test_sk_... sandbox key and add sandbox: true to the payin request. Then call POST /v1/sandbox/payments/:id/simulate to instantly confirm it — no blockchain needed.

Going Live

Before accepting real payments, work through this checklist. Most items take under 5 minutes each.

1
Complete KYB
Submit your business documents in the dashboard. The freemium tier allows up to $14,500/month — KYB unlocks higher limits.
2
Switch to live API keys
Replace your zyp_test_sk_... keys with zyp_live_sk_... keys. Live keys create real on-chain transactions.
3
Register a webhook endpoint
Add your production webhook URL in the dashboard and store the webhook secret in your environment variables.
4
Test end-to-end in production
Send a real $1 USDT payment through your integration. Verify the webhook fires and your order logic executes correctly.
5
Set up error monitoring
Subscribe to webhook delivery failure alerts in the dashboard. Implement retry logic for failed webhook processing.
6
Review rate limits
Default: 30 payins/min, 10 payouts/min, 5 withdrawals/min. Contact [email protected] if you need higher limits.
Tip
Start with a single test transaction using a real live key before announcing to customers — this catches environment variable issues that sandbox testing can't reveal.

Payins

Create a Payin

POST/v1/payments

docs.createPayinP

ParameterTypeRequiredDescription
amountstringrequiredAmount in USDT as a decimal string (e.g. "100.00"). Minimum: "1.00".
externalRefstringoptionalYour order or reference ID (optional). Must be unique per merchant if provided.
expiresInSecondsnumberoptionalSeconds until the address expires. Minimum: 900. Default: 1800 (30 minutes).
successUrlstringoptionalURL to redirect the customer after a successful payment.
cancelUrlstringoptionalURL to redirect the customer if the payment expires or is cancelled.
metadataobjectoptionalArbitrary key-value pairs stored with the payment and included in webhooks.
bash
curl -X POST https://api.zyndpay.io/v1/payments \
  -H "X-Api-Key: zyp_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100.00",
    "externalRef": "order_123",
    "expiresInSeconds": 3600,
    "successUrl": "https://yoursite.com/payment/success",
    "cancelUrl": "https://yoursite.com/payment/cancel"
  }'
json
{
  "success": true,
  "data": {
    "transactionId": "pay_abc123...",
    "address": "TRXabc123def456ghi789jkl",
    "paymentUrl": "https://checkout.zyndpay.io/pay_abc123...",
    "qrCodeUrl": "data:image/png;base64,iVBOR...",
    "amount": "100.00",
    "amountExpected": "101.00",
    "networkFee": "1",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "status": "AWAITING_PAYMENT",
    "expiresAt": "2026-03-06T12:00:00.000Z"
  }
}

Get a Payin

GET/v1/payments/:id
json
{
  "success": true,
  "data": {
    "id": "f4b2cb0f-08ce-4408-88d4-0a678ca0aae2",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100",
    "amountReceived": "100.00",
    "zyndpayFee": "1.00",
    "status": "CONFIRMED",
    "txHash": "abc123def456...",
    "externalRef": "order_123",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "paymentUrl": "https://checkout.zyndpay.io/f4b2cb0f...",
    "isSandbox": false,
    "createdAt": "2026-03-06T11:00:00.000Z",
    "updatedAt": "2026-03-06T11:01:02.000Z"
  }
}
PENDINGAwaiting payment
CONFIRMINGFunds received, waiting for 5 confirmations
CONFIRMED5+ confirmations — balance credited
UNDERPAIDLess than expected received
OVERPAIDMore than expected received
EXPIREDAddress expired without payment

List Payins

GET/v1/payments
ParameterTypeRequiredDescription
pagenumberoptionalPage number. Default: 1.
limitnumberoptionalResults per page. Default: 20. Max: 100.
statusstringoptionalFilter by status: PENDING, CONFIRMING, CONFIRMED, EXPIRED, UNDERPAID, or OVERPAID.
currencystringoptionalFilter by currency (e.g. USDT_TRC20).
json
{
  "success": true,
  "data": {
    "items": [ ... ],
    "meta": {
      "page": 1,
      "limit": 20,
      "total": 134,
      "totalPages": 7,
      "hasNext": true,
      "hasPrev": false
    }
  }
}

Payment Links
POST/v1/paylinks

docs.createPaylinkP

ParameterTypeRequiredDescription
typestringrequiredPayment link type: FIXED (default), VARIABLE, or RECURRING.
productsarrayrequiredArray of products. At least one product is required.
products[].namestringrequiredProduct name displayed to the customer.
products[].pricestringrequiredPrice in USDT as a decimal string.
products[].productTypestringrequiredPHYSICAL (default) or DIGITAL.
products[].digitalUrlstringoptionalExternal URL for digital product delivery (Google Drive, Mega, etc.).
collectEmailstringoptionalCustomer email collection: HIDDEN (default), OPTIONAL, or REQUIRED.
collectNamestringoptionalCustomer name collection: HIDDEN, OPTIONAL, or REQUIRED.
brandColorstringoptionalHex color for branded checkout page (e.g. #635BFF).
successUrlstringoptionalURL to redirect the customer after a successful payment.
recurringIntervalstringoptionalBilling interval for recurring paylinks: WEEKLY, MONTHLY, or YEARLY.
bash
curl -X POST https://api.zyndpay.io/v1/paylinks \
  -H "X-Api-Key: zyp_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "FIXED",
    "products": [
      {
        "name": "Premium Course",
        "price": "49.99",
        "productType": "DIGITAL",
        "digitalUrl": "https://drive.google.com/file/d/abc123"
      }
    ],
    "collectEmail": "REQUIRED",
    "brandColor": "#635BFF"
  }'
json
{
  "success": true,
  "data": {
    "id": "pl_cma1xyz8f0001yx5k",
    "slug": "aB3dE6fG7hI9",
    "type": "FIXED",
    "status": "ACTIVE",
    "paymentUrl": "https://dashboard.zyndpay.io/pay/link/aB3dE6fG7hI9",
    "products": [
      {
        "id": "prod_abc123",
        "name": "Premium Course",
        "price": "49.990000000000000000",
        "productType": "DIGITAL",
        "digitalUrl": "https://drive.google.com/file/d/abc123"
      }
    ],
    "createdAt": "2026-03-08T10:00:00.000Z"
  }
}
GET/v1/paylinks/:id
json
{
  "success": true,
  "data": {
    "id": "pl_cma1xyz8f0001yx5k",
    "slug": "aB3dE6fG7hI9",
    "type": "FIXED",
    "status": "ACTIVE",
    "paymentUrl": "https://dashboard.zyndpay.io/pay/link/aB3dE6fG7hI9",
    "products": [
      {
        "id": "prod_abc123",
        "name": "Premium Course",
        "price": "49.990000000000000000",
        "productType": "DIGITAL",
        "digitalUrl": "https://drive.google.com/file/d/abc123"
      }
    ],
    "createdAt": "2026-03-08T10:00:00.000Z"
  }
}
GET/v1/paylinks
ParameterTypeRequiredDescription
pagenumberoptionalPage number. Default: 1.
limitnumberoptionalResults per page. Default: 20. Max: 100.
json
{
  "success": true,
  "data": {
    "items": [ ... ],
    "meta": {
      "page": 1,
      "limit": 20,
      "total": 134,
      "totalPages": 7,
      "hasNext": true,
      "hasPrev": false
    }
  }
}
POST/v1/pay/link/:slug/checkout

Public endpoint (no auth required). Creates an order and payin from a payment link. The customer receives a TRON deposit address to send USDT to.

ParameterTypeRequiredDescription
itemsarrayrequiredArray of product IDs and quantities to purchase.
customerEmailstringoptionalCustomer email address (required for RECURRING and digital products).
customerNamestringoptionalCustomer name (optional).
bash
curl -X POST https://api.zyndpay.io/v1/pay/link/aB3dE6fG7hI9/checkout \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "productId": "prod_abc123", "quantity": 1 }
    ],
    "customerEmail": "[email protected]",
    "customerName": "John Doe"
  }'
json
{
  "success": true,
  "data": {
    "orderId": "ord_xyz789",
    "transactionId": "cma2abc9g0002yz6l0def",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "49.99",
    "expiresAt": "2026-03-08T11:00:00.000Z"
  }
}

Payouts

Create a Payout

POST/v1/payout

docs.createPayoutP

Note
docs.payoutVsWithdrawalNote
ParameterTypeRequiredDescription
amountstringrequiredAmount in USDT to send (e.g. "50.00"). Minimum: "5.00". A fee of 1% (min $1.50, max $5) is deducted from your balance on top of the payout amount.
destinationAddressstringrequiredTRON (TRC20) wallet address of the recipient. Must match the format T followed by 33 base58 characters.
currencystringoptionalCurrency to send. Default: USDT_TRC20.
chainstringoptionalBlockchain network. Default: TRON.
externalRefstringoptionalYour internal reference ID for this payout (e.g. vendor invoice number).
metadataobjectoptionalArbitrary key-value pairs stored with the payout.
bash
curl -X POST https://api.zyndpay.io/v1/payout \
  -H "X-Api-Key: zyp_live_sk_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "amount": "50.00",
    "destinationAddress": "TXYZabc123def456ghi789jkl012mno345",
    "externalRef": "payout_vendor_456"
  }'
json
{
  "success": true,
  "data": {
    "transactionId": "cma2abc9g0002yz6l0def5678",
    "status": "PROCESSING",
    "processingFee": "1.50",
    "requiresManualApproval": false,
    "currentPayinFee": "1%",
    "currentTier": "flat"
  }
}

Get a Payout

GET/v1/payout/:id
json
{
  "success": true,
  "data": {
    "transactionId": "cma2abc9g0002yz6l0def5678",
    "status": "PROCESSING",
    "processingFee": "1.50",
    "requiresManualApproval": false,
    "currentPayinFee": "1%",
    "currentTier": "flat"
  }
}
PENDINGAwaiting manual approval (amount > $50K)
PROCESSINGApproved, preparing broadcast
BROADCASTSubmitted to TRON network
CONFIRMEDConfirmed on-chain
FAILEDBroadcast failed — balance refunded
CANCELLEDCancelled before broadcast

List Payouts

GET/v1/payout
json
{
  "success": true,
  "data": {
    "items": [ ... ],
    "meta": {
      "page": 1,
      "limit": 20,
      "total": 42,
      "totalPages": 3,
      "hasNext": true,
      "hasPrev": false
    }
  }
}

Withdrawals

Request a Withdrawal

POST/v1/withdrawals

docs.requestWithdrawalP

Note
docs.withdrawalAddressNote
ParameterTypeRequiredDescription
amountstringrequiredAmount in USDT to withdraw (e.g. "500.00"). Minimum: "5.00". A fee of 1% (min $1.50) is deducted from this amount.
whitelistAddressIdstringoptionalID of the saved withdrawal address to use. Defaults to your primary address if omitted.
bash
curl -X POST https://api.zyndpay.io/v1/withdrawals \
  -H "X-Api-Key: zyp_live_sk_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "amount": "500.00",
    "whitelistAddressId": "addr_cma1xyz8f0001yx5k"
  }'
json
{
  "success": true,
  "data": {
    "id": "wdr_cma1xyz8f0001yx5k",
    "amount": "500.00",
    "fee": "5.00",
    "netAmount": "495.00",
    "destinationAddress": "TRXabc123def456ghi789jkl",
    "status": "PENDING",
    "createdAt": "2026-03-06T11:00:00.000Z"
  }
}

Get a Withdrawal

GET/v1/withdrawals/:id
json
{
  "success": true,
  "data": {
    "id": "wdr_cma1xyz8f0001yx5k",
    "amount": "500.00",
    "fee": "5.00",
    "netAmount": "495.00",
    "destinationAddress": "TRXabc123def456ghi789jkl",
    "status": "PENDING",
    "createdAt": "2026-03-06T11:00:00.000Z"
  }
}
PENDING_REVIEWAwaiting admin review
APPROVEDApproved, queued for processing
BROADCASTSubmitted to TRON network
CONFIRMEDConfirmed on-chain
REJECTEDRejected by admin review
FAILEDBroadcast failed — balance refunded
CANCELLEDCancelled before broadcast

Cancel a Withdrawal

DELETE/v1/withdrawals/:id

docs.cancelWithdrawalP


Webhooks

Overview

docs.webhooksOverviewP

Tip
docs.webhooksRetryTip
json
{
  "event": "payin.confirmed",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "status": "CONFIRMED",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "amount": "100.00",
    "amountRequested": "100.00",
    "txHash": "abc123def456...",
    "confirmedAt": "2026-03-06T11:01:02.000Z"
  },
  "createdAt": "2026-03-06T11:01:03.000Z"
}

Signature Verification

docs.webhookVerifyP1

X-ZyndPay-Signature
t=1680000000,v1=5257a869e7ecebeda32af...

docs.webhookSignatureNote

javascript
const crypto = require('crypto');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-zyndpay-signature'];
  const [tPart, v1Part] = sig.split(',');
  const timestamp = tPart.split('=')[1];
  const received  = v1Part.split('=')[1];

  // Reject events older than 5 minutes
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
  if (age > 300) return res.status(400).send('Webhook too old');

  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // Handle event...
  res.json({ received: true });
});
Warning
docs.webhookRawBodyWarning

Events

Note
payin.* events fire for payments you create via POST /v1/payments (customer checkout). deposit.* events fire for direct USDT transfers to your wallet address that were not created through the API. Subscribe to both if your integration also accepts manual or over-the-counter transfers.
EventTypeRequiredDescription
payin.createdeventrequiredFired when a new payin is created and a deposit address is generated.
payin.confirmingeventrequiredFired when funds are received and awaiting 5 block confirmations.
payin.confirmedeventrequiredFired when a payin reaches 5 on-chain confirmations. Merchant balance is credited.
payin.expiredeventrequiredFired when a payin address expires without receiving the expected amount.
payin.failedeventrequiredFired when a payin fails due to a network or processing error.
payin.underpaideventrequiredFired when a payin receives less than the requested amount.
payin.overpaideventrequiredFired when a payin receives more than the requested amount.
withdrawal.requestedeventrequiredFired when a new withdrawal is requested and queued for review.
withdrawal.approvedeventrequiredFired when a withdrawal passes review and is approved for on-chain broadcast.
withdrawal.confirmedeventrequiredFired when a withdrawal is confirmed on-chain.
withdrawal.failedeventrequiredFired when a withdrawal broadcast fails. Amount is refunded to balance.
aml.flaggedeventrequiredFired when a transaction is flagged by the AML screening engine for manual review.

Payload Schemas

Every webhook is a JSON POST with the same envelope: event, data, createdAt. The shape of data depends on the event type. Click an event below to see its exact payload.

Payin events
json
{
  "event": "payin.confirmed",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "status": "CONFIRMED",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "amount": "100.00",
    "amountRequested": "100.00",
    "txHash": "abc123def456...",
    "confirmedAt": "2026-03-06T11:01:02.000Z"
  },
  "createdAt": "2026-03-06T11:01:03.000Z"
}
Payout events
json
{
  "event": "payout.confirmed",
  "data": {
    "transactionId": "tx_sample_payout123",
    "status": "CONFIRMED",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "amount": "50.00",
    "fee": "1.50",
    "destinationAddress": "TXyz1234567890AbCdEfGhIjKlMnOpQrSt",
    "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
    "externalRef": "vendor_invoice_456",
    "confirmedAt": "2026-01-15T12:01:00.000Z"
  },
  "createdAt": "2026-01-15T12:01:05.000Z"
}
Withdrawal events
json
{
  "event": "withdrawal.confirmed",
  "data": {
    "transactionId": "wdr_sample_abc123",
    "status": "CONFIRMED",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "amount": "500.00",
    "fee": "5.00",
    "netAmount": "495.00",
    "toAddress": "TXyz1234567890AbCdEfGhIjKlMnOpQrSt",
    "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
    "confirmedAt": "2026-01-15T12:01:00.000Z"
  },
  "createdAt": "2026-01-15T12:01:05.000Z"
}

Test Webhook

Send a test event to one of your registered webhook endpoints. Useful for verifying your endpoint is reachable and your signature verification code works correctly.

POST/v1/webhooks/test
ParameterTypeRequiredDescription
endpointIdstringrequiredThe ID of the webhook endpoint to send the test event to. Get this from GET /webhooks/endpoints.
eventTypestringrequiredThe event type to simulate (e.g. payin.confirmed, payout.confirmed). Must be a valid event name.
bash
curl -X POST https://api.zyndpay.io/v1/webhooks/test \
  -H "X-Api-Key: zyp_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "endpointId": "your-endpoint-id",
    "eventType": "payin.confirmed"
  }'
json
{
  "success": true,
  "data": {
    "message": "Test event sent",
    "deliveryId": "9270e4aa-8391-4e3f-a70b-23c431714b84"
  }
}

Payload Schemas

Every webhook delivery shares the same envelope: an event string, a data object with event-specific fields, and a createdAt timestamp. Examples below show exact field names and types.

payin.created

Fired immediately when a new payin is created and a TRON deposit address is assigned.

json
{
  "event": "payin.created",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "status": "AWAITING_PAYMENT",
    "expiresAt": "2026-03-06T12:00:00.000Z",
    "paymentUrl": "https://checkout.zyndpay.io/cma1xyz8f0001yx5k9abc1234"
  },
  "createdAt": "2026-03-06T11:00:00.000Z"
}
payin.confirmed

Fired when a payin reaches 5 on-chain confirmations. Merchant balance is credited at this point.

json
{
  "event": "payin.confirmed",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "amountReceived": "100.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "txHash": "abc123def456...",
    "status": "CONFIRMED",
    "confirmedAt": "2026-03-06T11:01:02.000Z"
  },
  "createdAt": "2026-03-06T11:01:03.000Z"
}
payin.expired

Fired when a payin address expires without receiving the expected USDT amount.

json
{
  "event": "payin.expired",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "status": "EXPIRED",
    "expiredAt": "2026-03-06T12:00:00.000Z"
  },
  "createdAt": "2026-03-06T12:00:01.000Z"
}
payin.failed

Fired when a payin fails due to a network or processing error. No funds were received.

json
{
  "event": "payin.failed",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "status": "FAILED",
    "reason": "Network processing error"
  },
  "createdAt": "2026-03-06T11:05:00.000Z"
}
payin.underpaid

Fired when the customer sends less USDT than the requested amount. The shortfall field shows how much is missing.

json
{
  "event": "payin.underpaid",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "amountReceived": "90.00",
    "shortfall": "10.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "txHash": "abc123def456...",
    "status": "UNDERPAID",
    "confirmedAt": "2026-03-06T11:01:02.000Z"
  },
  "createdAt": "2026-03-06T11:01:03.000Z"
}
payin.overpaid

Fired when the customer sends more USDT than the requested amount. The surplus field shows the excess.

json
{
  "event": "payin.overpaid",
  "data": {
    "transactionId": "cma1xyz8f0001yx5k9abc1234",
    "address": "TRXabc123def456ghi789jkl",
    "amount": "100.00",
    "amountReceived": "110.00",
    "surplus": "10.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "order_123",
    "txHash": "abc123def456...",
    "status": "OVERPAID",
    "confirmedAt": "2026-03-06T11:01:02.000Z"
  },
  "createdAt": "2026-03-06T11:01:03.000Z"
}
payout.confirmed

Fired when an outbound payout is confirmed on-chain. The txHash is the on-chain transaction ID.

json
{
  "event": "payout.confirmed",
  "data": {
    "transactionId": "cma2abc9g0002yz6l0def5678",
    "destinationAddress": "TXYZabc123def456ghi789jkl012mno345",
    "amount": "50.00",
    "fee": "1.50",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "payout_vendor_456",
    "txHash": "def456abc123...",
    "status": "CONFIRMED",
    "confirmedAt": "2026-03-06T11:05:00.000Z"
  },
  "createdAt": "2026-03-06T11:05:01.000Z"
}
payout.failed

Fired when a payout broadcast fails. The amount is automatically refunded to your merchant balance.

json
{
  "event": "payout.failed",
  "data": {
    "transactionId": "cma2abc9g0002yz6l0def5678",
    "destinationAddress": "TXYZabc123def456ghi789jkl012mno345",
    "amount": "50.00",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "externalRef": "payout_vendor_456",
    "status": "FAILED",
    "reason": "Broadcast failed — balance refunded",
    "failedAt": "2026-03-06T11:05:00.000Z"
  },
  "createdAt": "2026-03-06T11:05:01.000Z"
}
withdrawal.approved

Fired when a withdrawal request passes admin review and is queued for on-chain broadcast.

json
{
  "event": "withdrawal.approved",
  "data": {
    "id": "wdr_cma1xyz8f0001yx5k",
    "amount": "500.00",
    "fee": "5.00",
    "netAmount": "495.00",
    "destinationAddress": "TRXabc123def456ghi789jkl",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "status": "APPROVED",
    "approvedAt": "2026-03-06T12:00:00.000Z"
  },
  "createdAt": "2026-03-06T12:00:01.000Z"
}
withdrawal.confirmed

Fired when a withdrawal is confirmed on-chain. netAmount is what your wallet actually received after fees.

json
{
  "event": "withdrawal.confirmed",
  "data": {
    "id": "wdr_cma1xyz8f0001yx5k",
    "amount": "500.00",
    "fee": "5.00",
    "netAmount": "495.00",
    "destinationAddress": "TRXabc123def456ghi789jkl",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "txHash": "ghi789abc123...",
    "status": "CONFIRMED",
    "confirmedAt": "2026-03-06T12:30:00.000Z"
  },
  "createdAt": "2026-03-06T12:30:01.000Z"
}
withdrawal.failed

Fired when a withdrawal broadcast fails. The amount is automatically refunded to your balance.

json
{
  "event": "withdrawal.failed",
  "data": {
    "id": "wdr_cma1xyz8f0001yx5k",
    "amount": "500.00",
    "destinationAddress": "TRXabc123def456ghi789jkl",
    "currency": "USDT_TRC20",
    "chain": "TRON",
    "status": "FAILED",
    "reason": "Broadcast failed — balance refunded",
    "failedAt": "2026-03-06T12:30:00.000Z"
  },
  "createdAt": "2026-03-06T12:30:01.000Z"
}

SDKs & Plugins

Node.js SDK

The official TypeScript SDK with full type safety and built-in webhook verification.

bash
npm install @zyndpay/sdk
javascript
const { ZyndPay } = require('@zyndpay/sdk');

const client = new ZyndPay({
  apiKey: 'zyp_live_sk_...',
  webhookSecret: process.env.ZYNDPAY_WEBHOOK_SECRET,
});

// Create a payment
const payment = await client.payins.create({
  amount: '100.00',
  externalRef: 'order_123',
});
console.log(payment.address);

// Verify a webhook (uses raw body, not parsed JSON)
const event = client.webhooks.verify(
  rawBody,
  req.headers['x-zyndpay-signature']
);

TypeScript Quickstart

Accept your first USDT payment in under 5 minutes. This example uses Express — swap in your framework of choice.

typescript
import { ZyndPay } from '@zyndpay/sdk';
import express from 'express';

// 1. Initialize the SDK
const client = new ZyndPay({
  apiKey: process.env.ZYNDPAY_API_KEY,       // zyp_live_sk_...
  webhookSecret: process.env.WEBHOOK_SECRET,
});

// 2. Create a pay-in (generates a TRON deposit address for your customer)
const payment = await client.payins.create({
  amount: '100.00',
  externalRef: 'order_123',
  expiresInSeconds: 3600,
  successUrl: 'https://yoursite.com/payment/success',
});
// Send payment.paymentUrl or payment.address to your customer
console.log('Deposit address:', payment.address);
console.log('Payment page:', payment.paymentUrl);

// 3. Handle webhook events
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  // Always verify using the raw body — never parsed JSON
  const event = client.webhooks.verify(
    req.body,
    req.headers['x-zyndpay-signature'] as string,
  );

  switch (event.event) {
    case 'payin.confirmed':
      console.log(`Payment confirmed: ${event.data.amount} USDT — ${event.data.externalRef}`);
      // Fulfill the order, unlock access, send receipt, etc.
      break;
    case 'payin.expired':
      console.log(`Payment expired: ${event.data.externalRef}`);
      break;
  }
  res.json({ received: true });
});

// 4. Check payment status anytime
const status = await client.payins.get(payment.transactionId);
console.log('Status:', status.status); // AWAITING_PAYMENT → CONFIRMING → CONFIRMED
Tip
Webhook handlers must return a 2xx status within 10 seconds. Failed deliveries are retried with exponential backoff up to 5 times.

Python SDK

docs.pythonSdkP

bash
pip install zyndpay
python
from zyndpay import ZyndPay

client = ZyndPay(
    api_key="zyp_live_sk_...",
    webhook_secret=os.environ.get("ZYNDPAY_WEBHOOK_SECRET"),
)

payment = client.payins.create(
    amount="100.00",
    external_ref="order_123"
)
print(payment["address"])

Python Quickstart

Accept your first USDT payment in under 5 minutes. This example uses Flask — swap in your framework of choice.

python
import os
from zyndpay import ZyndPay
from flask import Flask, request, jsonify

# 1. Initialize the SDK
client = ZyndPay(
    api_key=os.environ["ZYNDPAY_API_KEY"],        # zyp_live_sk_...
    webhook_secret=os.environ["WEBHOOK_SECRET"],
)

# 2. Create a pay-in (generates a TRON deposit address for your customer)
payment = client.payins.create(
    amount="100.00",
    external_ref="order_123",
    expires_in_seconds=3600,
    success_url="https://yoursite.com/payment/success",
)
# Send payment["paymentUrl"] or payment["address"] to your customer
print("Deposit address:", payment["address"])
print("Payment page:", payment["paymentUrl"])

# 3. Handle webhook events
app = Flask(__name__)

@app.post("/webhook")
def handle_webhook():
    # Always verify using the raw body — never parsed JSON
    event = client.webhooks.verify(
        request.get_data(),
        request.headers.get("X-ZyndPay-Signature"),
    )

    if event["event"] == "payin.confirmed":
        data = event["data"]
        print(f"Payment confirmed: {data['amount']} USDT — {data['externalRef']}")
        # Fulfill the order, unlock access, send receipt, etc.
    elif event["event"] == "payin.expired":
        print("Payment expired:", event["data"]["externalRef"])

    return jsonify(received=True)

# 4. Check payment status anytime
status = client.payins.get(payment["transactionId"])
print("Status:", status["status"])  # AWAITING_PAYMENT → CONFIRMING → CONFIRMED
Tip
Webhook handlers must return a 2xx status within 10 seconds. Failed deliveries are retried with exponential backoff up to 5 times.

PHP SDK

The official PHP SDK with cURL-based HTTP client and webhook verification. Requires PHP 8.0+.

bash
composer require zyndpay/zyndpay-php
php
<?php
require_once 'vendor/autoload.php';

$client = new ZyndPay\ZyndPay('zyp_live_sk_...', [
    'webhook_secret' => getenv('ZYNDPAY_WEBHOOK_SECRET'),
]);

// Create a payment
$payment = $client->payins->create([
    'amount' => '100.00',
    'externalRef' => 'order_123',
]);
echo $payment['address'];

// Verify a webhook
$event = $client->webhooks->verify(
    $rawBody,
    $_SERVER['HTTP_X_ZYNDPAY_SIGNATURE']
);

PHP Quickstart

Accept your first USDT payment in under 5 minutes. This example uses plain PHP — adapt the webhook handler to your framework.

php
<?php
require_once 'vendor/autoload.php';

// 1. Initialize the SDK
$client = new ZyndPay\ZyndPay(
    getenv('ZYNDPAY_API_KEY'),   // zyp_live_sk_...
    ['webhook_secret' => getenv('WEBHOOK_SECRET')]
);

// 2. Create a pay-in (generates a TRON deposit address for your customer)
$payment = $client->payins->create([
    'amount' => '100.00',
    'externalRef' => 'order_123',
    'expiresInSeconds' => 3600,
    'successUrl' => 'https://yoursite.com/payment/success',
]);
// Send $payment['paymentUrl'] or $payment['address'] to your customer
echo "Deposit address: " . $payment['address'] . "\n";
echo "Payment page: " . $payment['paymentUrl'] . "\n";

// 3. Handle webhook events (in your webhook endpoint file)
$rawBody   = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_ZYNDPAY_SIGNATURE'];
$event     = $client->webhooks->verify($rawBody, $sigHeader);

switch ($event['event']) {
    case 'payin.confirmed':
        $data = $event['data'];
        echo "Payment confirmed: {$data['amount']} USDT — {$data['externalRef']}\n";
        // Fulfill the order, unlock access, send receipt, etc.
        break;
    case 'payin.expired':
        echo "Payment expired: " . $event['data']['externalRef'] . "\n";
        break;
}
http_response_code(200);
echo json_encode(['received' => true]);

// 4. Check payment status anytime
$status = $client->payins->get($payment['transactionId']);
echo "Status: " . $status['status'] . "\n"; // AWAITING_PAYMENT → CONFIRMING → CONFIRMED
Tip
Webhook handlers must return a 2xx status within 10 seconds. Failed deliveries are retried with exponential backoff up to 5 times.
PHP Signature Verification

Raw PHP implementation without the SDK. Produces the same HMAC-SHA256 check as the Node.js and Python examples above.

php
<?php
// IMPORTANT: read raw body before any JSON parsing
$payload   = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_ZYNDPAY_SIGNATURE'] ?? '';

function verifyWebhook(string $payload, string $sigHeader, string $secret): bool {
    $parts = [];
    foreach (explode(',', $sigHeader) as $part) {
        [$k, $v] = explode('=', $part, 2);
        $parts[$k] = $v;
    }

    if (empty($parts['t']) || empty($parts['v1'])) {
        return false;
    }

    // Reject events older than 5 minutes
    if (abs(time() - (int) $parts['t']) > 300) {
        return false;
    }

    $expected = hash_hmac('sha256', $parts['t'] . '.' . $payload, $secret);

    // Timing-safe comparison to prevent timing attacks
    return hash_equals($expected, $parts['v1']);
}

if (!verifyWebhook($payload, $sigHeader, getenv('ZYNDPAY_WEBHOOK_SECRET'))) {
    http_response_code(400);
    echo 'Invalid signature';
    exit;
}

$event = json_decode($payload, true);
// Handle $event['event'] ...

WooCommerce Plugin

Accept USDT payments in your WordPress / WooCommerce store without writing a single line of code. The plugin generates deposit addresses, polls confirmation status, and auto-fulfills orders.

1

Download the plugin ZIP from the ZyndPay GitHub repository.

2

In WordPress admin go to Plugins → Add New → Upload Plugin.

3

Upload the ZIP and click Activate.

4

Navigate to WooCommerce → Settings → Payments → ZyndPay.

5

Enter your API key and webhook secret, then save.


Sandbox

Overview

docs.sandboxOverviewP

Isolated balance
Sandbox funds are completely separate from your live balance.
Identical API
Same endpoints, same response shapes, same webhook events.
Instant confirm
Simulate payment confirmation in one API call — no waiting.
Free forever
Available to all merchants from day one, no approval needed.

Testing Guide

Follow these steps to test your full payment flow end-to-end before going live.

1
Get your sandbox API key
In your dashboard, go to API Keys and create a key with the zyp_test_sk_... prefix. Sandbox keys are free and always available. You can also use the key from the dashboard's Sandbox Playground.
2
Create a test pay-in
Call POST /v1/payments with your zyp_test_sk_... key and include "sandbox": true in the request body. You receive a real-looking TRON address and payment URL, but no blockchain activity happens.
3
Simulate blockchain confirmation
Call POST /v1/sandbox/payments/:id/simulate. This instantly moves the transaction to CONFIRMED status, fires the payin.confirmed webhook to your endpoint, and credits your sandbox balance.
4
Verify your webhook fires
Check your server logs or use a tool like ngrok or webhook.site to expose a local endpoint. The simulate call fires a real webhook with a real signature — run your verification code against it.
5
Reset and repeat
Call POST /v1/sandbox/reset to clear all sandbox transactions and start fresh. Useful for automated test suites.
bash
# Using the sandbox API key (zyp_test_sk_...)
curl -X POST https://api.zyndpay.io/v1/payments \
  -H "X-Api-Key: zyp_test_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100",
    "externalRef": "test_order_001",
    "sandbox": true
  }'
typescript
import { ZyndPay } from '@zyndpay/sdk';

const zyndpay = new ZyndPay({ apiKey: 'zyp_test_sk_...' });

// 1. Create a sandbox payin
const payin = await zyndpay.payins.create({
  amount: '100',
  sandbox: true,
  externalRef: 'test_order_001',
});

// 2. Instantly simulate confirmation (fires the payin.confirmed webhook)
const result = await zyndpay.payins.simulate(payin.transactionId);
console.log('Simulated status:', result.status); // "CONFIRMED"

// 3. Confirm the status
const tx = await zyndpay.payins.get(payin.transactionId);
console.log('Final status:', tx.status); // "CONFIRMED"
bash
# Reset all sandbox transactions (useful for clean test runs)
curl -X POST https://api.zyndpay.io/v1/sandbox/reset \
  -H "X-Api-Key: zyp_test_sk_..."
Common gotchas
Using a live key (zyp_live_sk_...) with sandbox: true returns LIVE_KEY_SANDBOX_REQUEST. Use a test key for sandbox requests.
The simulate endpoint only works on transactions in AWAITING_PAYMENT status. If you call it twice or after expiry, you get a 400 error.
Webhook signatures in sandbox are real — the same HMAC-SHA256 algorithm applies. Your verification code must work exactly the same as in production.
Sandbox balances are not transferable to production. They exist only for testing and are wiped on reset.

Simulate Payment

POST/v1/sandbox/payments/:id/simulate

docs.sandboxSimulateP

bash
curl -X POST \
  https://api.zyndpay.io/v1/sandbox/payments/{id}/simulate \
  -H "X-Api-Key: zyp_test_sk_..."
json
{
  "success": true,
  "data": {
    "message": "Payin simulation triggered",
    "transactionId": "f4b2cb0f-08ce-4408-88d4-0a678ca0aae2"
  }
}
Tip
Call POST /v1/sandbox/reset to delete all sandbox transactions and start with a clean slate. Useful for automated test suites that need a consistent starting state.

ZyndPaySubs

Overview

docs.subsOverviewP

Tip
ZyndPaySubs uses the same merchant account as the ZyndPay API. Your balance, webhooks, and dashboard work across both products.

Setup

Get your channel monetized in 60 seconds:

1
Add @ZyndPaySubsBot as an administrator to your private Telegram channel or group.
2
Send /setup to the bot to configure your subscription price and billing interval (weekly, monthly, or yearly).
3
Share your channel link — subscribers will see a "Join" button that opens the Mini App checkout.

Bot Commands

Available commands for channel owners:

CommandTypeRequiredDescription
/setupcommandrequiredConfigure subscription price, billing interval, and channel settings.
/plancommandrequiredView your current plan and earnings.
/withdrawcommandrequiredWithdraw your USDT balance to a TRON wallet address.
/helpcommandrequiredShow all available commands and usage information.
/revenuecommandrequiredView revenue analytics — total earned, active subscribers, and monthly trends.
/subscriberscommandrequiredList active subscribers with their subscription status and renewal dates.

Reference

Error Codes

docs.errorCodesP

json
{
  "success": false,
  "error": {
    "code": "AMOUNT_TOO_LOW",
    "message": "Minimum payin amount is 1 USDT"
  },
  "statusCode": 400
}
CodeHTTP StatusRequiredDescription
UNAUTHORIZED401requiredAPI key is missing or invalid. Check that you are sending the X-Api-Key header and the key exists in your dashboard.
FORBIDDEN403requiredThe API key does not have the required scope for this operation. Use a secret key (sk) not a publishable key (pk).
NOT_FOUND404requiredThe requested resource does not exist or does not belong to your merchant account.
VALIDATION_ERROR400requiredRequest body failed validation. The details field lists each invalid field and reason.
AMOUNT_TOO_SMALL400requiredAmount is below the minimum for this operation (1 USDT for payins, 5 USDT for payouts/withdrawals).
AMOUNT_TOO_LARGE400requiredAmount exceeds the maximum allowed for this operation or your current compliance tier.
INVALID_ADDRESS400requiredThe TRON wallet address is malformed. Addresses must start with T and be 34 characters in base58 format.
INVALID_TRANSACTION_STATUS400requiredThe operation is not allowed in the current transaction status (e.g. trying to cancel a confirmed payout).
DUPLICATE_EXTERNAL_REF409requiredA transaction with this externalRef already exists for your account. Use a unique reference per request, or omit it.
INSUFFICIENT_BALANCE400requiredYour USDT balance is too low for the requested payout or withdrawal amount. Check your balance at GET /v1/wallets/balance.
KYB_REQUIRED403requiredThis operation requires completed KYB (Know Your Business) verification. Complete KYB in your dashboard to unlock full limits.
COMPLIANCE_LIMIT_REACHED403requiredYour monthly transaction volume has reached the compliance limit for your current tier. Complete KYB to increase your limits.
AML_BLOCKED403requiredThis transaction was flagged by our AML screening engine and cannot be processed. Contact [email protected].
ADDRESS_NOT_FOUND404requiredThe whitelistAddressId does not match any saved withdrawal address on your account. Add addresses in your dashboard under Withdrawal Addresses.
ADDRESS_NOT_WHITELISTED403requiredThe destination address is not on your merchant whitelist. Add it in your dashboard before sending funds to it.
CANNOT_CANCEL400requiredThis resource can no longer be cancelled (it has already been approved, broadcast, or completed).
MISSING_IDEMPOTENCY_KEY400requiredThis endpoint requires an Idempotency-Key header. Pass a unique UUID per request.
IDEMPOTENCY_CONFLICT409requiredAn existing request with the same Idempotency-Key has different parameters. Use a new key for a different request.
DUPLICATE_RESOURCE409requiredA resource with this unique value already exists (e.g. duplicate email, slug, or field).
LIVE_KEY_SANDBOX_REQUEST400requiredYou sent sandbox: true with a live API key. Use a zyp_test_sk_... key for sandbox requests.
SANDBOX_KEY_LIVE_REQUEST400requiredYou sent a sandbox key but the resource is a live transaction. Use a zyp_live_sk_... key for production requests.
RATE_LIMIT_EXCEEDED429requiredToo many requests. Check the Retry-After response header for the number of seconds to wait before retrying.
FREEMIUM_LIMIT_EXCEEDED403requiredMonthly freemium volume limit ($14,500) reached. Complete KYB to unlock higher limits.
INVALID_WEBHOOK_SIGNATURE400requiredWebhook signature verification failed. Check your webhook secret and that you are using the raw request body.
INTERNAL_ERROR500requiredUnexpected server error. Include the requestId from the response when contacting [email protected].

Rate Limits

docs.rateLimitsP

EndpointRate LimitRequiredDescription
Default100/60srequiredGlobal rate limit per API key.
POST /payments30/60srequiredPayin creation (POST /payments) per merchant.
POST /payout10/60srequiredPayout creation (POST /payout) per merchant.
POST /withdrawals5/60srequiredWithdrawal requests per merchant.
Tip
docs.rlEnterprise
Was this helpful?