Developer Docs

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: webhooks section — /docs/api-reference

Events

EventWhen
PAYMENT_CREATEDPayment Intent created via API
PAYMENT_MATCHEDSMS transaction matched to payment
PAYMENT_PAIDPayment confirmed
PAYMENT_CANCELLEDPayment cancelled
PAYMENT_EXPIREDPayment expired
PAYMENT_REVIEW_REQUIREDManual review needed
PAYMENT_REVIEW_APPROVEDManual review approved
PAYMENT_REVIEW_REJECTEDManual 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:

AttemptBackoff
1Immediate
21 minute
35 minutes
415 minutes
51 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:

  1. Signature verification — reject forged requests.
  2. eventId deduplication — skip already-processed events.
  3. 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, and paymentId for 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.

Was this page helpful?

webhooks

Command Palette

Search for a command to run...