Skip to main content
A trigger watches a connected application and captures an event when something happens there — a row is added, an email arrives, a record changes. Engini then delivers that event to an HTTPS endpoint you own. Triggers are entities in their own right, addressed by a 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.
enable also clears a block, and blocked is not the same as disabled.Engini blocks a trigger by itself when something is wrong — too many consecutive failures, a deleted connection, a provider that refused the subscription. A blocked trigger reports status: "errored" with a status_reason, and stays silent until an enable clears the block.The one case enable will not fix: an account over its activity limit is refused with 409 rather than unblocked. The block is the limit doing its job; get usage back under it first.

How a trigger fires

Every trigger type declares a delivery, 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 with POST /v1/triggers/destinations, or set the account default with PUT /v1/triggers/destination.
The signing secret is shown exactly once. It comes back from the three calls that mint one — creating a destination, rotating its secret, and PUT /v1/triggers/destination when that call creates the default. No read path ever returns it again. Store it when you see it; if you lose it, rotate.The prefix is esec_.
Engini disables a destination automatically after too many consecutive delivery failures (max_failures, default 5). Events keep being captured; they just stop being delivered. Re-enable it and the failure counter resets. 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:
Three things break verification if you get them wrong:
  • 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.
Deduplicate on X-Engini-Event-Id, never on the signature. t is regenerated for every attempt, so a retry of the same event arrives with a completely different signature. Treating the signature as an idempotency key means processing every retry as a new event.
Both SDKs ship a verify_webhook / verifyWebhook that does all of this — see Triggers in the SDKs.

Reading events

Two read paths, and they answer different questions.
The stream’s delivery field is always "pending". The stream reports dispatch, not delivery — an event is put on the stream before anyone has tried to POST it. To know whether a delivery actually succeeded, read GET /v1/triggers/events/{eventId}, which carries the full attempt history.
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. If the destination has auto-disabled, replay fails until you enable it again.

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.

What does not exist yet

  • There is no test-fire endpoint. POST /v1/triggers/{id}/test is not implemented. To see a trigger fire, cause the change in the connected app.
  • GET /v1/triggers?include=workflow returns one page only, whatever top says. Designer-built workflow triggers surfaced this way are not paginated yet. If you need the full set, read them from the workflow surface instead.