Trigger API
Receive events, and manage triggers, handlers and requests programmatically.
Two groups of endpoints with different authentication.
The receiving endpoints under /api/trigger are public. They are what external systems
post to, and they carry no platform credentials: a trigger is identified by its code and
protected, if at all, by its own Basic auth.
The management endpoints under /api/event-triggers, /api/event-handlers,
/api/event-requests and /api/trigger-monitoring require a platform JWT and are scoped to
your tenant.
Receiving events
Fire a trigger
ANY /api/trigger/{triggerCode}
ANY /api/trigger/{triggerCode}/{...path}Every HTTP method executes the trigger, GET included. Trailing path segments are exposed
to expressions as parameters.path.0, parameters.path.1 and so on.
curl -X POST https://service.ensembleapp.dev/api/trigger/AbC123XyZ \
-H 'Content-Type: application/json' \
-d '{ "event": "order.created", "order": { "id": "A-1001" } }'Send Basic credentials when the trigger requires them:
curl -X POST https://service.ensembleapp.dev/api/trigger/AbC123XyZ \
-u 'webhook-user:the-password' \
-H 'Content-Type: application/json' \
-d '{ "event": "order.created" }'Or a bearer token, for a trigger set to OAuth:
curl -X POST https://service.ensembleapp.dev/api/trigger/AbC123XyZ \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "event": "order.created" }'Or an API key, for a trigger set to API Key:
curl -X POST https://service.ensembleapp.dev/api/trigger/AbC123XyZ \
-H 'x-api-key: sk_...' \
-H 'Content-Type: application/json' \
-d '{ "event": "order.created" }'Both modes are bound to one key, chosen when the trigger is configured. An API Key
trigger accepts that key and no other; an OAuth trigger accepts access tokens issued to
clients belonging to that key. A credential for a different key is refused even within the
same tenant, so a key issued to call /external cannot fire a webhook.
The key's own state follows the rule below: an OAuth trigger can be bound to a disabled
key and keeps firing, because the clients on it keep working. An API Key trigger cannot,
since a disabled key is not accepted as a header, and the binding is refused when you set
it rather than failing silently later.
Responses:
| Status | Body | Cause |
|---|---|---|
200 | handlersExecuted and one results entry per handler | Handlers matched and were dispatched |
401 | Basic Authentication required, Invalid credentials | Basic auth configured and not satisfied |
404 | Trigger not found | Unknown code, or the trigger is archived |
404 | No handlers found for this trigger | Classified, and nothing matched |
500 | Internal server error | Platform failure |
A 200 means every matched handler was started. It is not a statement about what those
workflows went on to do.
Inspect a payload
ANY /api/trigger/inspect/{triggerCode}Records the request and runs no handlers. Answers 200 with the stored headers, body and
parameters, or 404 if the code is unknown.
The token endpoint
POST /oauth/tokenPublic: the request body is the credential. Only the client_credentials grant is
supported, which is the whole of what a machine caller needs.
curl -X POST https://service.ensembleapp.dev/oauth/token \
-d grant_type=client_credentials \
-d client_id=cli_... \
-d client_secret=cls_...Credentials may also be sent as HTTP Basic, which the specification prefers:
curl -X POST https://service.ensembleapp.dev/oauth/token \
-u 'cli_...:cls_...' \
-d grant_type=client_credentials{ "access_token": "ens_at_...", "token_type": "Bearer", "expires_in": 3600 }| Status | Body | Cause |
|---|---|---|
200 | access_token, token_type, expires_in | The credentials verified |
400 | invalid_request | grant_type is missing |
400 | unsupported_grant_type | A grant other than client_credentials |
401 | invalid_client | Unknown client, wrong secret, disabled, or expired |
405 | invalid_request | Anything other than POST |
The 401 is deliberately one answer for four causes: distinguishing them would let someone
enumerate client ids.
A token is accepted anywhere an API key is, as Authorization: Bearer. It authenticates as
the API key the client was created against, so it reaches exactly what that key reaches --
there are no separate permissions to grant. Revoking the client invalidates its tokens
immediately rather than at expiry.
What the key does and does not take with it:
| To the key | Effect on its clients |
|---|---|
| Disabled | None. The key stops working as an x-api-key header; its clients keep working. Not putting a long-lived key on the wire is most of the reason to use a client, so the two switch independently. |
| Expired | Its clients stop. Expiry is the end of the identity, not of one credential for it. |
| Deleted | Its clients are deleted with it, and their tokens. |
To stop a client, disable or revoke the client.
Create, rotate and revoke clients under Credentials, on the key they act as, or
through /api/oauth-clients:
GET /api/oauth-clients List this tenant's clients
POST /api/oauth-clients Create one against an API key
PATCH /api/oauth-clients/{clientId}/disable Switch it off, reversibly
PATCH /api/oauth-clients/{clientId}/enable Switch it back on
POST /api/oauth-clients/{clientId}/rotate-secret Issue a new secret, same client id
DELETE /api/oauth-clients/{clientId} Revoke it, and its tokensPOST takes name, the apiKeyHash to act as, and an optional expiresInDays. Omitting
the last means the client never expires.
Three ways to stop a client, for three different reasons:
| What happens to its tokens | Reversible | |
|---|---|---|
| Disable | Refused while it is off, honoured again when you enable it | Yes, with nothing reconfigured |
| Rotate | Deleted. The old secret stops working; the client id stays | The caller needs the new secret |
| Revoke | Deleted with the client | No |
Disable is for pausing an integration. Rotate is for a secret that may have leaked, since it invalidates the secret and the tokens issued under it while keeping the id, so the caller's configuration changes in one field rather than two. Revoke is for a caller that is going away.
All of these require a signed-in admin. An API key, or a token issued against one, is
refused here even though it reaches the rest of /api: a credential that can mint
credentials could outlive its own rotation.
Managing triggers
GET /api/event-triggers List, with ?includeArchived=true
GET /api/event-triggers/{id} One trigger
POST /api/event-triggers Create
PUT /api/event-triggers/{id} Update
DELETE /api/event-triggers/{id} Archivecurl -X POST https://service.ensembleapp.dev/api/event-triggers \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Orders",
"triggerCode": "AbC123XyZ",
"eventTypePath": "${body.event}",
"auth": "basic",
"basicAuthUsername": "webhook-user",
"basicAuthPassword": "the-password"
}'| Field | Required | Notes |
|---|---|---|
name | yes | Display name |
triggerCode | yes | Unique across the platform; becomes the URL |
eventTypePath | no | LiquidJS template with ${ }. The only way to set this — the trigger form has no field for it |
auth | no | basic or oauth. oauth is not enforced |
basicAuthUsername, basicAuthPassword | with basic | Required for Basic auth to be applied |
DELETE archives rather than deletes. The request history is kept, the URL stops accepting
events, and the code stays claimed.
Managing handlers
GET /api/event-handlers List, filterable by ?triggerId=
GET /api/event-handlers/{id} One handler
GET /api/event-handlers/trigger/{triggerId} Handlers for a trigger
POST /api/event-handlers Create
PUT /api/event-handlers/{id} Update
DELETE /api/event-handlers/{id} Delete
POST /api/event-handlers/suggest-mappings Suggest parameter mappings from a payloadcurl -X POST https://service.ensembleapp.dev/api/event-handlers \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Fulfil order",
"triggerId": 42,
"handlerType": "workflow",
"workflowId": "wf-fulfil",
"eventType": "order.created",
"userExpression": "${body.customer.email}",
"workflowParams": {
"orderId": "${body.order.id}",
"orderTotal": "${body.order.total}"
}
}'| Field | Required | Notes |
|---|---|---|
name | yes | Named in every outcome and failure |
triggerId | yes | The trigger this belongs to |
handlerType | yes | workflow. orchestration is a stub that does nothing |
workflowId | with workflow | The workflow to start |
workflowParams | no | JSON; values are JEXL expressions |
userExpression | no | LiquidJS; resolves the user the workflow runs as |
eventType | no | Exact match against the classified event type. See Event types |
Unlike triggers, handlers are deleted outright.
Reading requests
GET /api/event-requests List, filterable by ?triggerId=
GET /api/event-requests/activity Aggregated activity
DELETE /api/event-requests/clear/{triggerId} Clear a trigger's request logGET /api/event-requests/activity takes hours (24, 168 or 720) and an optional
triggerId, and returns the figures behind the Webhooks activity tab: requests, how many
matched nothing, how many had a failure, handler executions, a series stacked by outcome, a
per-trigger breakdown and the recent failures.
An unrecognised hours is refused rather than clamped, so a request for a window that does
not exist does not quietly answer for 24 hours.
Monitoring
GET /api/trigger-monitoring/{triggerId} Trigger with its handlers
GET /api/trigger-monitoring/{triggerId}/requests Recent requests
GET /api/trigger-monitoring/{triggerId}/workflow-status/{workflowId} Result of a started workflow
POST /api/trigger-monitoring/{triggerId}/test-event Send a test eventThe workflow-status endpoint is how a workflow's outcome is retrieved after the fact, since
the trigger response carries only runId.
A WebSocket on the same trigger id streams the live stages described in Monitoring. Browsers cannot set headers on a WebSocket, so it authenticates with a first message carrying the token rather than putting it in the URL, where it would land in proxy and access logs.