Skip to content

API triggers and webhooks

Any workflow can be started over HTTP by another service. To start one from a Slack or Telegram message instead, see Chat triggers. For the Python and TypeScript SDKs and every error code, see Public API and SDKs.

Trigger via API

Before you begin

  • Owner access to the workflow: you own it, or you are an admin. Editors of a shared workflow can run it but cannot create tokens.
  • A saved workflow. API runs use the latest saved version.
  • The worker running (by default it runs inside the API process).

Create a token

⋯ → API in the editor creates a workflow token (wft_…) and a signing secret (whsec_…). Each is shown once. Revoke either at any time; rotating the secret keeps the previous one valid for STUDIO_SECRET_GRACE seconds (default one day).

Start a run

curl -X POST http://localhost:8080/api/hooks/workflows/<wid>/runs \
  -H "Authorization: Bearer wft_..." -H "Content-Type: application/json" \
  -d '{"input": {"message": "hi"}, "mode": "sync"}'
Field Meaning
input The initial state.
mode async (default) returns 202 {run_id, status: "queued"} at once. sync waits for final_state, at most STUDIO_SYNC_WAIT_MAX seconds (default 30), then falls back to 202 with poll_url.
callback_url Optional. Receives a POST with X-Studio-Event: run.finished when the run ends.

Poll with GET /api/hooks/workflows/<wid>/runs/<run_id>. A bad token is 401 TRIGGER_TOKEN_INVALID. API runs carry an API badge in History.

Verify callbacks

Each callback carries X-Studio-Signature: t=<timestamp>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with the signing secret. Reject a timestamp more than five minutes off. During a secret rotation the header carries two v1 values; accept the request when any of them matches.

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    mac = hmac.new(secret.encode(), f'{parts["t"]}.'.encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts["v1"])

Deliveries and retries

Deliveries are stored with status, attempts, HTTP code and latency under Deliveries in the API dialog, with Resend. Network errors and non-2xx responses retry with backoff min(STUDIO_WEBHOOK_BACKOFF_BASE^n, STUDIO_WEBHOOK_BACKOFF_MAX), up to STUDIO_WEBHOOK_MAX_ATTEMPTS. Receivers should deduplicate on run_id plus the event.

Callback URLs go through the SSRF guard: the hostname is resolved once, every address is checked, and the connection goes to that IP without following redirects. Private addresses fail at once unless STUDIO_ALLOW_PRIVATE_CALLBACKS=1.

Rate limits

Each token may start STUDIO_TRIGGER_RATE_LIMIT runs per STUDIO_TRIGGER_RATE_WINDOW seconds (default 60 per 60). Over the limit the API returns 429 RATE_LIMITED with Retry-After. Polling is not limited. The counter is shared through Redis; without Redis each process counts alone.

Where API runs execute

Queued runs run on the worker. See Deploy with Compose for running workers as their own processes.