Crypto Payment API Integration: Invoices, Signatures and Webhooks
A practical developer walkthrough: one signed request to create an invoice, one webhook endpoint to receive status, and the security details that keep it trustworthy.

A crypto payment API integration needs two pieces: a server-side request that creates an invoice for each order and returns a checkout URL, and a webhook endpoint that receives signed status updates when the payment is detected and confirmed on-chain. With mistKET both are plain HTTPS and JSON, signed with HMAC-SHA256 — no SDK required.
This guide walks through the full flow with code, then covers the security and reliability details that separate a demo from production.
Architecture in one picture
Every crypto payment API integration follows the same five-step loop:
- Order created — your backend calls
POST /api/v1/invoiceswith amount, currency, order ID and callback URL. - Redirect — you send the customer to the returned
checkout_url. - Payment — the customer picks a network and pays on the hosted page; the rate is locked for the invoice lifetime.
- Webhook — mistKET POSTs
invoice.paid,invoice.confirmedor an exception event to your callback URL. - Fulfilment — you verify the signature, mark the order paid and deliver.
Your API key, API secret and webhook secret are issued in the merchant panel after your account is approved. The full reference lives in the panel under API.
Step 1: sign and send the invoice request
Every API request carries three headers. The signature is the hex HMAC-SHA256 of the string {timestamp}.{raw body} using your API secret:
POST /api/v1/invoices
X-Api-Key: mp_live_…
X-Api-Timestamp: 1760000000
X-Api-Signature: hex(hmac_sha256("{ts}.{body}", secret))
{
"amount": "184.50",
"currency": "USD",
"order_id": "2041",
"callback_url": "https://yourstore.com/webhooks/mistket"
} A minimal PHP version looks like this:
$body = json_encode([
'amount' => '184.50', 'currency' => 'USD',
'order_id' => '2041',
'callback_url' => 'https://yourstore.com/webhooks/mistket',
]);
$ts = (string) time();
$sig = hash_hmac('sha256', $ts . '.' . $body, $apiSecret);
// send $body with X-Api-Key, X-Api-Timestamp, X-Api-Signature Sign the exact bytes you send. Re-encoding the JSON after signing (different spacing or key order) is the number-one cause of signature errors.
Step 2: redirect to the hosted checkout
A successful call returns 201 Created with the invoice UUID, its status (new) and a checkout_url. Store the UUID against your order and redirect the customer. The hosted page shows the exact amount, a QR code, a countdown and live status, and works on mobile. Customers choose the network there — USDT on TRON, BNB Smart Chain, Ethereum or Arbitrum, plus TRX, BNB and ETH — unless you fix the network in the API call.
Besides creating invoices, the API lets you list, fetch and cancel them, which is useful when a cart changes or an order is abandoned.
Step 3: receive and verify webhooks
When the status changes, mistKET POSTs JSON to your callback URL with these headers:
| Header | Purpose |
|---|---|
X-Mistket-Event | Event name, e.g. invoice.confirmed |
X-Mistket-Timestamp | Unix time used in the signature |
X-Mistket-Signature | sha256= + HMAC-SHA256 of {timestamp}.{raw body} with your webhook secret |
X-Mistket-Delivery | Delivery ID, handy for logging |
Verification in PHP:
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_MISTKET_TIMESTAMP'] ?? '';
$got = $_SERVER['HTTP_X_MISTKET_SIGNATURE'] ?? '';
$exp = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $webhookSecret);
if (!hash_equals($exp, $got) || abs(time() - (int) $ts) > 300) {
http_response_code(401); exit;
}
$event = json_decode($raw, true); Use a constant-time comparison (hash_equals in PHP, hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) and reject stale timestamps to block replays. For background on the primitive, see RFC 2104 (HMAC).
Example webhook payload
A confirmed payment arrives as a compact JSON document. The fields you will use most are the event, your order ID, the status and the expected and received amounts:
POST https://yourstore.com/webhooks/mistket
X-Mistket-Event: invoice.confirmed
X-Mistket-Signature: sha256=…
{ "event": "invoice.confirmed", "data": {
"order_id": "2041", "status": "confirmed",
"pay_amount": "184.5", "paid_amount": "184.5",
"pay_currency": "USDT", "network": "TRC20" } } Store pay_currency and network with the order. They are useful for accounting and for support conversations later.
Verification in Node.js and Python
The same check in two other popular languages:
// Node.js (Express with express.raw for this route)
const exp = 'sha256=' + crypto.createHmac('sha256', secret)
.update(ts + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(sig));
# Python
exp = 'sha256=' + hmac.new(secret.encode(), f'{ts}.{raw}'.encode(),
hashlib.sha256).hexdigest()
ok = hmac.compare_digest(exp, sig) In both cases, make sure you read the raw request body. Many frameworks parse JSON automatically; once parsed and re-serialised, the bytes no longer match the signature.
Testing locally
Webhooks need a public HTTPS URL, so a local laptop cannot receive them directly. Two practical options: deploy the handler to a staging server early, or temporarily expose your local port through a tunnelling tool. Create small invoices pointing to the staging callback, then inspect the delivery logs in the merchant dashboard to see exactly what was sent and what your server answered.
Try to trigger every event type at least once during testing: paid, confirmed, underpaid and expired. When each branch of your handler has run against a real webhook, there is very little left to surprise you in production, and you will know exactly what the delivery logs look like when something does go wrong.
Step 4: handle every status
A webhook payload includes the order ID, status, expected amount (pay_amount), received amount (paid_amount), currency and network. Map each event to an action:
| Event | Meaning | Typical action |
|---|---|---|
invoice.paid | Transfer detected, confirmations pending | Show "payment received, confirming" |
invoice.confirmed | Required confirmations reached | Mark paid and fulfil |
invoice.underpaid | Below your tolerance | Wait for top-up or contact the customer |
invoice.overpaid | More than expected | Fulfil; decide on the difference |
invoice.expired | No valid payment in time | Release stock, keep order unpaid |
Underpayment tolerance, partial payments that add up and late payments within a grace window are handled by mistKET; your code only reacts to the final outcome.
Reliability: retries and idempotency
If your endpoint is down or returns an error, webhooks are retried with back-off for hours. That means you will sometimes receive the same event twice. Make your handler idempotent by order ID:
- Respond with a 2xx quickly, then process heavy work in a queue.
- Before fulfilling, check whether the order is already marked paid.
- Log the delivery ID and event so you can trace issues; the dashboard also shows webhook delivery logs.
Security checklist
A crypto payment API integration handles money, so treat it like any other payment code:
- Keep the API secret and webhook secret server-side only — never in front-end code or mobile apps.
- Restrict API access with the IP allow-list in the panel.
- Reject requests outside a short timestamp window.
- Configure invoice expiry to match your stock-reservation logic.
- Treat amounts as strings or decimals, never floats.
Where to go next
That is the complete crypto payment API integration: one signed request, one verified webhook and a status map. For the non-developer view of the same flow, read how to accept crypto payments on a website. Building a bot instead of a website? See accepting crypto payments in a Telegram bot. Request your API keys by applying on Telegram @mistnetwork, check the sample in the developer section, and manage keys at the merchant login.


