Bridge by Wekoya / Docs / Your account

Webhooks

Updated

Add an HTTPS URL on the End users page, or with POST /v1/webhooks and { "url": "...", "events": ["*"] }. The signing secret is shown once. Events:

{
  "id": "5b0c9f5e-...",
  "type": "end_user.budget_low",
  "created_at": "2026-10-14T08:03:11Z",
  "data": {
    "id": "e3a1...",
    "external_id": "user_8412",
    "status": "active",
    "allowance_minor": "500000",
    "top_up_minor": "0",
    "used_minor": "402500",
    "remaining_minor": "97500",
    "period": "monthly",
    "period_end": "2026-11-01T09:12:00Z",
    "metadata": { "plan": "student" },
    "threshold_percent": 80
  }
}

Each delivery carries Bridge-Signature: t=<unix time>,v1=<hex>, an HMAC-SHA256 of t.body with your secret. Check it, and reject anything more than five minutes old. Answer with any 2xx within 10 seconds. Failures are retried with backoff for about a day, so handle the same id arriving twice.

import crypto from "node:crypto"

// Read the raw body before any JSON parser touches it: the signature is over the exact bytes.
export function verifyBridge(secret, rawBody, header) {
  const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.trim().split("=")))
  const t = Number(parts.t)
  if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest()
  const given = Buffer.from(parts.v1, "hex")
  return given.length === expected.length && crypto.timingSafeEqual(given, expected)
}

// Express:
// app.post("/bridge-webhook", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyBridge(process.env.BRIDGE_WEBHOOK_SECRET, req.body.toString(), req.get("Bridge-Signature")))
//     return res.sendStatus(400)
//   const event = JSON.parse(req.body)
//   if (event.type === "end_user.budget_exhausted") notifyUser(event.data.external_id)
//   res.sendStatus(200)
// })