跳到主要内容
ArcBlock Community

Payment Kit Webhook Signatures

Twelve
支持
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 条回复

Xiao Fang7个月前

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个月前

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个月前

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个月前(edited)

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

Twelve6个月前

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

wangshijun6个月前

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个月前

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

回复