Skip to content

Webhooks

Signed, real-time event deliveries for links and subscriptions.

Updated View as Markdown

Webhooks push events to your server the moment they happen — no polling. Configure endpoints under Settings → Webhooks on the Business plan. Each endpoint has its own signing secret (shown once) and subscribes to any of the events below.

Events

Event Fires when
link.created A link is created.
link.updated A link’s fields change.
link.deleted A link is deleted.
link.clicked A link receives a (human) click.
link.expired A link passes its expiry date.
link.disabled A link is disabled.
link.click_limit_reached A link hits its click limit.
subscription.updated Your plan or subscription changes.

Delivery format

Each delivery is a JSON POST to your URL. The envelope wraps the resource in data (link events carry the same fields as the link resource).

{
  "id": "evt_01J8XZ...",
  "event": "link.created",
  "created_at": "2026-07-22T10:00:00+00:00",
  "data": { "id": 42, "slug": "spring-sale", "...": "link resource fields" }
}
Header Value
X-Inbio-Event The event name (e.g. link.created).
X-Inbio-Delivery Unique delivery id.
X-Inbio-Signature t=<unix>,v1=<hex> — see below.

Verifying signatures

Compute v1 = HMAC-SHA256(secret, "<t>.<raw body>") over the exact raw request body, compare in constant time, and reject stale timestamps to prevent replays.

[$t, $v1] = explode(',', $signature);
$t  = substr($t, 2);   // strip "t="
$v1 = substr($v1, 3);  // strip "v1="

$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);

abort_unless(
    hash_equals($expected, $v1) && abs(time() - (int) $t) < 300,
    401,
);
import crypto from "node:crypto";

const [tPart, vPart] = signature.split(",");
const t = tPart.slice(2);
const v1 = vPart.slice(3);

const expected = crypto
  .createHmac("sha256", secret)
  .update(`${t}.${rawBody}`)
  .digest("hex");

const valid =
  crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) &&
  Math.abs(Date.now() / 1000 - Number(t)) < 300;
import hashlib, hmac, time

t_part, v_part = signature.split(",")
t, v1 = t_part[2:], v_part[3:]

expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()

valid = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) < 300

Retries

Non-2xx responses are retried 5 times with growing backoff (1m, 10m, 1h, 6h). After 20 consecutive failures the endpoint is disabled automatically — you can re-enable it and replay individual deliveries from Settings → Webhooks, where every delivery and its response code is logged.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close