Superchat BD
Developer Docs
v1.0
Real-time Events

Webhooks & Signature Verification

Superchat BD uses webhooks to notify your server immediately when an asynchronous event occurs, such as a customer completing payment.

Cryptographic Signature Verification

Every webhook request sent by Superchat includes a custom signature header:X-Superchat-Signature: t=1700000000,v1=a1b2c3d4e5...

The signature is computed as:

HMAC_SHA256(timestamp + "." + raw_body, webhook_signing_secret)

This guarantees that the payload was not altered in transit and originated from Superchat BD.

Node.js Express Verification Example

import express from 'express';
import crypto from 'crypto';

const app = express();
// CRITICAL: Read the raw body as a Buffer or string for signature verification
app.use(express.raw({ type: 'application/json' }));

app.post('/api/webhooks/superchat', (req, res) => {
  const signatureHeader = req.headers['x-superchat-signature'];
  const secret = process.env.SUPERCHAT_WEBHOOK_SECRET; // whsec_...
  const rawBody = req.body.toString('utf8');

  // 1. Parse header
  const parts = signatureHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t='))?.slice(2);
  const signature = parts.find(p => p.startsWith('v1='))?.slice(3);

  // 2. Compute expected HMAC
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // 3. Timing-safe comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );

  if (!isValid) {
    return res.status(400).send('Invalid signature');
  }

  // 4. Process event
  const event = JSON.parse(rawBody);
  if (event.event === 'payment.completed') {
    const payment = event.data;
    console.log('Fulfilling order:', payment.metadata.orderId);
  }

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

What the request looks like

HeaderValue
X-Superchat-Signaturet=<unix-seconds>,v1=<hex hmac>
X-Superchat-EventThe event name, for example payment.completed
X-Superchat-DeliveryDelivery ID (del_…) for support and log correlation
{
  "id": "evt_38d3765777ad12d2900a001e",
  "event": "payment.completed",
  "created_at": "2026-09-20T05:23:08.914Z",
  "data": { /* the payment object, exactly as returned by the API */ }
}

Signatures older than 300 seconds are rejected. Pass toleranceInSeconds to change that, or 0 to disable the replay check.

For payment.refunded, data carries an extra refund_reason. A test.ping sends { event, timestamp, developer_id, message } instead of a payment.

Delivery, retries and debugging

  • The platform makes a single attempt per event with an 8 second timeout; failures are recorded, not retried. Respond 2xx quickly and process the event asynchronously.
  • Deliveries are fire-and-forget: a failing endpoint never blocks or reverses the payment itself.
  • Every attempt is logged with its HTTP status and a truncated response body — inspect them with GET /api/v1/webhooks/deliveries, or send a test.ping with POST /api/v1/webhooks/:id/test while you wire an endpoint up.
  • Make your handler idempotent: the platform can deliver the same event more than once.
Verify with the SDK instead of hand-rolling HMAC

@superchatbd/sdk exposes verifyWebhookSignature() (returns a boolean, safe in middleware) and constructWebhookEvent() (throws a typed SuperchatSignatureError and returns the parsed envelope). The API can also verify a signature for you: POST /api/v1/webhooks/verify is public and needs no API key. See the TypeScript / Node.js SDK page.

Supported Webhook Events

Event NameTrigger Description
payment.createdA checkout session was created. Useful for reconciling your own order records.
payment.completedDispatched immediately upon confirmed payment approval from EPS or Stripe.
payment.refundedDispatched when a completed payment has been refunded to the customer.
test.pingSent only by POST /api/v1/webhooks/:id/test to check an endpoint.

Subscribing with * receives every event. There is no payment.failed event today — treat a payment that never reaches completed as abandoned and reconcile it with GET /api/v1/payments/:id.