StrataDocs

Agent webhooks

Webhooks let an external system trigger an agent run — useful for "fire when this Jira ticket changes", "run after the nightly ETL finishes", or "kick off when monitoring catches a spike". Authentication is by HMAC-SHA256 over a timestamped payload, not a session cookie, so the trigger endpoint works from any system that can hash a request.

Generate the webhook secret

Each agent has its own webhook secret. There is no global secret and no page that lists all secrets — they're scoped per agent for blast-radius reasons.

Webhooks are an API-driven feature: you generate the secret by calling the agent's webhook endpoint while signed in. Send an authenticated request as the agent's owner:

POST /api/agents/<agent-id>/webhook

The server creates a new 32-byte secret (64 hex characters) and returns it in the response, along with:

  • The trigger URL: /api/agents/<agent-id>/webhook/trigger
  • The secret version (v2) and a note explaining the headers you must send.

New secrets are version v2, which requires every request to carry a timestamp (see Sign the request). The timestamp defends against replay of an intercepted payload.

Copy the secret immediately. Strata stores only the secret value the server signs against — the secret is returned by this one call and never again. If you lose it, call POST /api/agents/<agent-id>/webhook again to rotate, then update every caller with the new value.

Sign the request

Every webhook call must include two headers:

HeaderRequiredValue
X-Webhook-TimestampYes (v2 secrets)The current Unix time — either seconds or milliseconds. The server accepts values within ±5 minutes of its own clock.
X-Webhook-SignatureYesThe hex-encoded HMAC-SHA256 of the signing string using the agent's webhook secret as the key.

The signing string is the timestamp, a literal dot, and then the raw request body:

<X-Webhook-Timestamp>.<raw request body>

Important: hash the raw bytes the client actually sends, not a re-serialized version. JSON parsers normalize whitespace and may drop trailing newlines, so a signature generated from a re-parsed-and-re-stringified body will mismatch the server's signature computed over the raw body.

Warning

v2 secrets reject any request missing X-Webhook-Timestamp with 401. Generate the timestamp at send time — a stale value (older than 5 minutes, or in the future) is also rejected with 401.

curl example

SECRET="<your agent webhook secret>"
URL="https://app.strata.kronisys.com/api/agents/<agent-id>/webhook/trigger"
BODY='{"trigger":"jira-PROJ-123","priority":"high"}'
TS=$(date +%s)

SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -sS -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Timestamp: $TS" \
  -H "X-Webhook-Signature: $SIG" \
  --data "$BODY"

Node.js example

import crypto from 'node:crypto';

const secret = process.env.STRATA_AGENT_WEBHOOK_SECRET;
const url = 'https://app.strata.kronisys.com/api/agents/<agent-id>/webhook/trigger';

const body = JSON.stringify({ trigger: 'jira-PROJ-123', priority: 'high' });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${body}`)
  .digest('hex');

const res = await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Webhook-Timestamp': timestamp,
    'X-Webhook-Signature': signature,
  },
  body,
});

console.log(res.status, await res.json());

Both examples send the exact same byte sequence to Strata that they sign, which is the rule. If you regenerate JSON or rewrite the body between signing and sending, the signatures won't match.

The webhook payload

Strata passes the request body into the agent's prompt context. The body is prepended to the agent's system prompt as a Webhook payload: block, so the agent can read the metadata you send and act on it — for example, branching on a priority field or referencing a ticket ID.

Note

The body is truncated at 32 KB; anything beyond that is dropped. Send compact JSON, not large file dumps.

The agent still runs its saved prompt — the payload augments it rather than replacing it. If you send no body (or an empty one), the agent runs exactly as it would on a scheduled trigger.

Success response

A valid request returns:

{ "ok": true, "queued": true }

The run executes asynchronously. The same in-process and cross-instance locks that protect scheduled runs apply here — if the same agent is already executing, the new trigger returns success (queued: true) but is silently skipped so a single agent never runs twice concurrently. Watch the agent's run history (trigger_type=webhook) to confirm the run actually fired.

Notifications fire on webhook runs the same way they do on scheduled runs.

No session, no CSRF

The trigger endpoint deliberately accepts requests with no Strata session and no CSRF token. A valid timestamp plus an HMAC over <timestamp>.<raw body>, validated with a constant-time compare against the per-agent secret, is the only proof of authorization. This is what makes webhooks usable from GitHub Actions, Zapier, Jira automation, Power Automate, etc.

The flip side: anyone with the secret can fire the agent. Treat it like an API key — store it in a secret manager, scope it to the smallest possible system, and rotate it if it's ever exposed.

Replay protection

Each signed request can be used once. Strata caches the signature for 10 minutes and rejects a re-send of the identical signed request with 409 Webhook replay detected. Because the signature includes the timestamp, generating a fresh timestamp per call produces a new signature each time — so legitimate repeat triggers always go through, but a captured request can't be replayed by an attacker.

Rate limits

The trigger endpoint is rate-limited to cap the damage from a leaked secret:

ScopeLimit
Per agent60 triggers per hour
Per source IP20 attempts per minute
Per source IP120 attempts per hour

Exceeding any limit returns 429 with a short retry message. The per-agent limit is loose enough for normal CI usage but caps a runaway loop.

Rotating the secret

Call POST /api/agents/<agent-id>/webhook again. The server overwrites the stored secret with a new random v2 value and returns it. Any caller still using the old secret immediately returns 401 Invalid signature on its next attempt. Update each caller with the new value to restore service.

There is no grace period — old and new secrets are not both valid at once. If you need a transition window, fire the agent in parallel against a new staging agent with its own secret until you can swap.

Disabling the webhook

Send DELETE /api/agents/<agent-id>/webhook as the agent's owner. The server clears the stored secret. Subsequent POST /trigger calls return 404 Webhook not configured for this agent. Generate a new secret to re-enable.

The agent's schedule keeps running — disabling the webhook only removes the external trigger path, not the cron-driven one.

What can go wrong

  • 401 Invalid signature — your signature doesn't match what the server computes. Common causes:
    • You signed the raw body alone instead of <timestamp>.<body>.
    • You signed a re-stringified version of the body instead of the raw bytes you're sending.
    • You used the wrong secret (typo, stale rotation).
    • Whitespace differences (trailing newline) between the body you signed and the body you sent.
  • 401 X-Webhook-Timestamp header is required — you sent a v2 secret request without the timestamp header. Add it.
  • 401 Webhook timestamp older than 5 minutes / …is in the future — your timestamp is outside the ±5-minute window. Generate it at send time and check the caller's clock.
  • 409 Webhook replay detected — you re-sent an identical signed request within 10 minutes. Generate a fresh timestamp (and therefore a fresh signature) for each trigger.
  • 429 Too many webhook triggers / …attempts from this IP — you hit a rate limit. Back off and retry after the window.
  • 404 Webhook not configured for this agent — the webhook was disabled, never generated, or the agent ID in the URL doesn't exist (deleted or mistyped). The trigger endpoint authenticates purely by the per-agent secret, so a nonexistent agent and a secret-less agent both surface as this one error. Call POST /api/agents/<agent-id>/webhook to generate a fresh secret.
  • 409 Agent is paused — the agent is disabled. Resume it from its detail view before triggering.
  • 403 Organization is suspended — the org is suspended; an admin must lift the suspension before any agent can run.
  • 400 Agent has expired — the agent's expiration date has passed. Edit the agent and extend the expiration.
  • The webhook returns 200 ok but no run appears in history — most likely a dedup against an already-running execution of the same agent. Wait for the in-flight run to finish, then trigger again. Verify by checking the Recent runs section a few seconds later.

Related