A crypto payment webhook is an HTTP POST your gateway sends to a URL on your server the moment a payment confirms on-chain. To handle it safely you do four things: receive the raw POST, verify the HMAC-SHA256 signature against your webhook secret, check idempotency so a retry does not fulfil twice, then update the order and trigger fulfillment. This guide shows each step with Node.js and PHP code.
What Is a Webhook?
When a crypto payment confirms on-chain, the payment gateway sends an HTTP POST request to a URL you specify. This is called a webhook. Your server receives the request, checks that it is genuine, and updates the order accordingly. It is the difference between polling an API every few seconds and being told the instant a payment lands. For context on where this fits, see how crypto payment gateways work.
Setting Up Your Webhook URL
In your CryptoGate dashboard, go to Settings → Webhooks and add your endpoint URL, for example:
https://yoursite.com/webhooks/cryptogate
Make sure this URL is publicly accessible (not localhost), served over HTTPS, and returns a 200 OK response when it receives a valid payload. A non-200 response signals failure and the gateway will retry.
The Webhook Payload
CryptoGate sends a JSON body like this on a completed payment:
{
"event": "payment.completed",
"order_id": "order_123",
"transaction_id": "TXN-abc123",
"amount_requested": "49.99",
"amount_received": "49.99",
"currency": "USD",
"coin": "USDT",
"network": "tron",
"txid": "abc123def456",
"timestamp": 1746000000
}
Key fields at a glance:
| Field | Meaning |
|---|---|
| event | What happened (payment.completed, payment.partial, etc.) |
| order_id | Your reference - use this to look up the order |
| amount_requested / amount_received | Compare these to detect under or overpayment |
| coin / network | Which asset and chain the payment arrived on |
| txid | On-chain transaction hash for your records |
Verifying the Signature
Never trust a webhook without verifying it - the URL is public and anyone could POST to it. CryptoGate includes an X-CryptoGate-Signature header with every request. It is an HMAC-SHA256 of the raw request body, signed with your webhook secret.
Two rules that catch most developers out: verify against the raw request body (not a re-serialized JSON object, which may reorder keys), and compare with a constant-time function to avoid timing attacks.
Verification in Node.js:
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Verification in PHP:
function verifyWebhook($rawBody, $signature, $secret) {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
In Express, capture the raw body before any JSON middleware parses it:
app.use('/webhooks/cryptogate',
express.raw({ type: 'application/json' }));
app.post('/webhooks/cryptogate', (req, res) => {
const sig = req.header('X-CryptoGate-Signature');
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const payload = JSON.parse(req.body.toString());
// ... handle event
res.status(200).send('ok');
});
Handling Events
CryptoGate sends webhooks for the following events:
payment.completed- payment confirmed on-chain, safe to fulfillpayment.partial- customer sent less than the required amountpayment.expired- session timed out before payment arrivedpayment.overpaid- customer sent more than required
For most stores, you only need to handle payment.completed to mark the order as paid and trigger fulfillment. Handle payment.partial and payment.overpaid if you want to flag those orders for manual review rather than auto-fulfilling.
Idempotency - Handle Duplicates
Webhooks can occasionally be delivered more than once (network retries, or a slow response that the gateway treats as failed). Before fulfilling an order, check whether it has already been marked as paid:
const order = await db.orders.findOne({ id: payload.order_id });
if (order.status === 'paid') {
return res.status(200).send('already processed');
}
await db.orders.update(
{ id: payload.order_id },
{ status: 'paid' }
);
await fulfillOrder(payload.order_id);
Always return 200 for a duplicate you have already handled - this tells the gateway to stop retrying. Reserve non-200 responses for genuine failures you want retried.
A Safe Handler in Order
| Step | Why it matters |
|---|---|
| 1. Read the raw body | Needed for an accurate signature check |
| 2. Verify the HMAC signature | Rejects forged requests to your public URL |
| 3. Check idempotency | Prevents double fulfillment on retries |
| 4. Update the order, then fulfill | Records the payment before side effects |
| 5. Return 200 quickly | Avoids timeouts that trigger more retries |
Testing Webhooks Locally
Use a tunneling tool like ngrok to expose your local server during development:
ngrok http 3000
Copy the HTTPS URL ngrok provides and set it as your webhook URL in the CryptoGate dashboard. You can then use sandbox mode to trigger test events and inspect the full request payload before going live.
Putting It to Work
Webhooks are the backbone of any automated crypto checkout. If you are wiring one into a store rather than custom code, the platform plugins handle this for you - see our WooCommerce crypto payments setup guide and Shopify crypto payments guide. To get an API key and webhook secret, create a CryptoGate account.
Summary
Webhooks are straightforward: receive the POST, verify the HMAC signature against the raw body, check idempotency, update the order, fulfill, and return 200. The whole handler is under 30 lines of code regardless of your stack.
Frequently Asked Questions
What is a crypto payment webhook?
It is an HTTP POST your payment gateway sends to a URL on your server the moment a payment confirms on-chain. Your server verifies it is genuine and updates the order, so you learn about payments instantly instead of polling an API.
How do I verify a webhook signature?
Compute an HMAC-SHA256 of the raw request body using your webhook secret, then compare it to the X-CryptoGate-Signature header with a constant-time comparison. Always hash the raw body, not a re-serialized object, or the signatures will not match.
Why must I handle duplicate webhooks?
Networks retry, and a slow or failed response can cause the same event to be delivered more than once. Check whether the order is already marked paid before fulfilling, and return 200 for duplicates so the gateway stops retrying.
What response should my webhook return?
Return 200 OK once you have accepted the event (including duplicates you have already processed). Return a non-200 status only for genuine failures you want the gateway to retry, and respond quickly to avoid timeouts.
How do I test webhooks on localhost?
Use a tunnel like ngrok to expose your local server with a public HTTPS URL, set that URL in your dashboard, and trigger test events in sandbox mode so you can inspect the full payload before going live.