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:
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
| Header | Value |
|---|---|
| X-Superchat-Signature | t=<unix-seconds>,v1=<hex hmac> |
| X-Superchat-Event | The event name, for example payment.completed |
| X-Superchat-Delivery | Delivery 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
2xxquickly 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 atest.pingwithPOST /api/v1/webhooks/:id/testwhile you wire an endpoint up. - Make your handler idempotent: the platform can deliver the same event more than once.
@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 Name | Trigger Description |
|---|---|
| payment.created | A checkout session was created. Useful for reconciling your own order records. |
| payment.completed | Dispatched immediately upon confirmed payment approval from EPS or Stripe. |
| payment.refunded | Dispatched when a completed payment has been refunded to the customer. |
| test.ping | Sent 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.
