Skip to main content
ArcBlock Community

Payment Kit Webhook Signatures

Twelve
Support
payment-kitfeaturequalifiedresolved

Hello Team,

I have a few questions.

  1. How do we retrieve webhook signing secret for Payment Kit webhooks created in Blocklet dashboard?
  2. Which header contains the signature (X-Hub-Signature-256?) and exact verification algorithm?
  3. Is there an official Node helper for signature verification in @blocklet/payment-js?
  4. Are webhook event IDs globally unique and stable for idempotency?
  5. Do they publish webhook source IP ranges?

Any insight into these would be very helpful in helping me setup my automated system.

Thank you,

Mario

7 replies

Xiao Fang7 months ago

What specific scenario are you trying to solve? You can describe in detail what you want to do and the problems you are currently encountering.

Twelve7 months ago

Hi ArcBlock team, we’re implementing production webhook security for Payment Kit and need exact verification details.

Context:

  • We use Payment Kit webhooks to update DeBOS license state (trial/paid/suspended) in our control plane.
  • We currently use URL token + idempotency, but want to switch to official signature verification.

Questions:

  1. For webhooks created in Blocklet Dashboard -> Integrations, where do we retrieve the webhook signing secret?
  2. Which header contains the signature, and what is the exact verification algorithm/canonical payload?
  3. Is there an official Node helper in @blocklet/payment-js (or another ArcBlock package) for signature verification?
  4. Are webhook event.id values globally unique and stable across retries (safe for idempotency keys)?
  5. Do you publish webhook source IP ranges for allowlisting?

Thanks. A code sample for verification in Node would be very helpful.

wangshijun6 months ago

Hi, the current webhook in blocklet server do not have a signing mechanism, you can just embed the auth token in the webhook URL.

wangshijun6 months ago(edited)

Will ship a new version with a more secure signature mechanism soon.

Twelve6 months ago

Thank you! Here is short video demonstrating how I'm using the payment kit.

wangshijun6 months ago

Hi, latest blocklet server beta: 1.17.11-beta-20260227-073629-1efb93f4 is out, there is a stripe alike signing mechanism, you can upgrade to latest version, and try figure out how to obtain or rotate signing secret for your webhook endpoint.

Here is an example on how to verify the signature:

javascriptCopy
/**
 * Blocklet Server Webhook Signature Verification Example
 *
 * This example shows how to verify webhook signatures sent by Blocklet Server.
 *
 * Setup:
 *   1. Create a Webhook Endpoint in Blocklet Server admin panel
 *   2. Copy the Signing Secret shown after creation
 *   3. Set it as environment variable: WEBHOOK_SIGNING_SECRET=<secret>
 *   4. Set the Webhook Endpoint URL to: http://<your-host>:3999/webhook
 *
 * Run:
 *   WEBHOOK_SIGNING_SECRET=<secret> node webhook-verify-server.js
 *
 * Signature format:
 *   Header:  X-Webhook-Signature: t=<unix-timestamp>,v1=<hmac-sha256-hex>
 *   Signed:  HMAC-SHA256(secret, "<timestamp>.<json-body>")
 */

const crypto = require('crypto');
const express = require('express');

const app = express();
const PORT = process.env.PORT || 3999;
const SECRET = process.env.WEBHOOK_SIGNING_SECRET;

if (!SECRET) {
  console.error('Error: WEBHOOK_SIGNING_SECRET environment variable is required');
  console.error('Usage: WEBHOOK_SIGNING_SECRET=<secret> node webhook-verify-server.js');
  process.exit(1);
}

/**
 * Verify a Blocklet Server webhook signature.
 *
 * @param {string} secret    - The signing secret from Blocklet Server
 * @param {object} body      - The parsed JSON request body
 * @param {string} signature - The X-Webhook-Signature header value
 * @param {number} [tolerance=300] - Max allowed age in seconds (default: 5 min)
 * @returns {{ valid: boolean, reason?: string }}
 */
function verifyWebhookSignature(secret, body, signature, tolerance = 300) {
  if (!signature) {
    return { valid: false, reason: 'missing signature' };
  }

  const parts = signature.split(',');
  const timestamp = parts[0]?.replace('t=', '');
  const hmac = parts[1]?.replace('v1=', '');

  if (!timestamp || !hmac) {
    return { valid: false, reason: 'malformed signature' };
  }

  // Reject expired signatures to prevent replay attacks
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > tolerance) {
    return { valid: false, reason: `signature expired (${age}s old)` };
  }

  // Recompute HMAC and compare using timing-safe equality
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${JSON.stringify(body)}`)
    .digest('hex');

  const valid = crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(expected, 'hex'));

  return valid ? { valid: true } : { valid: false, reason: 'signature mismatch' };
}

app.use(express.json());

app.post('/webhook', (req, res) => {
  const result = verifyWebhookSignature(SECRET, req.body, req.headers['x-webhook-signature']);

  if (!result.valid) {
    console.log('[webhook] rejected:', result.reason);
    return res.status(401).json({ error: result.reason });
  }

  console.log('[webhook] verified:', req.body.type || 'unknown event');

  // TODO: handle your business logic here
  return res.json({ ok: true });
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}/webhook`);
});
Twelve6 months ago

thank you.. works great! Perhaps a rotate key button if possible would help for quick refresh.

Reply