This guide covers delivery patterns after a query triggers, with Auto as the source of truth for event emission.
note
Query title and description in notifications
The title and description you set on the query are included in outbound notifications across channels (Telegram, webhook, notify). Recipients often see an alert hours or days after the query was created — these fields are what make it immediately clear what fired and why it was set up. See Title and Description.
action.type = "webhook" with signature verification and queue/worker processing
Telegram Bot
Fast human-readable alerts
action.type = "telegram_bot" with params.botToken + params.chatId for direct delivery, or webhook/SSE relay for custom formatting. The body is auto-composed from query title + description + trigger context — no message param.
SSE Stream
Real-time event consumers
Always available regardless of the selected action — GET /v2/auto/queries/stream for every query on one connection, or GET /v2/auto/queries/{queryId}/stream for a single query, using the same auth as query creation. For minimal setup, add a notify action with a message; those notifications are retrievable only via SSE or poll.
To keep webhook, Telegram relay, and SSE processing consistent, normalize incoming events into one internal contract.
Canonical event object:
Note: SSE uses Server-Sent Events format with event: notification.
The canonical payload below is for documentation reference only — actual SSE delivery uses the format defined in the SSE section.
status is one of triggered, stopped, ended, or update. The payload also carries the query's execution context when present (executionId, triggerTime, conditionsMet, autoDetails).
Note: The id in the SSE frame header is the notification outbox event UUID, not a query ID. queryId is a top-level field on the payload — use it to correlate with poll results via /v2/auto/queries/{queryId}.
Implementation note:
Preserve the original payload for audit/debug.
Map to the canonical contract before queueing downstream work.
signingSecret is write-only. Elfa stores it for delivery signing, but does not
return it in public query, execution, webhook, SSE, or LLM callback payloads.
Generate a separate high-entropy webhook secret for each webhook/action, for
example openssl rand -hex 32.
API-key clients: do not use x-elfa-api-key or ELFA_HMAC_SECRET as the
webhook secret. The API key authenticates your client to Elfa; the request
HMAC secret signs your requests to Elfa; webhook.params.signingSecret
verifies outbound webhook deliveries from Elfa to your server.
x402/agent clients: prefer explicit webhook.params.signingSecret. If it is
omitted, older x402/agent webhooks may be signed with
SHA256(x-elfa-agent-secret) as the HMAC key. Treat that as a legacy fallback,
not the recommended setup.
If no explicit signingSecret is provided, signature headers may be absent
depending on access mode and server configuration. Require an explicit
signingSecret for production receivers.
telegram_bot delivers to private chats, groups, and supergroups. Channels
are not supported and are rejected at create time.
Two things to watch for when using a group:
Group chat IDs are negative. A group's chatId looks like
-1001234567890, not a positive integer. Copy it verbatim from getUpdates —
including the leading -.
The bot must be able to post in the chat. At create time Auto checks the
bot's membership and permissions. Administrators and the chat creator always
pass; an ordinary member passes unless the chat has disabled
can_send_messages; a restricted member passes only if its own
can_send_messages is enabled. If the bot cannot post, create fails with
Telegram bot cannot send messages in this chat (check its permissions).
If a group is later upgraded to a supergroup, Telegram issues it a new chat ID.
Auto handles this for you — it detects the migration, updates the stored chat ID,
and retries the send. No action needed on your side.
There are two streams. Both are free, both are GET, and neither requires HMAC.
Endpoint
Scope
Use when
GET /v2/auto/queries/stream
Every query you own, on one connection
Default choice. Correlate events with the payload's queryId.
GET /v2/auto/queries/{queryId}/stream
A single query
You only care about one query, or you are on x402 / an agent identity.
Prefer the account-wide stream over opening one connection per query.
Auth: the stream requires the same auth used to create the query — send x-elfa-api-key: <YOUR_API_KEY> for API-key queries, or the x402 secret for x402 queries. It is not limited to x-elfa-api-key.
The account-wide stream is API-key only
GET /v2/auto/queries/stream is not available to agent identities (x-elfa-agent-secret) and is not exposed on x402 — both return 403. An agent's identity is self-asserted, and the per-query stream tolerates that only because a caller must also know the query's UUID; an account-wide firehose would drop that second factor. Agents keep the per-query stream.
Both streams emit the same frames (event: notification, id = outbox event UUID) and send a : keep-alive comment every 15s. Events are live-only: there is no replay and Last-Event-ID is ignored.
A recurring query is never terminal, so its stream stays open across triggers. If the stream fails after it has started, it emits event: error with {"code":"STREAM_UNAVAILABLE"}.
Open the account-wide stream after creating at least one query — it returns 410 when you have none.
SSE best practices:
Run SSE consumers on server/worker (not browser-only clients) so you can set auth headers.
Reconnect automatically with backoff.
Persist last seen event IDs to avoid duplicate downstream actions.