Webhooks
Subscribe to post lifecycle events and verify delivery signatures.
Webhooks let Aidelly push real-time notifications to your server when post events occur.
Subscribing
curl -sS "$AIDELLY_API_BASE/webhooks" \
-X POST \
-H "Authorization: Bearer $AIDELLY_API_KEY" \
-H "Idempotency-Key: webhook-$(uuidgen)" \
-H "x-aidelly-workspace-id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.example.com/aidelly/webhooks",
"events": ["post.published", "post.failed"],
"description": "Post completion handler"
}'Available events
| Event | Triggered when |
|---|---|
post.created | A post is created (draft or scheduled) |
post.scheduled | A post is confirmed scheduled |
post.published | A post is successfully published to the platform |
post.failed | A post failed to publish |
post.canceled | A post is canceled before publishing |
inbox.item.created | A new inbox message or comment is stored |
inbox.lead.captured | The AI DM agent first collects an email or phone number (once per conversation) |
Payload shape
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "post.published",
"occurred_at": "2026-02-18T17:30:02Z",
"workspace_id": "uuid",
"brand_id": "uuid",
"data": {
"post_id": "uuid",
"status": "completed"
}
}id matches the x-aidelly-event-id header and stays the same on every retry
of that event. data can carry extra event-specific fields.
Signature verification
Every delivery is a POST with these headers:
| Header | Meaning |
|---|---|
x-aidelly-event | Event name, e.g. post.published |
x-aidelly-event-id | Stable id for this event. Dedupe on it |
x-aidelly-timestamp | ISO-8601 send time of this attempt, part of the signed data |
x-aidelly-signature | sha256=<hex> HMAC-SHA256 signature |
The signature is an HMAC-SHA256, keyed with the webhook's secret, over the
string `${timestamp}.${rawBody}`: the exact x-aidelly-timestamp header
value, a literal ., then the raw request body. The secret is returned once,
as webhook.secret, when you create the webhook (or is the secret you
supplied).
Each attempt, including retries, gets a fresh timestamp and signature. Aidelly does not enforce a time window, so reject stale deliveries yourself; 5 minutes is a good tolerance.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_MS = 5 * 60 * 1000;
function verifyAidellyWebhook(rawBody, headers, secret) {
const timestamp = headers.get('x-aidelly-timestamp');
const received = headers.get('x-aidelly-signature');
if (!timestamp || !received) return false;
// Reject stale or future-dated deliveries so a captured request cannot be replayed.
const ageMs = Date.now() - Date.parse(timestamp);
if (!Number.isFinite(ageMs) || Math.abs(ageMs) > TOLERANCE_MS) return false;
const expected =
'sha256=' +
createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
// timingSafeEqual throws when lengths differ, so check length first.
return a.length === b.length && timingSafeEqual(a, b);
}
// Example: a fetch-style handler (Next.js route, Hono, Workers, ...)
export async function POST(req) {
const rawBody = await req.text(); // raw text, before JSON.parse
if (
!verifyAidellyWebhook(
rawBody,
req.headers,
process.env.AIDELLY_WEBHOOK_SECRET
)
) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(rawBody);
// Dedupe on event.id (same as x-aidelly-event-id), then process asynchronously.
return new Response(null, { status: 204 });
}Verify against the raw body. Parsing and re-serializing the JSON can change key
order or spacing, and the signature will not match. In Express, use
express.raw({ type: 'application/json' }) on the webhook route and pass
req.body.toString('utf8').
Delivery and retries
Any 2xx response counts as delivered. Anything else, including 4xx, 5xx,
a connection error, or no response within 15 seconds, is a failed attempt and
is retried.
- Inline attempts. Aidelly tries up to 3 times in a row, about 150 ms and 300 ms apart.
- Scheduled redelivery. If all three fail, the delivery is marked
failedand a background job retries it after about 15 minutes, then 1 hour, 3 hours, 6 hours, and 12 hours (each measured from the previous failure, and checked every 5 minutes). That is 5 scheduled retries over roughly 22 hours. Each scheduled retry again makes up to 3 quick attempts. - Dead letter. When the schedule runs out, or the webhook is disabled or
deleted while retries are pending, the delivery becomes
dead_letterand is not retried again. Re-fetch the resource through the API to reconcile.
Delivery is at-least-once, so the same event can arrive more than once. Return
2xx quickly and do slow work asynchronously. Inspect delivery history with
GET /webhooks/{id}/deliveries (filter with
status=pending|delivered|failed|dead_letter).
Listing and deleting subscriptions
# List
curl "$AIDELLY_API_BASE/webhooks" -H "Authorization: Bearer $AIDELLY_API_KEY"
# Delete
curl -X DELETE "$AIDELLY_API_BASE/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $AIDELLY_API_KEY" \
-H "Idempotency-Key: delete-$(uuidgen)"