Accept USDT Payments in 30 Minutes
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
- A wallet address you control on BSC, TRON, or Ethereum (MetaMask, Trust Wallet, a hardware wallet — anything that gives you the private key)
- An HTTPS endpoint on your server that can receive POST requests (for webhooks)
- Basic familiarity with REST APIs and a tool like
curl
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
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:
checkout_url— redirect your customer here. They see a hosted page with a QR code, deposit address, and countdown timer.assigned_address— the deposit address drawn from your pool.expected_crypto_amount— the exact USDT amount the customer needs to send.expires_at— the payment auto-expires if no on-chain activity is detected by this time (default 30 minutes).
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:
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
- Monitor webhook deliveries from the /webhooks dashboard. Replay any failed delivery with one click.
- Use
GET /api/v1/payments/{payment_number}/to check payment status from your backend — a good double-check alongside the webhook. - Read the full API documentation for error handling, billing behavior, and the complete response schema.
- The free tier covers 100 confirmed payments per month. Pro and Scale plans unlock higher volume with lower per-payment rates.