Skip to content

Receive webhooks

This page is the receiver. The event table, collection filter, payload data variants, SSRF checks, and the admin route table are on Webhooks.

  1. Open Database → Automation → Webhooks → Add webhook.

  2. URL: https://your-app.example/api/webhooks/tefily. Production requires HTTPS. Loopback and private IPs are rejected.

  3. Signing secret (optional): set one. The console shows the secret once after create. Later responses only have hasSecret and secretLastFour.

  4. Events: include at least Record created (record.created). An empty event list means all events. The event names are on Webhooks.

  5. Collections: posts, or leave the list empty for every collection. Auth events ignore this filter.

  6. Leave the webhook active.

Optional:

POST /api/projects/YOUR_PROJECT_ID/webhooks
Authorization: Bearer kfx_…
Content-Type: application/json
{
"url": "https://your-app.example/api/webhooks/tefily",
"secret": "YOUR_SIGNING_SECRET",
"events": ["record.created"],
"collections": ["posts"],
"isActive": true
}

Create returns 201 and includes secret only on that response.

Store the secret in server env TEFILY_WEBHOOK_SECRET. Do not use a service token as the HMAC secret.

The signature is HMAC-SHA256 over the raw UTF-8 body. Hex digest, lowercase. The header value is sha256=<hex>. The header is omitted when the webhook has no secret. Every delivery also sends:

Header Value
Content-Type application/json
X-Kallax-Event Event name, for example record.created
X-Kallax-Delivery Delivery id
X-Kallax-Signature sha256=<hex> when a secret is set

Envelope (the same bytes that were signed):

{
"id": "<same value as X-Kallax-Delivery>",
"event": "record.created",
"projectId": "…",
"collection": "posts",
"timestamp": "2026-09-15T18:00:00.000Z",
"actor": { "type": "project", "userId": "…" },
"data": { "record": { } }
}

actor.type is project, platform, service, or anonymous. For record.created, data.record is the document after insert. Other data shapes are on Webhooks.

id is the delivery row id. Retries reuse that id and the same body.

Next.js App Router. Call request.text() first. Do not call request.json() before the HMAC.

import { createHmac, timingSafeEqual } from 'node:crypto';
import { NextResponse } from 'next/server';
function verifySignature(secret: string, rawBody: string, header: string | null): boolean {
if (!header?.startsWith('sha256=')) return false;
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const actual = header.slice('sha256='.length);
const a = Buffer.from(expected);
const b = Buffer.from(actual);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
export async function POST(request: Request) {
const rawBody = await request.text();
const ok = verifySignature(
process.env.TEFILY_WEBHOOK_SECRET!,
rawBody,
request.headers.get('x-kallax-signature'),
);
if (!ok) return NextResponse.json({ error: 'Bad signature' }, { status: 401 });
const payload = JSON.parse(rawBody) as {
id: string;
event: string;
collection?: string;
data: { record?: { _id?: string } };
};
const deliveryId = request.headers.get('x-kallax-delivery') ?? payload.id;
if (await alreadyHandled(deliveryId)) {
return new NextResponse(null, { status: 204 });
}
await markHandled(deliveryId);
if (payload.event === 'record.created' && payload.collection === 'posts') {
// payload.data.record
}
return new NextResponse(null, { status: 204 });
}

alreadyHandled and markHandled are your store, for example a database unique key on the delivery id. Tefily has no dedupe API.

Return 2xx for a duplicate. A non-2xx is a failure and will be retried.

  • HTTP 200–299 marks the delivery success. Anything else, including a network error, stays pending until attempts are exhausted.
  • At most 5 attempts (kallaxWebhookMaxAttempts).
  • After a failure, the next try is scheduled min(3600, 2^attempt) seconds later. attempt is 1 on the first failure, so 2s, then 4s, 8s, 16s. A worker runs about once a minute, so a short delay still waits for that pass.
  • Each try uses a 5 second connect timeout, a 5 second timeout opening the POST, and a 5 second timeout reading the response.
  • The same delivery id and the same body are sent again. The original Data API call does not fail when delivery fails.
  • Deliveries are listed in the console on that webhook, and at GET /api/projects/:projectId/webhooks/:id/deliveries.

The console can send a test (webhook.ping).