FeedByte

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 v1 digest 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.

On this page