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.createdPayment CreatedA new payment has been created.
payment.processingPayment ProcessingTransaction hash submitted, awaiting on-chain confirmation.
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.
payment.refundedPayment RefundedA refund for this payment has been completed.
refund.createdRefund CreatedA new refund has been initiated.
refund.completedRefund CompletedRefund transaction confirmed on-chain.
refund.failedRefund FailedRefund transaction failed.
settlement.requestedSettlement RequestedA settlement has been requested.
settlement.approvedSettlement ApprovedSettlement has been approved.
settlement.completedSettlement CompletedSettlement transaction confirmed.
settlement.failedSettlement FailedSettlement transaction failed.
customer.createdCustomer CreatedA new customer record was created.
customer.updatedCustomer UpdatedA customer record was updated.

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: v1=a4b3c2d1e0f9...
X-Crypax-Event: payment.confirmed
X-Crypax-Timestamp: 1704067200

{
  "id": "pay_01HZ...",
  "status": "confirmed",
  "txHash": "0xabcdef1234...",
  "blockNumber": 12345,
  "amount": "10.00",
  "currency": "native",
  "chainId": 41956
}

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-Signaturev1={hex} — HMAC-SHA256 of {timestamp}.{rawBody} signed with your webhook secret
X-Crypax-EventEvent type, e.g. payment.confirmed
X-Crypax-TimestampUnix timestamp (seconds) of when the event was sent. Use this to reject stale requests.

Verification Algorithm

The signed payload is timestamp + '.' + rawBody. Compute HMAC-SHA256 of this string using your webhook secret, encode as hex, and compare with the value after v1= in the signature header. Use crypto.timingSafeEqual to prevent timing attacks. Reject events with a timestamp older than 5 minutes.

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

function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
  // signatureHeader format: "v1=<hex>"
  const signedPayload = timestampHeader + '.' + rawBody;

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Reject stale requests (older than 5 minutes)
  const age = Math.floor(Date.now() / 1000) - Number(timestampHeader);
  if (age > 300) {
    throw new Error('Webhook timestamp too old');
  }

  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();

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

  // Build signed payload: timestamp.rawBody
  const signedPayload = timestamp + '.' + req.body.toString();
  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .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 (eventType) {
    case 'payment.confirmed':
      console.log('Payment confirmed:', payload.id, payload.txHash);
      break;
    case 'payment.failed':
      console.log('Payment failed:', payload.id);
      break;
    case 'payment.expired':
      console.log('Payment expired:', payload.id);
      break;
    case 'refund.completed':
      console.log('Refund completed:', 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.ts
import { Crypax } from '@crypax/node';
import express from 'express';

const app = express();
const crypax = new Crypax('sk_live_...');

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

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

    switch (event.type) {
      case 'payment.confirmed':
        console.log('Confirmed:', event.data.id, event.data.txHash);
        break;
      case 'payment.expired':
        console.log('Expired:', event.data.id);
        break;
      case 'refund.completed':
        console.log('Refund completed:', event.data.id);
        break;
      case 'customer.created':
        console.log('New customer:', event.data.id);
        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