Webhooks
Receive real-time payment events with signature verification.
BSync delivers real-time payment events to your server via HTTP POST. Configure your webhook endpoint in the Dashboard.
OpenAPI:
webhookssection — /docs/api-reference
Events
| Event | When |
|---|---|
PAYMENT_CREATED | Payment Intent created via API |
PAYMENT_MATCHED | SMS transaction matched to payment |
PAYMENT_PAID | Payment confirmed |
PAYMENT_CANCELLED | Payment cancelled |
PAYMENT_EXPIRED | Payment expired |
PAYMENT_REVIEW_REQUIRED | Manual review needed |
PAYMENT_REVIEW_APPROVED | Manual review approved |
PAYMENT_REVIEW_REJECTED | Manual review rejected |
Delivery Format
Every webhook is an HTTP POST to your configured endpoint:
POST https://merchant.example/webhooks/bsync
Content-Type: application/json
X-BSYNC-EVENT: PAYMENT_PAID
X-BSYNC-SIGNATURE: a1b2c3d4e5f6...
Body:
{
"eventId": "evt_abc123",
"eventType": "PAYMENT_PAID",
"createdAt": "2026-07-14T18:35:00.000Z",
"data": {
"paymentId": "pay_abc123",
"amount": 150
}
}Event-specific data fields are documented in the OpenAPI WebhookEvent* schemas.
Signature Verification
Every webhook includes an HMAC-SHA256 signature in the X-BSYNC-SIGNATURE header. Always verify before processing.
SDK:
import { verifySignature } from "@bsync/node-sdk";
const isValid = verifySignature({
payload: rawBody,
signature: req.headers["x-bsync-signature"],
secret: process.env.BSYNC_WEBHOOK_SECRET,
});
if (!isValid) {
return res.status(401).send("Invalid signature");
}Node.js (without SDK):
const crypto = require("crypto");
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
const signature = req.headers["x-bsync-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);Critical: Use the raw request body (as received), not a re-serialized JSON object. Parsing and re-stringifying changes the payload and breaks verification.
Express setup:
app.post(
"/webhooks/bsync",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body;
// verify and process
}
);Retries
BSync retries failed webhook deliveries automatically:
| Attempt | Backoff |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
Maximum 5 attempts. Delivery statuses: pending, success, failed, retrying, dead_letter.
Your endpoint must return HTTP 2xx within the timeout window to acknowledge receipt. Return non-2xx only if you want BSync to retry.
Replay Protection
Use eventId for idempotent processing:
const processed = await db.webhookEvents.findOne({ eventId: event.eventId });
if (processed) {
return res.status(200).json({ received: true });
}
await processPayment(event);
await db.webhookEvents.insertOne({ eventId: event.eventId, processedAt: new Date() });
res.status(200).json({ received: true });The same eventId is delivered on retries. Processing it twice must be safe.
Timeout
Respond within a few seconds. Offload heavy processing to a background queue:
app.post("/webhooks/bsync", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature({ ... })) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
webhookQueue.enqueue(event);
res.status(200).json({ received: true });
});Duplicate Protection
Three layers prevent duplicate order fulfillment:
- Signature verification — reject forged requests.
eventIddeduplication — skip already-processed events.- Server-side status check — call
getStatus()before fulfilling.
const status = await bsync.paymentIntents.getStatus(event.data.paymentId);
if (status.status !== "paid") {
return;
}
await fulfillOrder(event.data.paymentId);Best Practices
- Verify signatures on every request.
- Return 200 immediately; process asynchronously.
- Deduplicate by
eventId. - Confirm payment status server-side before fulfilling.
- Use HTTPS for your webhook endpoint.
- Log
eventId,eventType, andpaymentIdfor debugging. - Handle all event types, not just
PAYMENT_PAID.
Example Payloads
PAYMENT_CREATED
{
"eventId": "evt_001",
"eventType": "PAYMENT_CREATED",
"createdAt": "2026-07-14T18:30:00.000Z",
"data": {
"paymentId": "pay_abc123",
"amount": 150,
"currency": "EGP"
}
}PAYMENT_PAID
{
"eventId": "evt_002",
"eventType": "PAYMENT_PAID",
"createdAt": "2026-07-14T18:35:00.000Z",
"data": {
"paymentId": "pay_abc123",
"amount": 150
}
}PAYMENT_REVIEW_REQUIRED
{
"eventId": "evt_003",
"eventType": "PAYMENT_REVIEW_REQUIRED",
"createdAt": "2026-07-14T18:33:00.000Z",
"data": {
"reason": "Amount mismatch detected",
"reviewId": "rev_abc123"
}
}Full payload schemas: OpenAPI WebhookEvent* components.
Related
- Quick Start — webhook setup in the 5-minute flow
- Security — webhook verification details
- Best Practices — webhook queue pattern
- Examples: Webhook Receiver
Was this page helpful?
webhooks