Skip to main content

Webhook signatures

Lantfly signs every webhook delivery, so your endpoint can reject requests that didn't come from Lantfly or were changed on the way. To set up webhooks and choose events, see Webhooks.

Headers​

Each delivery is an HTTPS POST with a JSON body and these headers:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON.
User-AgentLantfly-Webhooks/1Identifies Lantfly.
X-Lantfly-EventACTOR.RUN.SUCCEEDEDThe event type.
X-Lantfly-Webhook-Dispatch-Id9b2e…ID of this delivery. Retries of the same delivery keep it, so use it to drop duplicates.
X-Lantfly-Signaturesha256=5f0c…HMAC-SHA256 of the raw body with the webhook's secret, as lowercase hex, after sha256=.

Headers you added to the webhook are sent too, but they can't replace the ones above.

The signing secret​

Each webhook has its own secret. It's shown once: in the answer that creates the webhook (secret), and in the answer to POST /webhooks/{id}/rotate-secret. Store it with your other secrets.

Rotate the secret
curl -X POST https://lantfly.com/api/v3/webhooks/$WEBHOOK_ID/rotate-secret \
-H "Authorization: Bearer $LANTFLY_TOKEN"

Rotation takes effect at once: deliveries after it are signed with the new secret. REST hook subscriptions (Zapier, Make, n8n) also get a secret when they subscribe and are signed the same way.

note

Webhooks attached to a single run (the webhooks field when you start a run) don't return their secret. To authenticate those, add your own header, such as a shared token, in the webhook's headers.

Verify a delivery​

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Compute HMAC-SHA256 of those bytes with the secret, as lowercase hex.
  3. Compare sha256= plus your hex with X-Lantfly-Signature in constant time.
  4. Reject the request (for example with 401) if they differ. Otherwise answer 2xx quickly and do slow work after.
warning

Sign the bytes you received. Parsing and re-serializing the JSON changes spacing and escaping, and the signature won't match.

server.mjs (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.LANTFLY_WEBHOOK_SECRET;

function isValid(rawBody, header) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps the body as a Buffer, exactly as sent.
app.post('/lantfly/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!isValid(req.body, req.get('X-Lantfly-Signature'))) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204); // answer first

console.log(req.get('X-Lantfly-Event'), req.get('X-Lantfly-Webhook-Dispatch-Id'), event.resource?.id);
});

app.listen(3000);

Payload​

Unless the webhook has a payload template, the body is:

Run event
{
"user_id": 4821,
"created_at": "2026-11-03T08:15:30.123Z",
"event_type": "ACTOR.RUN.SUCCEEDED",
"event_data": {
"actor_id": "1a7d…",
"task_id": null,
"run_id": "6f1c…"
},
"resource": {
"id": "6f1c…",
"actor_id": "1a7d…",
"status": "SUCCEEDED",
"default_dataset_id": "c2f3…",
"default_key_value_store_id": "e91a…"
}
}

resource is the run as GET /runs/{id} returns it (shortened above).

Run eventSent when
ACTOR.RUN.CREATEDA run was created.
ACTOR.RUN.SUCCEEDEDA run finished successfully.
ACTOR.RUN.FAILEDA run failed.
ACTOR.RUN.TIMED_OUTA run hit its timeout.
ACTOR.RUN.ABORTEDA run was aborted.
ACTOR.RUN.RESURRECTEDA finished run was resurrected.

Social listening events​

Webhooks for a listening project (condition project_id) send:

Listening event
{
"id": "918273",
"created_at": "2026-11-03T08:16:02.000Z",
"event_type": "LISTENING.MENTION.CREATED",
"event_data": {
"project_id": "0b6e…",
"mention_id": 918273
},
"resource": {
"id": 918273,
"platform": "tiktok",
"project_id": "0b6e…",
"project": "Brand monitoring"
}
}
Listening eventevent_dataSent when
LISTENING.MENTION.CREATEDproject_id, mention_idA new mention was collected (hidden and irrelevant ones are skipped).
LISTENING.ALERT.FIREDproject_id, alert_id (the alert event), rule_id, mention_idAn alert rule fired.

resource is the mention or alert event as the listening API shows it, plus project_id and project (the project's name). Listening events go out once a minute, one delivery per mention or alert, at most 25 per webhook each minute, once the mention is about two minutes old. A new subscription starts after the latest mention or alert, so it doesn't replay history. See Social listening.

Test deliveries​

POST /webhooks/{id}/test sends one delivery now, with event type TEST: the account's latest run as the sample resource, or a bare event_type, created_at and message when the account has no run yet. Tests are signed like real deliveries and are tried once, without retries.

Payload templates​

A webhook can set a payload template. Lantfly fills each {{path}} from the default body above, for example {{resource.default_dataset_id}} or {{event_data.run_id}}, and signs the body it sends.

Retries and redelivery​

  • A delivery succeeds when your endpoint answers any 2xx within 30 seconds. Redirects aren't followed.

  • Otherwise Lantfly retries up to 5 more times, after 1 s, 5 s, 25 s, 125 s and 625 s (about 13 minutes in all).

  • After the last attempt the delivery is FAILED. The webhook shows last_delivery and failed_deliveries.

  • Send a FAILED delivery again with:

    Redeliver
    curl -X POST https://lantfly.com/api/v3/webhooks/$WEBHOOK_ID/dispatches/$DISPATCH_ID/redeliver \
    -H "Authorization: Bearer $LANTFLY_TOKEN"

    It answers 202. A delivery that isn't FAILED answers 422. Limit: 30 a minute.

Because of retries, your endpoint can receive the same delivery more than once. Make the handler idempotent: store X-Lantfly-Webhook-Dispatch-Id and skip IDs you've already handled.

tip

Webhook URLs must be public. Addresses on private or local networks are refused at delivery time.