MEXICOP2P

Signature Verification

Verify webhook signatures with Node.js, Python, and curl examples.

How it works

Every webhook delivery is signed with HMAC-SHA256 using your webhook secret. The signature is computed over the string {timestamp}.{raw JSON body}.

Verification steps

  1. Extract the X-Webhook-Signature and X-Webhook-Timestamp headers
  2. Concatenate: "{timestamp}.{raw request body}"
  3. Compute HMAC-SHA256 with your webhook secret
  4. Compare the computed signature with the header value
  5. Optionally check that the timestamp is within 5 minutes to prevent replay attacks

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(secret, payload, timestamp, signature) {
  const data = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(data)
    .digest('hex');

  // Timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

// Express middleware example
app.post('/webhooks/mexicop2p', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const payload = req.body.toString();

  // Check timestamp is within 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return res.status(400).send('Timestamp too old');
  }

  if (!verifyWebhookSignature(process.env.WEBHOOK_SECRET, payload, timestamp, signature)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  console.log(`Received ${event.event} for order ${event.data.orderId}`);

  // Process asynchronously
  processWebhook(event);

  res.status(200).send('ok');
});

Python

import hashlib
import hmac
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

def verify_webhook_signature(secret, payload, timestamp, signature):
    data = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret.encode('utf-8'),
        data.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/mexicop2p', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    payload = request.get_data(as_text=True)

    # Check timestamp is within 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        abort(400, 'Timestamp too old')

    if not verify_webhook_signature(WEBHOOK_SECRET, payload, timestamp, signature):
        abort(401, 'Invalid signature')

    event = request.get_json()
    print(f"Received {event['event']} for order {event['data']['orderId']}")

    return 'ok', 200

Testing with curl

Generate a test signature and send a webhook to your local server:

# Set your webhook secret
SECRET="your_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"event":"order.completed","timestamp":"2025-06-15T12:35:00.000Z","data":{"orderId":"ord_test","status":"COMPLETED"}}'

# Compute HMAC-SHA256
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

# Send test webhook
curl -X POST http://localhost:3000/webhooks/mexicop2p \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Id: del_test_001" \
  -H "X-Webhook-Signature: $SIGNATURE" \
  -H "X-Webhook-Timestamp: $TIMESTAMP" \
  -d "$PAYLOAD"

Security notes

  • Store your webhook secret securely (environment variable, secrets manager)
  • Always verify signatures before processing payloads
  • Use the raw request body for signature verification (not parsed JSON)
  • Implement timestamp checking to prevent replay attacks
  • Use timing-safe comparison functions to prevent timing attacks

On this page