API documentation

Everything needed to accept USDT/USDC on your site: create an invoice from your backend, redirect the customer to the hosted payment page, and get notified by a signed webhook when it settles. Base URL: https://payhusk.com. All requests and responses are JSON.

Authentication

Every API request carries X-Api-Key: phk_…. Keys are issued per verified website: add your site under Settings, upload the shown code as payhusk-verify.txt at the site root, and click Verify — the check runs in real time and the key appears. Each site's key can be regenerated independently; the old key stops working immediately. A missing or invalid key returns:

HTTP/1.1 401 Unauthorized
{"error": "missing or invalid X-Api-Key header"}

Create an invoice

POST /api/v1/invoices

FieldTypeDescription
amountnumber, requiredInvoice amount in whole tokens, e.g. 49.90. Up to 6 decimal places.
tokenstring, requiredUSDT or USDC.
order_idstring, optionalYour own reference — echoed back everywhere, searchable in the dashboard.
success_urlstring, optionalThe payer's browser is sent here after settlement.
cancel_urlstring, optionalRenders a “cancel and return to store” link on the payment page.
callback_urlstring, optionalReceives the signed server-to-server invoice.paid webhook.
curl -X POST https://payhusk.com/api/v1/invoices \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: phk_...' \
  -d '{"amount": 49.90, "token": "USDT", "order_id": "order-1042",
       "success_url": "https://yourstore.com/thanks",
       "cancel_url": "https://yourstore.com/cart",
       "callback_url": "https://yourstore.com/webhooks/payhusk"}'

Redirect your customer to payment_url. They pick a network (ERC-20, TRC-20 or BEP-20), get a reserved deposit address with a QR code, and the invoice settles when the transfer confirms on-chain. Invoices and their addresses expire after exactly one hour.

HTTP/1.1 201 Created
{
  "id": "9c1f2ab4-…",
  "status": "pending",
  "amount": 49.9,
  "token": "USDT",
  "payment_url": "https://payhusk.com/pay/9c1f2ab4-…",
  "success_url": "https://yourstore.com/thanks",
  "cancel_url": "https://yourstore.com/cart",
  "callback_url": "https://yourstore.com/webhooks/payhusk",
  "expires_at": 1760000000
}

Validation failures return 400 with an error field (non-positive amount, unknown token).

Fetch an invoice

GET /api/v1/invoices/{id} with the same header. Status is one of pendingpaid or expired. Amounts appear twice: amount as a decimal for convenience and amount_micro as the exact integer (1 token = 1,000,000 micro) — use the integer for any arithmetic on your side.

{
  "id": "9c1f2ab4-…",
  "amount": 49.9,
  "amount_micro": 49900000,
  "token": "USDT",
  "network": "ERC20",
  "address": "0x9858…da94",
  "status": "paid",
  "order_id": "order-1042",
  "created_at": 1759996400,
  "expires_at": 1760000000,
  "paid_at": 1759998211,
  "txid": "0x4f2c…",
  "payment_url": "…/pay/9c1f2ab4-…"
}

An unknown id returns 404. The payment page itself polls the public, unauthenticated GET /pay/{id}/status, which you may also use client-side.

Webhooks

When an invoice settles, Payhusk POSTs the event to your callback_url and retries on failure with exponential backoff (up to ~6 h between attempts) until your server answers any 2xx.

Headers

Content-Type: application/json
X-Payhusk-Event: invoice.paid
X-Payhusk-Signature: t=1759998211,v1=<hex hmac-sha256>

Payload

{
  "event": "invoice.paid",
  "invoice_id": "9c1f2ab4-…",
  "status": "paid",
  "amount": 49.9,
  "token": "USDT",
  "network": "ERC20",
  "order_id": "order-1042",
  "txid": "0x4f2c…",
  "paid_at": 1759998211
}

Verifying the signature

Compute HMAC-SHA256 of "<t>." + raw request body with your whsec_… secret (shown under Settings), compare against v1 in constant time, and reject timestamps older than ~5 minutes. Only genuine Payhusk requests pass.

import hmac, hashlib

def verify(secret: str, sig_header: str, raw_body: bytes) -> bool:
    parts = dict(p.split('=', 1) for p in sig_header.split(','))
    expected = hmac.new(secret.encode(),
                        f"{parts['t']}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])

Good to know

• Amounts in requests are decimal tokens; internally everything is integer micro-units — no float drift. • order_id and the on-chain txid are both searchable on your Invoices page. • Transfers are final: there are no chargebacks, and the deposit address is reserved exclusively for one invoice until it settles or expires.