CrypaxDocs

Webhooks

Receive real-time payment events via signed POST requests. Crypax delivers webhooks with HMAC-SHA256 signatures so you can verify authenticity before processing.


Overview

When a payment's status changes, Crypax sends a POST request to your configured webhook URL. You must respond with a 2xx status code within 10 seconds. If delivery fails, Crypax retries up to 4 times. Configure your webhook URL in the Webhooks section of the dashboard.

Event Types

Crypax fires the following events. Subscribe to individual events or leave the selection empty to receive all.

EventNameWhen
payment.confirmedPayment ConfirmedTransaction verified on-chain. Safe to fulfill the order.
payment.failedPayment FailedTransaction submitted but on-chain verification failed.
payment.expiredPayment ExpiredPayment was not confirmed before expiresAt.

Payload Structure

Crypax sends a JSON body along with two headers: X-Crypax-Signature for authentication and X-Crypax-Event for the event type.

POST /your-webhook-endpoint
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Crypax-Signature: sha256=a4b3c2d1e0f9...
X-Crypax-Event: payment.confirmed

{
  "id": "pay_01HZ...",
  "status": "confirmed",
  "txHash": "0xabcdef1234...",
  "blockNumber": 12345,
  "amount": "1000000000000000000",
  "currency": "native",
  "orderId": "order_123"
}

Payload Fields

FieldTypeDescription
idstringUnique payment ID
statusstringPayment status at the time of the event
txHashstring | nullOn-chain transaction hash (null if not submitted)
blockNumbernumber | nullBlock number where tx was included (null if not confirmed)
amountstringAmount in smallest unit (wei for PLM)
currencystring'native' for PLM, or ERC20 contract address
orderIdstring | nullYour internal order ID passed when creating the payment

Signature Verification

Every webhook request includes an X-Crypax-Signature header. Always verify this before processing the event to ensure the request came from Crypax.

HeaderDescription
X-Crypax-Signaturesha256={HMAC-SHA256 hex of raw body, signed with your webhook secret}
X-Crypax-EventEvent type, e.g. payment.confirmed

Verification Algorithm

Compute HMAC-SHA256 of the raw request body (bytes before JSON parsing) using your webhook secret, then compare with the value after the sha256= prefix. Use crypto.timingSafeEqual to prevent timing attacks.

verify-signature.js
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  // signatureHeader format: "sha256=<hex>"
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

Handler Examples

A webhook handler must parse the raw body, verify the signature, and respond with 200 immediately — defer any heavy work to a background queue.

Express.js (manual verification)

webhook-handler.js
const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANT: Use raw body parser — JSON.parse changes byte representation
app.post('/webhooks/crypax', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-crypax-signature'];
  const event = req.headers['x-crypax-event'];
  const secret = process.env.CRYPAX_WEBHOOK_SECRET;

  // Verify signature
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body.toString());

  switch (event) {
    case 'payment.confirmed':
      // Fulfill the order
      console.log('Payment confirmed:', payload.id, payload.txHash);
      fulfillOrder(payload.orderId);
      break;
    case 'payment.failed':
      console.log('Payment failed:', payload.id);
      break;
    case 'payment.expired':
      console.log('Payment expired:', payload.id);
      break;
  }

  res.json({ received: true });
});

Express.js (@crypax/node SDK)

The Node.js SDK provides a <code>constructEvent</code> helper that verifies the signature and returns a typed event object.

webhook-handler-sdk.js
const { Crypax } = require('@crypax/node');
const express = require('express');

const app = express();
const crypax = new Crypax('sk_live_your_secret_key');

app.post('/webhooks/crypax', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-crypax-signature'];

  try {
    const event = crypax.webhooks.constructEvent(
      req.body,
      signature,
      process.env.CRYPAX_WEBHOOK_SECRET
    );

    switch (event.type) {
      case 'payment.confirmed':
        fulfillOrder(event.data.orderId);
        break;
      case 'payment.expired':
        cancelOrder(event.data.orderId);
        break;
    }

    res.json({ received: true });
  } catch (err) {
    res.status(400).json({ error: 'Invalid signature' });
  }
});

Retry Policy

If your endpoint does not respond with a 2xx status within 10 seconds, Crypax retries delivery with the following schedule:

AttemptDelayNotes
10sInitial delivery
21sFirst retry
35sSecond retry
430sFinal retry — no further attempts after this

Once a 2xx response is received, retries stop. Each attempt is logged in the Delivery Logs section of the webhook detail page in your dashboard.

Best Practices

  • Use the raw body for signature verification. Pass express.raw() or equivalent. Parsing JSON first changes the byte representation and invalidates the signature.
  • Use timing-safe comparison. Use crypto.timingSafeEqual instead of === to prevent timing side-channel attacks.
  • Make your handler idempotent. The same event may be delivered more than once. Use the id field to deduplicate.
  • Respond 200 immediately. Do not wait for database writes or downstream calls. Acknowledge first, process asynchronously.
  • HTTPS endpoints only. Crypax rejects webhook URLs that do not use https://.

Webhook Guide | Crypax