ti_… id. They are not workflow steps, and they are not connections.
The three pieces
Lifecycle
1
Pick a type
GET /v1/triggers/types lists what your connections can fire on. GET /v1/triggers/types/{slug} returns its config_schema, payload_schema, and which parameter blocks apply — read this before creating, because whether a type takes a schedule, listen_columns, or neither is per type.2
Create it
POST /v1/triggers. You get back a ti_… id.3
Enable it
A trigger is created disabled. Nothing is captured until you
POST /v1/triggers/{id}/enable. This catches people out — create is not start.4
Receive events
Engini POSTs each captured event to the destination, signed. Verify the signature, then act on it. The event log at
GET /v1/triggers/{id}/events is the record of what was captured and what happened to each delivery.How a trigger fires
Every trigger type declares adelivery, and it decides what you configure:
Polling intervals have a floor, and it is your plan’s. Every polling response echoes a
schedule.effective block — the schedule as applied, with min_interval_minutes resolved from your own account’s subscription. Compare it with what you sent to see whether you were clamped. It is echoed on every response, not only when it differs, so you never have to infer the floor from a field’s absence.subscription_count is usually not 1. A provider subscription covers a single watched column, so three listen_columns create three subscriptions. Providers generally meter them, so this is a number worth reading before you widen a trigger.Destinations and the signing secret
A destination is a name plus an HTTPS URL. Create one withPOST /v1/triggers/destinations, or set the account default with PUT /v1/triggers/destination.
Engini disables a destination automatically (status: "auto_disabled") after too many consecutive dead-lettered events - events that used up every retry, or were refused with a non-retryable status - set by max_consecutive_failures, default 5. Events keep being captured; they just stop being delivered. Re-enable it (PATCH /v1/triggers/destinations/{name} with status: "active") and the failure counter resets.
Rotating a secret (POST /v1/triggers/destinations/{name}/rotate-secret) has no overlap window: the old secret stops verifying the moment the call returns. Update your receiver straight away and replay whatever failed in between.
Deleting a destination that triggers still point at is a 409. Name a replacement to move them across in the same call — the API query parameter is reassign_to (the CLI flag is spelled --reassign).
Verifying a delivery
Every POST carries these headers:
The signature is:
- Use the whole secret,
esec_prefix included. Stripping the prefix produces a MAC that never matches. - Use the raw body bytes. Parsing the JSON and re-serialising it changes whitespace and key order, and the MAC is over the exact bytes.
- Reject anything outside a 300-second window on
t, in both directions.
verify_webhook / verifyWebhook that does all of this - see Triggers in the SDKs. If you verify by hand, compare the MACs in constant time:
The delivery body
"test": true, written only when it is true - a real delivery never has the key at all. When a record exceeds the payload cap, payload_truncated is true and payload is a truncation envelope rather than the record.
Retries
Engini treats any 2xx as delivered. Anything else is retried or given up on:
An event gets 6 attempts: the first straight away, then retries 1 min, 5 min, 15 min, 1 h and 6 h after the previous one - a little over 7 hours in all. An event that exhausts them is dead-lettered. It stays stored (events are kept 30 days) and you can still replay it.
Reading events
Two read paths, and they answer different questions.A stream is not a delivery mechanism, and every stream ends. The gateway caps one connection at one hour of wall-clock time. It is an absolute cap, not an idle timeout — the 20-second heartbeat does not defer it — and the cut arrives as an abrupt truncation with no goodbye frame.Resume by reconnecting with
?since=<last event id you saw>, which replays strictly after that event, so the seam neither duplicates nor skips. Both SDKs do this for you. If the cursor has been pruned by retention, the resume answers 410 SINCE_PRUNED — a real gap, surfaced rather than papered over with a silent restart from “now”.POST /v1/triggers/events/{eventId}/replay re-delivers a captured event. It answers 202: the attempt is queued, not sent inline, so read the event again a few seconds later to see the result. The attempt number carries on from the last one, so a replay gets whatever retries the event had left - none, if it had already used all six. If the destination has auto-disabled, replay answers 409 until you enable it again. Walkthrough: Debug and replay a failed delivery.
Testing a destination
POST /v1/triggers/{triggerId}/test sends one synthetic event through the real delivery path - signed with the destination’s secret, same headers - and returns what your endpoint answered, synchronously:
- A broken receiver is a successful test. The call returns
200whatever your endpoint did;deliveredistrueonly on a2xx. - Nothing is stored. The test event never appears in the log or on the stream, and never counts against deduplication. Its id is prefixed
te_test_and its body carries"test": true, both signed. - Optional body
{"payload": {...}}; omit it for a default. Limited to 10 calls a minute per account;409if the destination has auto-disabled.
Response shapes
The list envelope is camelCase; the items inside it are snake_case. A paginated
/v1 response looks like { "items": [...], "totalCount": 42, "offset": 0, "top": 100 }, and each entry inside items uses snake_case members (trigger_slug, status_reason, last_event_at). The Python SDK hides the seam; TypeScript and raw HTTP see both conventions in one response.Known limits
GET /v1/triggers?include=workflownow paginates correctly at the wire level.offset/topare split across the API and designer-built halves, so a sequential walk over the raw endpoint visits every row exactly once, with no duplicates or gaps. Both SDKs’list(include=...)still fetch a single page rather than walking the full set - passtopexplicitly if you need more than the default page. The CLI’s--include workflowalso requests a single page; see the CLI reference.
Recipes
- Receive trigger events on a webhook - end to end, with a verifying receiver and a test event.
- Debug and replay a failed delivery - attempts, re-enabling, replay, and safe secret rotation.
- Develop a trigger locally - the live stream and signed forwarding to
localhost.