Documentation menu

Webhooks

Register a URL and Adeli POSTs a signed JSON event to it when someone comments on a connected Instagram account's post, or taps a postback button you sent with Send Instagram message. Together they are everything a comment-to-DM automation needs.

Webhooks belong to one profile, and cover its Instagram accounts. An account connected before Unified Login through a Facebook Page lacks the comments permission, so it sends no events until it is reconnected.

Endpoints

Events

Every event has the same envelope:

json
{
  "id": "evt_3f1c2b9a8d7e6f5a4b3c2d1e0f9a8b7c",
  "type": "comment.created",
  "createdAt": "2026-10-27T21:08:59.000Z",
  "profileId": "00000000-0000-4000-8000-000000000001",
  "platform": "instagram",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "data": {}
}
typeWhendata
comment.createdSomeone comments on, or replies under, one of the account's posts.commentId, mediaId, parentId (null for a top-level comment), text, from: { id, username }, and isSelf, true when the account commented itself. from.id is the commenter's Instagram-scoped id: send it a private reply with recipient: { commentId }.
message.postbackSomeone taps a postback button the account sent.senderId (their Instagram-scoped id, for recipient: { id }), payload (the button's payload), title (the button's title), messageId.

Events arrive for every new comment, whether or not anyone has opened the post in Adeli.

Verifying signatures

Each request carries three headers:

HeaderValue
Adeli-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Adeli-Event-IdThe event's id
Adeli-Event-TypeThe event's type

v1 is the HMAC-SHA256 of <t>.<raw request body>, keyed with the webhook's signing secret, which Create webhook returns once. Compute it over the raw body before parsing JSON, compare in constant time, and reject a t more than five minutes from your clock, so a captured request cannot be replayed:

ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string | null, secret: string) {
  const parts = new Map((header ?? "").split(",").map((part) => part.split("=", 2) as [string, string]));
  const t = Number(parts.get("t"));
  const v1 = parts.get("v1") ?? "";
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300 || !/^[0-9a-f]{64}$/.test(v1)) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return timingSafeEqual(expected, Buffer.from(v1, "hex"));
}

Delivery and retries

Answer with any 2xx within 10 seconds; do slow work afterwards. Any other status, a timeout or a network error is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, six attempts in all. Redirects are not followed. A 410 Gone disables the webhook; create it again to resume.

Delivery is at least once, and Instagram itself sometimes sends the same comment twice. The same event always has the same id, so record the ids you have handled and ignore repeats.

To rotate a secret, delete the webhook and create it again.

Local development

A webhook URL must be https, except http://localhost and http://127.0.0.1, which are allowed so a local Adeli dev server can call an app running on the same machine.