Aidelly Docs
REST API

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

EventTriggered when
post.createdA post is created (draft or scheduled)
post.scheduledA post is confirmed scheduled
post.publishedA post is successfully published to the platform
post.failedA post failed to publish
post.canceledA post is canceled before publishing
inbox.item.createdA new inbox message or comment is stored
inbox.lead.capturedThe 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:

HeaderMeaning
x-aidelly-eventEvent name, e.g. post.published
x-aidelly-event-idStable id for this event. Dedupe on it
x-aidelly-timestampISO-8601 send time of this attempt, part of the signed data
x-aidelly-signaturesha256=<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.

  1. Inline attempts. Aidelly tries up to 3 times in a row, about 150 ms and 300 ms apart.
  2. Scheduled redelivery. If all three fail, the delivery is marked failed and 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.
  3. Dead letter. When the schedule runs out, or the webhook is disabled or deleted while retries are pending, the delivery becomes dead_letter and 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)"