Node.js USDT Payment API Integration

Create USDT payment pages with Node.js fetch, handle safe retries and verify raw-body webhook signatures with node:crypto.

Keep the integration on your server

This example uses Node.js 20+ and built-in modules, without a client SDK. Importing the module makes no API call. Never ship an API key or signing key to a browser bundle.

First configure the HTTPS webhook. Set DEDYX_API_KEY and DEDYX_WALLET in your server environment. Download dedyx_client.mjs and import it from your backend.

The code is an integration starting point, not a complete order system. Add your durable order storage, request body limits, receiver routing and fulfilment transaction. The examples use synthetic orders; replace the sample fields before making a real request.

Create a payment with safe retries

Node.js 20+ · downloadable client
// Node.js 20+ server-side example. Importing this module makes no API calls.
import {createHmac, timingSafeEqual} from 'node:crypto';
import {setTimeout as sleep} from 'node:timers/promises';
const API = 'https://api.dedyx.com/api/v1';

export async function createPayment({apiKey, wallet, amount, description, idempotencyKey}) {
  // Persist the key and fields with your order BEFORE calling this function.
  if (!/^[A-Za-z0-9._:-]{8,128}$/.test(idempotencyKey)) throw new Error('Invalid idempotency key');
  const body = JSON.stringify({merchant_wallet:wallet, amount, description});
  for (let attempt=0; attempt<3; attempt++) {
    let response, delay=500*(2**attempt)+Math.random()*250;
    try {
      response=await fetch(API+'/payments', {
        method:'POST', redirect:'manual', signal:AbortSignal.timeout(10000),
        headers:{'X-API-Key':apiKey,'Idempotency-Key':idempotencyKey,'Content-Type':'application/json'}, body
      });
    } catch (error) {
      if (attempt===2) throw error; // Uncertain outcome; preserve key/body and reconcile.
      await sleep(delay); continue;
    }
    if (response.ok) return response.json();
    const status=response.status, retryAfter=response.headers.get('retry-after')||'';
    // nginx may return HTML. Do not assume an error JSON body.
    await response.body?.cancel();
    if (![429,500,502,503,504].includes(status)||attempt===2) throw new Error(`Dedyx HTTP ${status}`);
    if (/^[0-9]+$/.test(retryAfter)) {
      const seconds=Number(retryAfter);
      if (seconds>60) throw new Error(`Retry later with the same key/body: ${seconds}s`);
      delay=Math.max(delay,seconds*1000);
    }
    await sleep(delay);
  }
}

The client uses a 10-second timeout and at most three attempts. 429 and selected 5xx responses are retried with the same serialized body and idempotency key. Numeric Retry-After is respected; long waits are left for your job scheduler. nginx errors can be HTML, so the client checks the status without assuming JSON.

409 is not blindly retried: check missing webhook settings, an occupied address or a changed body for the same key. For exhausted or uncertain retries keep the stored key and reconcile; replay retention is 24 hours by default. Keys must contain 8–128 ASCII letters, digits or ._:-.

Connect the response to your order

Example call — requires your own environment and order
import {createPayment} from './dedyx_client.mjs';

// These fields must come from a persisted order record.
const payment = await createPayment({
  apiKey: process.env.DEDYX_API_KEY,
  wallet: process.env.DEDYX_WALLET,
  amount: '49.00',
  description: 'Order #1024',
  idempotencyKey: 'order-1024-v1',
});
// Persist payment.payment_id and the response before redirecting.
console.log(payment.payment_url);

Save the response together with your order before redirecting the customer to payment_url. Reuse the same key and request fields for retries of that logical order. Do not put personal information in description.

Verify the original webhook body

Node.js 20+ · signature and event ID verification
import {createHmac, timingSafeEqual} from 'node:crypto';

export function verifyWebhook({rawBody, timestamp, signature, eventId, keyId, keys, now=Date.now()/1000}) {
  if (!Buffer.isBuffer(rawBody)) throw new Error('Use the original raw request Buffer');
  if (!/^[0-9]{1,12}$/.test(timestamp||'')||Math.abs(now-Number(timestamp))>300) throw new Error('Invalid or stale timestamp');
  const key=Object.hasOwn(keys,keyId)?keys[keyId]:undefined;
  if (!key||!/^v1=[0-9a-f]{64}$/.test(signature||'')) throw new Error('Unknown key or signature format');
  const expected=createHmac('sha256',key).update(timestamp+'.').update(rawBody).digest();
  const received=Buffer.from(signature.slice(3),'hex');
  if (!timingSafeEqual(expected,received)) throw new Error('Invalid signature');
  const event=JSON.parse(rawBody.toString('utf8'));
  if (!event||typeof event.event_id!=='string'||!event.event_id||event.event_id!==eventId) throw new Error('Event ID mismatch');
  // Validate your own order before fulfilling; signature verification alone is insufficient.
  return event;
}

Pass the raw request bytes/Buffer, X-Dedyx-Timestamp, X-Dedyx-Signature, X-Dedyx-Event-ID and X-Dedyx-Key-ID. The keys mapping contains your saved key IDs and full whsec_... secrets. Do not run JSON middleware before capturing the raw bytes.

After verification, require PAID, match the event to your stored order and compare the exact amount. Persist event_id and fulfilment atomically. A duplicate returns 2xx without repeating the action. See delivery and deduplication; signature verification alone must never issue a product.

Before enabling customer payments

  • Test a valid signature, altered body, stale timestamp and unknown key.
  • Test a duplicate event and a receiver crash during fulfilment.
  • Test an API timeout after a successful creation: retry the original key.
  • Test 429 with Retry-After, nginx HTML errors and temporary 503.
  • Review expiry, canceled orders and late transfers with your support team.

Full launch checklist and atomic fulfilment example · API errors · Request beta access