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.

Updated: 5 min read
Crypto payment API integration: signed POST request creating an invoice and webhook response

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:

  1. Order created — your backend calls POST /api/v1/invoices with amount, currency, order ID and callback URL.
  2. Redirect — you send the customer to the returned checkout_url.
  3. Payment — the customer picks a network and pays on the hosted page; the rate is locked for the invoice lifetime.
  4. Webhook — mistKET POSTs invoice.paid, invoice.confirmed or an exception event to your callback URL.
  5. 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:

HeaderPurpose
X-Mistket-EventEvent name, e.g. invoice.confirmed
X-Mistket-TimestampUnix time used in the signature
X-Mistket-Signaturesha256= + HMAC-SHA256 of {timestamp}.{raw body} with your webhook secret
X-Mistket-DeliveryDelivery 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).

Key takeaway: the webhook — not the customer's browser returning to your site — is the source of truth. Never mark an order paid without a valid signature.

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:

EventMeaningTypical action
invoice.paidTransfer detected, confirmations pendingShow "payment received, confirming"
invoice.confirmedRequired confirmations reachedMark paid and fulfil
invoice.underpaidBelow your toleranceWait for top-up or contact the customer
invoice.overpaidMore than expectedFulfil; decide on the difference
invoice.expiredNo valid payment in timeRelease 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.
Tip: before going live, pay a small invoice yourself, send a deliberately short amount to see the underpaid flow, and let one invoice expire. You will have tested every branch in under an hour.

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.

Frequently asked questions

Do I need an SDK to integrate a crypto payment API?
Not with mistKET. It uses plain HTTPS and JSON; any language that can make an HTTP request and compute an HMAC-SHA256 can integrate.
How are mistKET API requests authenticated?
Each request sends X-Api-Key, X-Api-Timestamp and X-Api-Signature, where the signature is the hex HMAC-SHA256 of '{timestamp}.{raw body}' using your API secret.
How do I verify a crypto payment webhook?
Recompute HMAC-SHA256 of '{timestamp}.{raw body}' with your webhook secret, compare it to X-Mistket-Signature in constant time, and reject old timestamps.
What if my server misses a webhook?
Webhooks are retried with back-off for hours, and you can also fetch the invoice status via the API. Make your handler idempotent by order ID.
Can I force a specific network through the API?
Yes. You can fix the network in the API call; otherwise the customer chooses it on the hosted payment page.
Which events should trigger order fulfilment?
Fulfil on invoice.confirmed. Use invoice.paid only to show progress, and handle underpaid, overpaid and expired events according to your policy.

Related articles