Skip to main content

Webhook recipes

A webhook posts JSON to a URL when an event happens. With a payload template you choose the body, so the same webhook can talk to Slack, Discord or any service that takes a JSON POST.

Create a webhook​

In the Console, open Webhooks → New webhook, or use the API:

Create a webhook
curl -X POST "https://lantfly.com/api/v3/webhooks" \
-H "Authorization: Bearer $LANTFLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"description": "Scraper finished",
"event_types": ["ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"],
"condition": { "actor_id": "<actorId>" },
"request_url": "https://example.com/lantfly-hook"
}'
FieldMeaning
event_typesOne or more events (see Integrations).
conditionactor_id and/or task_id for run events. Empty: every run of the account.
request_urlAn http or https URL on a public address. Private addresses are refused, and redirects are not followed.
payload_templateOptional: the body to send, as a string (below).
headersOptional extra headers, for example an Authorization header your server expects.
descriptionA name for the webhook.
is_enabledTurn it off without deleting it.

The answer contains the signing secret, once. Keep it if your server verifies deliveries.

Then click Test (or POST /api/v3/webhooks/{id}/test): Lantfly sends your latest run as a sample, once, and shows the answer.

How deliveries work​

  • POST with Content-Type: application/json, a 30-second timeout and the headers X-Lantfly-Event, X-Lantfly-Webhook-Dispatch-Id and X-Lantfly-Signature.
  • A 2xx answer is a success. Anything else, or a network error, is retried after 1, 5, 25, 125 and 625 seconds (6 attempts in all).
  • Each webhook shows its last delivery and failed deliveries. Redeliver a failed one with POST /api/v3/webhooks/{id}/dispatches/{dispatch}/redeliver.

Payload templates​

Without a template, the body is the default payload:

Default payload of a run event
{
"user_id": "…",
"created_at": "2026-11-03T08:00:00.000Z",
"event_type": "ACTOR.RUN.SUCCEEDED",
"event_data": { "actor_id": "…", "task_id": null, "run_id": "…" },
"resource": {
"id": "…",
"actor_slug": "lantfly~web-scraper",
"status": "SUCCEEDED",
"status_message": "ok: 42 items from 3 targets",
"default_dataset_id": "…",
"default_key_value_store_id": "…",
"stats": { "run_time_secs": 31, "result_count": 42 },
"usage_total_usd": 0.021,
"started_at": "…",
"finished_at": "…"
}
}

A template is a JSON text with {{path}} placeholders into that payload, for example {{resource.default_dataset_id}} or {{event_data.run_id}}.

Placeholders become JSON values

Each placeholder is replaced by the JSON value at that path: a string comes with its quotes, a number without, a missing value as null. Write placeholders without quotes around them, and do not put them inside a longer string:

  • Right: {"run": {{resource.id}}, "items": {{resource.stats.result_count}}}
  • Wrong: {"run": "{{resource.id}}"} and {"text": "Run {{resource.id}} finished"}

When the filled template is not valid JSON, Lantfly sends {"payload": "<the filled text>"} instead.

Listening events have their own payload: event_data has project_id and mention_id (alerts: alert_id, rule_id, mention_id), and resource is the mention (platform, url, text, author_name, author_handle, published_at, views, likes, comments, shares, engagement, sentiment, hashtags…) or the alert event, plus project_id and project.

Slack​

Incoming webhook​

  1. In Slack, create an app with Incoming Webhooks and add a webhook to a channel. Copy its https://hooks.slack.com/services/… URL.

  2. Create a Lantfly webhook with that URL and a template that fills Slack's text:

    payload_template: run finished
    {"text": {{resource.status_message}}}
    payload_template: new mention
    {"text": {{resource.url}}}

The status message is a short summary such as ok: 42 items from 3 targets. Actors built with the Lantfly SDKs always set one at the end; other actors may leave it empty, and Slack refuses an empty text.

Workflow Builder​

For a formatted message, create a Slack workflow that starts from a webhook and declare variables (actor, status, summary, dataset). Use them in the workflow's message step, and send them from Lantfly:

payload_template
{
"actor": {{resource.actor_slug}},
"status": {{resource.status}},
"summary": {{resource.status_message}},
"dataset": {{resource.default_dataset_id}}
}

Discord​

  1. In Discord: Server settings → Integrations → Webhooks → New webhook, pick a channel and copy the URL.

  2. Use a template that fills Discord's content:

    payload_template: run finished
    {"content": {{resource.status_message}}, "username": "Lantfly"}
    payload_template: new mention
    {"content": {{resource.url}}, "username": "Lantfly"}

Discord shows a preview for mention URLs it can unfurl.

Your own server​

Keep the default payload and verify the signature before you trust the body. X-Lantfly-Signature is sha256= followed by the hex HMAC-SHA256 of the raw body with the webhook's secret.

server.js (Node.js, Express)
import crypto from 'node:crypto';
import express from 'express';

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

app.post('/lantfly-hook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const given = req.get('X-Lantfly-Signature') ?? '';
const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();

const event = JSON.parse(req.body.toString('utf8'));
res.status(200).end(); // answer fast, work after
if (event.event_type === 'ACTOR.RUN.SUCCEEDED') {
queueDownload(event.resource.default_dataset_id);
}
});

app.listen(3000);
server.py (Python, Flask)
import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["LANTFLY_WEBHOOK_SECRET"].encode()


@app.post("/lantfly-hook")
def lantfly_hook():
body = request.get_data()
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Lantfly-Signature", "")):
abort(401)
event = request.get_json()
# e.g. fetch the items of event["resource"]["default_dataset_id"]
return "", 200

Tips:

  • Answer within 30 seconds, ideally at once, and do the work afterwards. A slow answer counts as a failure and is retried.
  • Deduplicate with X-Lantfly-Webhook-Dispatch-Id: a retry of the same delivery has the same id.
  • Rotate the secret with POST /api/v3/webhooks/{id}/rotate-secret if it leaks.

More on signatures: Webhook signatures.

Webhooks for one run​

You can also attach up to 10 webhooks to a single run when you start it, with webhooks in the run request. They fire only for that run. See Runs.