Webhooks Setup
Subscribe to events, verify signatures, and handle webhook deliveries
Overview
Webhooks let you receive real-time HTTP notifications when content changes in your organization. Instead of polling the API, subscribe to events and your endpoint receives a POST request whenever something happens.
Available Events
| Event | Trigger |
|---|---|
question.created | A new question is created |
question.updated | A question is updated |
question.published | A question is published |
question.deleted | A question is deleted |
category.created | A new category is created |
category.updated | A category is updated |
category.deleted | A category is deleted |
domain.created | A custom domain is added |
domain.verified | A custom domain is verified |
domain.deleted | A custom domain is removed |
feedback.received | Feedback is submitted on a question |
Creating a Subscription
curl -X POST https://app.thefaq.app/api/v1/your-org/webhooks \
-H "Authorization: Bearer faq_your-admin-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/thefaqapp",
"events": ["question.created", "question.updated", "question.deleted"],
"secret": "whsec_your-signing-secret"
}'const response = await fetch(
'https://app.thefaq.app/api/v1/your-org/webhooks',
{
method: 'POST',
headers: {
'Authorization': 'Bearer faq_your-admin-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://your-app.com/webhooks/thefaqapp',
events: ['question.created', 'question.updated', 'question.deleted'],
secret: 'whsec_your-signing-secret',
}),
}
);
const { data } = await response.json();
console.log(data.id); // Webhook subscription IDResponse:
{
"data": {
"id": "clx...",
"url": "https://your-app.com/webhooks/thefaqapp",
"events": ["question.created", "question.updated", "question.deleted"],
"active": true,
"createdAt": "2026-02-28T10:00:00.000Z"
}
}See Webhooks API Reference for full endpoint details.
Receiving Events
When an event fires, your endpoint receives a POST request with these headers and a JSON body.
Headers
| Header | Description |
|---|---|
X-Webhook-Event | The event type (e.g. question.created) |
X-Webhook-Delivery | Unique delivery ID for deduplication |
X-Webhook-Timestamp | ISO 8601 timestamp of when the event occurred |
X-Webhook-Signature | HMAC-SHA256 signature for verification |
Content-Type | application/json |
Payload Structure
{
"event": "question.created",
"timestamp": "2026-02-28T10:00:00.000Z",
"data": {
"id": "clx...",
"question": "How do I reset my password?",
"slug": "how-do-i-reset-my-password",
"published": true,
"organizationId": "org_..."
}
}Your endpoint must respond with a 2xx status code within 30 seconds to acknowledge receipt.
Verifying Signatures
Always verify the X-Webhook-Signature header to ensure the request came from TheFAQApp and was not tampered with.
The signature is an HMAC-SHA256 hash of the raw request body using your webhook secret.
# Compute expected signature (for testing)
echo -n '{"event":"question.created","timestamp":"2026-02-28T10:00:00.000Z","data":{}}' \
| openssl dgst -sha256 -hmac "whsec_your-signing-secret"import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = createHmac('sha256', secret)
.update(body)
.digest('hex');
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler (e.g. Express, Next.js Route Handler)
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('X-Webhook-Signature')!;
const secret = process.env.WEBHOOK_SECRET!;
if (!verifyWebhookSignature(body, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
switch (event.event) {
case 'question.created':
await handleQuestionCreated(event.data);
break;
case 'question.updated':
await handleQuestionUpdated(event.data);
break;
case 'question.deleted':
await handleQuestionDeleted(event.data);
break;
}
return new Response('OK', { status: 200 });
}Important: Use timingSafeEqual (not ===) for signature comparison to prevent timing attacks.
Delivery and Retries
Webhook deliveries are powered by QStash for reliable async delivery with automatic retries.
Retry Schedule
If your endpoint returns a non-2xx response, the delivery is retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is marked as failed. You can replay failed deliveries from the dashboard or API.
Idempotency
Use the X-Webhook-Delivery header to deduplicate events. Store processed delivery IDs and skip duplicates:
const deliveryId = request.headers.get('X-Webhook-Delivery')!;
if (await isAlreadyProcessed(deliveryId)) {
return new Response('Already processed', { status: 200 });
}
// Process the event...
await markAsProcessed(deliveryId);Testing
Send a Test Ping
Trigger a test delivery to verify your endpoint is working:
curl -X POST https://app.thefaq.app/api/v1/your-org/webhooks/clx_webhook_id/test \
-H "Authorization: Bearer faq_your-admin-api-key"await fetch(
'https://app.thefaq.app/api/v1/your-org/webhooks/clx_webhook_id/test',
{
method: 'POST',
headers: { 'Authorization': 'Bearer faq_your-admin-api-key' },
}
);Replay Failed Deliveries
If a delivery failed, you can replay it:
curl -X POST https://app.thefaq.app/api/v1/your-org/webhooks/clx_webhook_id/replay \
-H "Authorization: Bearer faq_your-admin-api-key" \
-H "Content-Type: application/json" \
-d '{ "deliveryId": "clx_delivery_id" }'await fetch(
'https://app.thefaq.app/api/v1/your-org/webhooks/clx_webhook_id/replay',
{
method: 'POST',
headers: {
'Authorization': 'Bearer faq_your-admin-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ deliveryId: 'clx_delivery_id' }),
}
);Local Development
For local development, use a tunneling service like ngrok to expose your local endpoint:
ngrok http 3000
# Use the https URL as your webhook endpoint