Webhooks
We POST a signed JSON body to your endpoint when something happens in your workspace. Add an endpoint in the dashboard under Webhooks, then use the simulator below to see exactly what arrives before you write any receiver code.
Try it against your own URL
Fires a real, signed event at any public address — your own server, a tunnel to your laptop, or a throwaway inbox like webhook.site. The signing secret is generated for this one send and shown to you, so you can verify the signature yourself.
The address has to be publicly reachable. Private and loopback addresses are refused — otherwise this would be a way to make our servers probe networks they have no business in.
Verify every request
Your endpoint URL is a public address. Anyone who learns it can POST to it, so the signature is the only thing separating an event from us and a body somebody made up. Check it before you act on anything.
import crypto from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
// The header looks like: t=1755691200,v1=abc123...
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string])
);
const timestamp = Number(parts.t);
if (!timestamp) return false;
// Reject anything older than five minutes. Without this, a signature
// captured once can be replayed forever.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time. A plain === leaks how many characters matched.
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1 ?? "")
);
}Sign the raw body, byte for byte
Parse the JSON after you verify, never before. Re-serialising the body changes the bytes — key order, whitespace, number formatting — and the signature will not match. This is the single most common reason a verifier that looks correct rejects everything.
What to expect
- Answer fast. Return a 2xx within ten seconds. Do the actual work afterwards, on your own queue — a slow handler turns into a timeout, and a timeout turns into a retry.
- Expect duplicates. Retries mean the same event can arrive twice. Every body carries an
id; treat it as an idempotency key. - A 4xx is not retried. We read it as "this request is wrong" and stop. If you want a retry, return a 5xx.
- Expect new fields. We add fields to payloads without a version bump. Ignore what you do not recognise rather than failing on it.
Events
Every payload is wrapped in the same envelope. Switch on event, and read the details from data.
click.receivedHigh volume
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "click.received",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
},
"click": {
"id": "9a8b7c6d-2222-4333-8444-555566667777",
"timestamp": "2026-08-20T12:00:00Z",
"country": "US",
"city": "San Francisco",
"device": "mobile",
"browser": "Safari",
"os": "iOS",
"referrer": "https://example.net/blog",
"ip": "203.0.113.42",
"isUnique": true
}
}
}click.milestone
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "click.milestone",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
},
"milestone": 100000,
"totalClicks": 100000,
"reachedAt": "2026-08-20T12:00:00Z"
}
}click.spike
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "click.spike",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
},
"window": "15m",
"clicksInWindow": 4200,
"baselinePerWindow": 60,
"multiplier": 70,
"detectedAt": "2026-08-20T12:00:00Z"
}
}link.created
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "link.created",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
},
"createdAt": "2026-08-20T12:00:00Z"
}
}link.updated
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "link.updated",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
},
"changed": [
"destinationUrl",
"title"
],
"updatedAt": "2026-08-20T12:00:00Z"
}
}link.deleted
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "link.deleted",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls"
},
"deletedAt": "2026-08-20T12:00:00Z"
}
}lead.created
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "lead.created",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"lead": {
"id": "1a2b3c4d-3333-4444-8555-666677778888",
"email": "someone@example.com",
"name": "Alex Doe",
"source": "bridge_page",
"createdAt": "2026-08-20T12:00:00Z"
},
"page": {
"id": "2b3c4d5e-4444-4555-8666-777788889999",
"slug": "launch"
}
}
}page.viewedHigh volume
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "page.viewed",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"page": {
"id": "2b3c4d5e-4444-4555-8666-777788889999",
"slug": "launch",
"title": "Launch"
},
"view": {
"timestamp": "2026-08-20T12:00:00Z",
"country": "GB",
"device": "desktop",
"referrer": "https://example.net/"
}
}
}page.clickHigh volume
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "page.click",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"page": {
"id": "2b3c4d5e-4444-4555-8666-777788889999",
"slug": "launch"
},
"block": {
"id": "cta-1",
"type": "button",
"label": "Buy now"
},
"timestamp": "2026-08-20T12:00:00Z",
"country": "GB"
}
}sale.completed
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "sale.completed",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"sale": {
"id": "3c4d5e6f-5555-4666-8777-888899990000",
"kind": "digital_product",
"amountCents": 4900,
"currency": "USD",
"buyerEmail": "someone@example.com",
"completedAt": "2026-08-20T12:00:00Z"
},
"page": {
"id": "2b3c4d5e-4444-4555-8666-777788889999",
"slug": "launch"
}
}
}assistant.intent
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "assistant.intent",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"intent": "pricing_question",
"confidence": 0.87,
"question": "How much is the business plan?",
"answered": true,
"conversationId": "4d5e6f70-6666-4777-8888-999900001111",
"timestamp": "2026-08-20T12:00:00Z"
}
}rss.item.created
{
"id": "evt_7f8a9b0c1d2e3f40",
"event": "rss.item.created",
"createdAt": "2026-08-20T12:00:00Z",
"workspaceId": "8f9a0b1c-8888-4999-8000-111122223333",
"attempt": 1,
"data": {
"source": {
"id": "5e6f7081-7777-4888-8999-000011112222",
"title": "Company blog"
},
"item": {
"title": "Introducing the spring sale",
"url": "https://example.com/blog/spring-sale",
"publishedAt": "2026-08-20T11:30:00Z"
},
"link": {
"id": "6f3c1b2a-1111-4222-8333-444455556666",
"slug": "spring-sale",
"domain": "ryabi.ls",
"shortUrl": "https://ryabi.ls/spring-sale",
"destinationUrl": "https://example.com/spring-sale",
"title": "Spring sale"
}
}
}