Webhooks
Signed HTTPS delivery, retries, and idempotency.
FeedByte POSTs JSON to an HTTPS endpoint you register on an App. Delivery is at-least-once. Verify every request before you trust it.
Register an endpoint
In Developers → Apps → Webhooks, add a URL such as https://crm.example.com/webhooks/feedbyte. Subscribe to the event types you need (see Events). FeedByte issues a signing secret whsec_… per endpoint.
You can also manage endpoints with the API (webhook:manage scope).
Request
POST /webhooks/feedbyte HTTP/1.1
Content-Type: application/json
FeedByte-Signature: t=1726482420,v1=8f3c1a…The body is one event:
{
"id": "evt_2Q8m0k1",
"type": "response.submitted",
"created": "2026-09-16T11:47:00.000Z",
"organizationId": "org_7c…",
"data": {}
}id is the idempotency key. Store it and skip duplicates.
Verify the signature
The v1 value is HMAC-SHA256 of {timestamp}.{rawBody} using the endpoint secret. Reject the request if:
- The header is missing or malformed.
- No
v1digest matches. |now - t|is greater than five minutes.
Always hash the raw body. Parsing JSON and re-serializing will break the digest.
import { FeedByte } from "@feedbyte/node";
const feedbyte = new FeedByte("fb_live_4e8c…");
export async function POST(req: Request) {
const rawBody = await req.text();
const event = feedbyte.webhooks.constructEvent(
rawBody,
req.headers.get("feedbyte-signature"),
process.env.FEEDBYTE_WEBHOOK_SECRET,
);
switch (event.type) {
case "response.submitted":
await enqueueCrmJob(event.id, event.data);
break;
default:
break;
}
return new Response("ok", { status: 200 });
}If you verify by hand:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const timestamp = parts.t;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const actual = parts.v1 ?? "";
if (actual.length !== expected.length) throw new Error("bad signature");
if (
!timingSafeEqual(Buffer.from(actual), Buffer.from(expected))
) {
throw new Error("bad signature");
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error("stale timestamp");
}
}Return 4xx for a bad signature so FeedByte does not retry as if your handler failed. Return 2xx only after the event is durably accepted (queued is enough).
Retries
Failed deliveries (network error, 5xx, or timeout) retry with exponential backoff for up to 24 hours, starting around five minutes. After repeated failures the endpoint is disabled and the App owner is notified. Successful 2xx stops the retry series.
FeedByte may send the same id more than once. Handlers must be idempotent.