PayHook

Accept USDT Payments in 30 Minutes

Code on a laptop screen

Why USDT

USDT (Tether) is the most widely held stablecoin, with over $140 billion in circulation. It's pegged 1:1 to the US dollar, so neither you nor your customer deals with price volatility. Transfer fees are low — roughly $0.05 on BSC and under $1 on TRON — and most crypto users already have USDT in their wallet.

USDT is available as BEP-20 (BSC), TRC-20 (TRON), and ERC-20 (Ethereum). This tutorial uses BSC for the examples, but the same flow works on any supported chain.

What you'll need

Step 1 — Sign up and get your API key

Create an account at app.payhook.app/sign-up. No KYC, no card — just an email and password.

Once you're in, go to /api-keys in the dashboard. Your API key has the format pk_<hex>. Copy it and store it like a password — anyone with the key can create payments on your account. Keep it server-side; never embed it in client-side JavaScript.

Step 2 — Add your wallet addresses

Go to /addresses in the dashboard. For each chain you want to accept payments on, paste the wallet addresses you control.

Each pending payment locks one address until it confirms or expires. If you only have one address and two customers try to pay at the same time, the second payment will fail with a 503 (all addresses held). Add at least five addresses per chain if you expect any concurrent volume.

Step 3 — Configure a webhook

Go to /webhooks in the dashboard. Add your server's webhook URL (e.g. https://yoursite.com/webhooks/payhook) and select the events you want: payment.confirmed and payment.expired.

Save the signing secret shown on the endpoint. You'll need it to verify incoming webhooks. The secret is shown only once — if you lose it, you'll need to rotate it from the dashboard.

Step 4 — Create your first payment

curlPOST /api/v1/payments/
curl -X POST https://api.payhook.app/api/v1/payments/ \
  -H "X-API-Key: pk_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_usd_cents": 5000,
    "network": "bsc",
    "currency": "usdt",
    "external_order_id": "order-123",
    "redirect_url": "https://yoursite.com/orders/123/thanks"
  }'

The response includes:

The Idempotency-Key header makes retries safe. If the same key is sent within 24 hours, PayHook returns the original payment instead of creating a duplicate.

Step 5 — Handle the webhook

When the customer's payment confirms on-chain, PayHook POSTs a JSON body to your webhook URL with an X-PayHook-Signature header. Here's a minimal Node.js handler:

Node.jsExpress webhook handler
const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhooks/payhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.headers['x-payhook-signature'];
    const parts = Object.fromEntries(
      header.split(',').map(p => p.split('=').map(s => s.trim()))
    );

    const ts = parseInt(parts.t, 10);
    if (Math.abs(Date.now() / 1000 - ts) > 300)
      return res.status(401).end();

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

    try {
      if (!crypto.timingSafeEqual(
        Buffer.from(expected, 'hex'),
        Buffer.from(parts.v1, 'hex')
      )) return res.status(401).end();
    } catch {
      return res.status(401).end();
    }

    const event = JSON.parse(req.body);
    if (event.event === 'payment.confirmed') {
      // Fulfill the order using event.external_order_id
    }

    res.status(200).end();
  }
);

Two things to get right: sign the raw request body (not parsed-then-re-stringified), and compare in constant time (timingSafeEqual). The docs have verifiers in Python, PHP, Ruby, and Go as well.

What's next

Start accepting USDT today.

Free tier — no card required.

Get started