Ensemble Docs

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:

StatusBodyCause
200handlersExecuted and one results entry per handlerHandlers matched and were dispatched
401Basic Authentication required, Invalid credentialsBasic auth configured and not satisfied
404Trigger not foundUnknown code, or the trigger is archived
404No handlers found for this triggerClassified, and nothing matched
500Internal server errorPlatform 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/token

Public: 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 }
StatusBodyCause
200access_token, token_type, expires_inThe credentials verified
400invalid_requestgrant_type is missing
400unsupported_grant_typeA grant other than client_credentials
401invalid_clientUnknown client, wrong secret, disabled, or expired
405invalid_requestAnything 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 keyEffect on its clients
DisabledNone. 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.
ExpiredIts clients stop. Expiry is the end of the identity, not of one credential for it.
DeletedIts 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 tokens

POST 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 tokensReversible
DisableRefused while it is off, honoured again when you enable itYes, with nothing reconfigured
RotateDeleted. The old secret stops working; the client id staysThe caller needs the new secret
RevokeDeleted with the clientNo

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}             Archive
curl -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"
      }'
FieldRequiredNotes
nameyesDisplay name
triggerCodeyesUnique across the platform; becomes the URL
eventTypePathnoLiquidJS template with ${ }. The only way to set this — the trigger form has no field for it
authnobasic or oauth. oauth is not enforced
basicAuthUsername, basicAuthPasswordwith basicRequired 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 payload
curl -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}"
        }
      }'
FieldRequiredNotes
nameyesNamed in every outcome and failure
triggerIdyesThe trigger this belongs to
handlerTypeyesworkflow. orchestration is a stub that does nothing
workflowIdwith workflowThe workflow to start
workflowParamsnoJSON; values are JEXL expressions
userExpressionnoLiquidJS; resolves the user the workflow runs as
eventTypenoExact 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 log

GET /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 event

The 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.

On this page