Crypto Payment Webhooks: How to Verify Signatures Safely

A webhook tells your store an order is paid, so it must be impossible to fake. How HMAC signatures work, with PHP and Node.js code you can copy.

Crypto Payment Webhooks: How to Verify Signatures Safely

A webhook is the message that says "this order is paid, ship it". If an attacker could send that message themselves, they could get goods for free. That is why every Cryptomo webhook is signed, and why your endpoint must verify the signature before trusting anything in it.

How the signature works

Each webhook request includes three headers:

X-Cryptomo-Event: invoice.paid
X-Cryptomo-Timestamp: 1767225600
X-Cryptomo-Signature: <hex HMAC-SHA256>

The signature is an HMAC-SHA256 of the timestamp, a dot and the raw request body, using your store's webhook secret (it starts with whsec_):

signature = hex( HMAC_SHA256( timestamp + "." + raw_body, webhook_secret ) )

Only Cryptomo and you know the secret, so only Cryptomo can produce a valid signature. Including the timestamp lets you reject old, replayed requests.

Verification in PHP

<?php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_CRYPTOMO_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_CRYPTOMO_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $ts . '.' . $raw, getenv('CRYPTOMO_WEBHOOK_SECRET'));

if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
$invoice = $event['data'];

if (in_array($invoice['status'], ['paid', 'paid_over', 'paid_late'], true)) {
    markOrderPaidOnce($invoice['order_id'], $invoice['id']); // idempotent
}
http_response_code(200);

Verification in Node.js (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/webhooks/cryptomo', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-Cryptomo-Timestamp') || '';
  const sig = req.get('X-Cryptomo-Signature') || '';
  const expected = crypto
    .createHmac('sha256', process.env.CRYPTOMO_WEBHOOK_SECRET)
    .update(ts + '.' + req.body.toString('utf8'))
    .digest('hex');

  const valid = sig.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (['paid', 'paid_over', 'paid_late'].includes(event.data.status)) {
    // mark event.data.order_id as paid, once
  }
  res.sendStatus(200);
});

Five mistakes to avoid

  1. Parsing before verifying. Compute the HMAC over the raw body. Re-encoded JSON (different spacing or key order) produces a different signature.
  2. Comparing with ==. Use a constant-time comparison (hash_equals, timingSafeEqual) so the secret cannot be guessed through timing.
  3. Ignoring the timestamp. Reject requests older than about five minutes to stop replays.
  4. Not being idempotent. The same event can arrive more than once (retries are normal). Record the invoice ID and do nothing if it is already processed.
  5. Trusting the status blindly for high-value orders. For extra safety, fetch the invoice with GET /api/v1/invoices/{id} before shipping expensive goods. Our official plugins do this.

Events you will receive

EventWhen
invoice.paidPaid in full (also sent for paid_late)
invoice.partially_paidLess than the amount arrived
invoice.overpaidMore than the amount arrived
invoice.expiredTime ran out
invoice.cancelledYou cancelled the invoice
invoice.refundedA refund you requested was sent

Reply with any 2xx status. Failed deliveries are retried for 24 hours (after 1 minute, 5 minutes, 15 minutes, 1, 3, 6, 12 and 24 hours), and you can resend any webhook from the dashboard.

Testing

  • Use Send test webhook in Dashboard → Stores to check your endpoint
  • Open an invoice in the dashboard to see each webhook delivery, the response code and body, and resend it
  • Rotate the webhook secret if it was ever exposed, then update your server

Full reference: API documentation → Webhooks. Using PHP? The PHP SDK verifies signatures for you.

Okumaya devam edin

Bugün kripto kabul etmeye başlayın

Başlamak ücretsiz. Ödeme başına %1. İstediğiniz zaman yükseltin.

Ücretsiz hesabınızı oluşturun