ReachBellDocs

Webhooks API

Webhooks push ReachBell events to your own HTTPS endpoint as they happen — a notification click, a dead token, a new subscriber, a finished campaign. Each delivery is a signed JSON POST so you can verify it came from us.

The management routes (create, update, delete, logs) authenticate with a dashboard JWT — send Authorization: Bearer <token> plus the x-org-id header. The event-list route is public. See Authentication.

Subscribable events

GET /webhooks/events

Returns every event type a webhook can subscribe to. This route needs no auth — it's safe to call when building your integration's setup UI.

curl https://api.reachbell.com/webhooks/events
{
  "events": [
    "push.clicked",
    "token.dead",
    "subscriber.created",
    "campaign.completed"
  ]
}
EventFires when
push.clickedA user tapped one of your notifications.
token.deadA push service reported a subscriber's token as expired or unregistered.
subscriber.createdA new subscriber registered via the SDK.
campaign.completedA campaign finished sending; aggregate stats are included in data.

A webhook with an empty events array receives all events. Provide a subset to receive only those.

Delivery payload

Every delivery is a POST with this envelope:

{
  "event": "push.clicked",
  "projectId": "proj_01HX2...",
  "timestamp": "2026-06-12T09:14:00.000Z",
  "data": {
    "subscriberId": "sub_01HX3...",
    "campaignId": "cmp_01HX5..."
  }
}
  • event — one of the subscribable events above.
  • projectId — the project the event belongs to.
  • timestamp — ISO 8601 send time.
  • data — event-specific payload.

Requests carry these headers:

Content-Type: application/json
User-Agent: ReachBell-Webhooks/1.0
X-ReachBell-Signature: sha256=<hex>

Verifying the signature

Every delivery includes an X-ReachBell-Signature header. It's an HMAC-SHA256 of the raw request body, keyed by your webhook's secret, formatted as sha256=<hex digest>. Verify it before trusting the payload.

Hash the raw bytes, not a re-serialized object. JSON.parse then JSON.stringify can reorder keys or change whitespace and break the comparison. Read the raw body first, verify, then parse.

Node.js (Express, using the raw body):

const crypto = require("crypto");
const express = require("express");
 
const app = express();
const WEBHOOK_SECRET = process.env.REACHBELL_WEBHOOK_SECRET;
 
// Capture the raw body so we can hash the exact bytes ReachBell signed.
app.use("/reachbell/webhook", express.raw({ type: "application/json" }));
 
app.post("/reachbell/webhook", (req, res) => {
  const signature = req.get("X-ReachBell-Signature") || "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.body).digest("hex");
 
  // Constant-time compare to avoid timing leaks.
  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
 
  if (!ok) return res.status(401).send("invalid signature");
 
  const payload = JSON.parse(req.body.toString("utf8"));
  console.log("event:", payload.event, payload.data);
 
  // Respond 2xx promptly — anything else counts as a failure (see retries).
  res.status(200).send("ok");
});

Respond with a 2xx status quickly. ReachBell treats any non-2xx response — or a connection that doesn't complete within 10 seconds — as a failed delivery. Do your heavy processing asynchronously after you've acknowledged.

Retries and auto-disable

Delivery is best-effort with bounded retries:

  • Each event is attempted up to 3 times per webhook: immediately, then after 10 seconds, then after 30 seconds.
  • A delivery succeeds on a 2xx response. On success the webhook's failedCount resets to 0 and lastSuccessAt updates.
  • If all 3 attempts fail, failedCount increments by one.
  • After 10 consecutive failures the webhook is auto-disabled (isActive set to false, disabledAt stamped) and stops receiving events until you re-enable it with a PATCH.

A single bad deploy that 500s for a few minutes won't disable your webhook — failedCount resets the moment one delivery succeeds. Auto-disable only trips on 10 failures in a row, which means a genuinely dead endpoint.

Security constraints

Webhook URLs must use https://. ReachBell refuses URLs that resolve to private, loopback, link-local, RFC1918, or cloud-metadata addresses — both at create time and again at delivery time, which closes the DNS-rebinding window. A blocked URL is rejected with 400 Bad Request on write, or logged as a failed delivery at send time.

Create a webhook

POST /webhooks/project/:projectId

url is required and must be HTTPS. events is optional (empty = all events). secret is optional — if you omit it, ReachBell generates a random one and returns it. Save the secret; you need it to verify signatures.

curl -X POST https://api.reachbell.com/webhooks/project/proj_01HX2... \
  -H "Authorization: Bearer eyJ..." \
  -H "x-org-id: org_01HX1..." \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://hooks.example.com/reachbell",
    "events": ["push.clicked", "campaign.completed"]
  }'

Success:

{
  "_id": "wh_01HXA...",
  "projectId": "proj_01HX2...",
  "url": "https://hooks.example.com/reachbell",
  "secret": "9f8c7b6a5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d",
  "events": ["push.clicked", "campaign.completed"],
  "isActive": true,
  "failedCount": 0,
  "createdAt": "2026-06-12T09:00:00.000Z"
}

List webhooks

GET /webhooks/project/:projectId
curl https://api.reachbell.com/webhooks/project/proj_01HX2... \
  -H "Authorization: Bearer eyJ..." \
  -H "x-org-id: org_01HX1..."

Update a webhook

PATCH /webhooks/:id/project/:projectId

Send any of url (re-validated for HTTPS + SSRF safety), events, or isActive. Set isActive: true to re-enable a webhook that was auto-disabled — that also lets failed deliveries start counting fresh again.

curl -X PATCH https://api.reachbell.com/webhooks/wh_01HXA.../project/proj_01HX2... \
  -H "Authorization: Bearer eyJ..." \
  -H "x-org-id: org_01HX1..." \
  -H "Content-Type: application/json" \
  -d '{ "isActive": true, "events": ["push.clicked", "token.dead", "campaign.completed"] }'

Delete a webhook

DELETE /webhooks/:id/project/:projectId
curl -X DELETE https://api.reachbell.com/webhooks/wh_01HXA.../project/proj_01HX2... \
  -H "Authorization: Bearer eyJ..." \
  -H "x-org-id: org_01HX1..."
{ "deleted": true }

Delivery logs

GET /webhooks/project/:projectId/logs

Returns recent delivery attempts, newest first. Filter to one webhook with webhookId, and cap the result with limit (default 50, max 200).

curl "https://api.reachbell.com/webhooks/project/proj_01HX2.../logs?webhookId=wh_01HXA...&limit=20" \
  -H "Authorization: Bearer eyJ..." \
  -H "x-org-id: org_01HX1..."

Each log entry records the outcome of one attempt:

[
  {
    "_id": "whl_01HXB...",
    "webhookId": "wh_01HXA...",
    "projectId": "proj_01HX2...",
    "event": "push.clicked",
    "statusCode": 200,
    "duration": 142,
    "success": true,
    "attempt": 1,
    "createdAt": "2026-06-12T09:14:00.123Z"
  },
  {
    "_id": "whl_01HXC...",
    "webhookId": "wh_01HXA...",
    "event": "campaign.completed",
    "statusCode": 0,
    "duration": 10003,
    "success": false,
    "error": "The operation was aborted due to timeout",
    "attempt": 3,
    "createdAt": "2026-06-12T08:00:30.000Z"
  }
]
  • statusCode — HTTP status from your endpoint. 0 means a network-level error (timeout, DNS, refused).
  • duration — round-trip time in milliseconds.
  • attempt — which try this was (1 = first, 2 = first retry, 3 = second retry).
  • error — present only on failures.

Logs auto-expire after 30 days via a TTL index. Pull anything you need into your own observability stack if you need longer retention.

Errors

All management routes use the standard error envelope.

{
  "statusCode": 400,
  "message": "webhook url must use https://",
  "requestId": "req_01HX4..."
}
StatusWhen
400 Bad RequestURL is missing, not a valid URL, not HTTPS, or resolves to a restricted network.
404 Not FoundThe webhook ID doesn't exist on this project.
403 ForbiddenMissing x-org-id, or your role can't write.

What's next?

  • Track clicks end to end with UTM tracking.
  • Reconcile campaign.completed payloads against the campaign stats in the dashboard.
  • Handle non-delivery cleanly with the error codes reference.