Skip to main content
How automations behave

Webhook ingestion contract (reference)

Webhook-triggered automations expose a unique, public HTTPS endpoint that external systems (your PSA, RMM, alerting stack, Zapier, and so on) POST to. This page is the full contract for anyone…

Written By Christopher Scaminaci

Last updated 3 days ago

Webhook-triggered automations expose a unique, public HTTPS endpoint that external systems (your PSA, RMM, alerting stack, Zapier, and so on) POST to. This page is the full contract for anyone configuring a sender: the URL model, authentication, optional request signing, payload handling, limits, every response code you can receive, and the best-effort receipt log used to investigate delivery outcomes.

The endpoint

Each webhook automation gets a unique URL of the form:

POST https://<automations-host>/webhooks/{automationId}/{webhookSecret}

Copy the complete URL from the automation's webhook configuration panel in the portal — don't construct it by hand. The secret embedded in the URL path is the primary authentication: anyone with the full URL can trigger the automation (subject to the limits below), so treat it like a password.

Authentication: two independent secrets

SecretWhere it livesPurpose
URL secretEmbedded in the webhook URL pathPrimary auth. Compared in constant time; wrong secret → 401.
Signing secretShown in the configuration panel; used by your sender to sign request bodiesOptional per-request body integrity (HMAC).

Both secrets are 32 random bytes, hex-encoded (64 hex characters). They are generated when the automation is provisioned as (or switched to) a webhook trigger. Switching the automation away from the webhook trigger preserves both secrets, so the URL still works if you switch back.

Optional request signing (HMAC-SHA256)

To protect against body tampering, your sender can sign each request:

  1. Compute HMAC-SHA256(rawRequestBody, signingSecret) — the raw 32-byte digest.
  2. Send it in the X-StackJack-Signature header.

StackJack decodes the header back to those raw digest bytes before comparing, so encoding does not affect the verdict. Any of these spellings of the same digest is accepted:

Header valueLengthNotes
sha256=<hex>64 hex chars after the prefixThe GitHub/Slack/Stripe convention
bare <hex>64For senders that strip the prefix
bare base6444 (including = padding)What HaloPSA's webhook signer emits
base64url43 or 44 (padding optional)- and _ in place of + and /

Hex casing is irrelevant, and the sha256= prefix may be combined with any of the encodings. Whatever the spelling, the value must decode to exactly 32 bytes — anything else, including a different algorithm prefix such as sha384=, is rejected.

Signature semantics:

  • No header sent → verification is skipped (signing is opt-in per request).
  • Header sent, signature wrong → 401 with support code SJ-WH-SIG-INVALID. When its receipt persists, the log distinguishes an unparseable header (not one of the encodings above, or not 32 bytes once decoded) from a digest mismatch (the header decoded cleanly, but the digest differs — meaning the signing secret or the signed byte range differs from ours; StackJack signs the raw request body). It records the header's length and the reason, never digest values.
  • Header sent, but the automation has no signing secret yet (it predates signing support) → 401 with support code SJ-WH-SIG-MISSING-SECRET. Rotate the webhook secrets once to provision a signing secret. StackJack deliberately rejects rather than silently accepting an unverifiable "signed" request.

Rotating secrets

The Rotate action regenerates both secrets at once. The old URL and old signing secret stop working immediately — update every sender when you rotate.

Payload handling

Choose how much of the request body reaches the agent via the automation's payload mode:

ModeWhat the agent receives
NoneNothing — the webhook is a pure "go" signal.
Full contextThe entire raw request body.
Selected fieldsA JSON object containing only the fields you list as dotted paths ($.ticket.id, $.alert.severity). Simple object paths only — no array indices or wildcards. Keys in the result are the original path expressions. If the body isn't valid JSON or no paths match, the agent receives no payload.

Size limits

  • Transport cap: 64 KB raw body, absolute. Larger requests are rejected.
  • Per-automation payload cap: after payload-mode filtering, the payload must fit the automation's configured limit — default 32 KB, configurable up to the 64 KB transport ceiling. Exceeding it → 413.

Rate limit

Each automation accepts at most 10 webhook invocations per one-minute window. Beyond that, the rate-limited attempt receives 429 and starts no new run. The rate-limit check happens before body reading, signature verification, and deduplication, so a 429 has no original run ID even when the same body may have been accepted earlier. This is separate from execution capacity: a delivery that passes the rate limit but finds all slots busy is normally accepted into the durable run queue.

Byte-identical redelivery handling

Webhook deduplication runs after URL-secret authentication and the per-automation rate-limit check, and after signature verification, but before payload filtering or run creation. For an automation with deduplication enabled, StackJack fingerprints the raw body with its character length and SHA-256 over its UTF-8 bytes.

  • If the request reaches the deduplication check and matches a successfully persisted Accepted or earlier Duplicate receipt for the same automation inside the default 60-second window, it starts no second run.
  • That matched request receives 200 with duplicate: true and the original run ID. Treat this response as accounted for and do not retry it.
  • This 200 is not guaranteed for every otherwise-identical resend. The rate limiter runs first and can return 429 with no run ID before deduplication. Receipt persistence is also best-effort; if the earlier receipt was not saved, there is no stored match to suppress another run.
  • A prior failed or rejected delivery never suppresses a retry. Only a delivery that produced a run (or deduplicated to one) can match.
  • Whitespace, property order, formatting, or any other body-byte change produces a different fingerprint. Deduplication is not semantic JSON matching.

New automations are created with deduplication enabled. Automations that existed before the setting was introduced were deliberately not backfilled and retain their earlier no-dedup behavior.

Smart filters

A webhook trigger can carry a smart filter: a short typed judgement that decides whether a delivery is worth a run. It is optional and off unless the author adds one.

Where it sits in the pipeline. After URL-secret authentication, the rate limit, signature verification, deduplication and payload-mode filtering, and after the per-automation payload cap — and before any run row is created. Both ends of that placement are deliberate: the filter judges the same filtered payload the run would have received, and a filtered delivery costs no run and no capacity. The filter's own judgement is a fast decision and is priced separately — see Fast Decisions and Your Own TypeSafe Key.

What it judges. The automation's own filtered trigger payload — never the raw body, which would defeat None mode. An automation in None mode therefore cannot carry a filter at all; the save is refused. A delivery that produces no payload is not judged and the run starts.

How the verdict is reached. The author writes one or more questions and at least one runWhen condition. When all conditions pass, the run starts. StackJack flags payloads that try to steer the filter on the delivery's receipt.

Failure behaviour is the author's choice. onUnavailable is Run (the default) or Skip. The whole evaluation, vendor retry included, is bounded at two seconds.

Deduplication interaction. The filter runs after deduplication, so a byte-identical resend of a delivery that started a run, inside the 60-second window, is answered from the duplicate path and never re-judged — but a resend that arrives outside the window, or against an automation with deduplication disabled, is judged again and can reach a different verdict. Filtered is deliberately not in the set of outcomes that suppress a later duplicate: that set protects recoverable failures, and a filtered delivery was a decision, not a failure.

Every evaluation is recorded against the automation and shown on its webhook receipts panel with what was asked and what came back. If an automation's stored filter cannot be read, the Portal marks it Not Ready.

Response codes (sender's reference)

This is the canonical response table for the webhook endpoint. Configure sender retries from the final column, not from a blanket “retry every non-2xx” rule.

Sender outcomeHTTPRun ID and responseRetry?
Accepted for execution202A new runId and current status are returned.No. StackJack owns the run.
Accepted but queued at capacity or behind the same automation202A new runId, status: "Queued", and queued: true are returned.No. Retrying would create additional work; watch the returned run.
Accepted but held because the AI service is overloaded202A new runId, status: "LaunchHeld", held: true, and nextAttemptAt are returned. StackJack tries to start the run again automatically.No. StackJack owns the run; a byte-identical resend inside the deduplication window returns this same run.
Byte-identical duplicate (deduplication enabled)200If the request reaches deduplication and matches a persisted receipt, duplicate: true and the original runId are returned. No new run is created.No. This response means the delivery is already accounted for.
Filtered by the automation's smart filter200{ "status": "filtered" }. No run is created and no run ID is returned. The response is deliberately the same whatever the filter decided — the sender is not the author, and the judgement is not its business.No. The delivery was received and deliberately not acted on.
Authentication or signature failure401Wrong URL secret returns an empty unauthorized response. Signature failures include SJ-WH-SIG-INVALID or SJ-WH-SIG-MISSING-SECRET. No run ID.No, not unchanged. Fix or rotate the secret/signature, then send again as a corrected delivery.
Malformed or invalid request for this endpoint400The usual case is an active automation that is not configured for webhook triggers. Malformed HTTP can also be rejected by the web server before it reaches the automation. No run ID.No, not unchanged. Fix the request or automation trigger type.
Insufficient credits402error: "insufficient_credits", the refused run's runId and status, and retryable: false; no Retry-After.No until credits are added. The returned run is terminal and did not execute.
Agent Runner plan ended while a BYOK key is on file402error: "agent_runner_base_lapsed", the refused run's runId and status, and retryable: false; no Retry-After.No until the tenant acts. Re-subscribe to Agent Runner, or remove the key so runs go on credits.
Unknown, inactive, archived, or tenant-disabled automation404Empty response; these cases are deliberately indistinguishable. No run ID.No, not unchanged. Correct the URL or automation/tenant state.
Payload too large413The raw body exceeded the 64 KB transport limit, or the filtered payload exceeded the automation's cap. No run ID.No, not unchanged. Shrink the request.
Rate limit exceeded429More than 10 attempts reached this automation in its one-minute window. No run ID. Rate limiting occurs before signature verification and deduplication, so an otherwise-identical resend can receive this instead of the duplicate 200.Only with duplicate awareness. Back off past the window, but do not treat the missing run ID as proof that an earlier matching delivery did not create a run. A resend may duplicate work.
Capacity fallback429error: "capacity_exceeded", retryable: true, and Retry-After: 30; no run ID. Normally, capacity pressure is the queued 202 case above.Yes. Honor Retry-After.
Ambiguous server error500{ "error": "Failed to start run" }; no run ID. The error can happen before any row exists, or after a Pending run was already persisted. That persisted run may later recover and execute.Do not retry unconditionally. No run ID is not proof that no run exists. The endpoint provides no idempotency guarantee for this outcome, and a retry can create duplicate work. Investigate first or accept at-least-once risk.
Run created but background launch failed503error: "launch_failed", the terminal failed run's runId and status, retryable: true, and Retry-After: 30. When the AI service is overloaded, the run can be held instead (the 202 above).Yes. The retry creates a new run; the returned original run is already terminal.

Invalid JSON is not automatically a 400. In Selected fields mode, invalid JSON or no matching paths produces no trigger payload; the otherwise-valid delivery can still be accepted. The optional example payload is documentation only and is never a validation schema.

Treat a 500 as an ambiguous, at-least-once delivery outcome, not a clean refusal. StackJack can persist a Pending run before a later startup operation throws; the webhook response then has no run ID, while crash recovery can still execute that persisted run. Check run history and any available receipts, and contact support with the automation ID, response time, and sender details when the outcome cannot be reconciled. A missing receipt does not resolve the ambiguity.

See Triggers and Execution Guarantees for queue expiry, cancellation, overlap, and restart behavior after a 202.

The webhook receipt log

StackJack attempts to record each application-level outcome that reaches a known, live automation in its webhook receipt log, visible in the portal (StackJack staff can see a mirror for support). Receipt persistence is best-effort, and a missing receipt does not prove that no request or run exists. A missing receipt therefore does not prove that the request never reached StackJack or that no run was created.

Deliveries that 404 because the automation ID is unknown, inactive, or archived are deliberately not submitted to the receipt log, so internet scanner noise cannot flood it. A 404 caused by the tenant safety switch is normally submitted as TenantDisabled. A request rejected by the web server before the controller runs — for example at the transport-size boundary — may also have no receipt. Treat the log as useful operational evidence, not as an idempotency or delivery ledger; give support the automation ID, approximate UTC time, sender address, and HTTP response when an expected receipt is absent.

Each successfully persisted receipt records:

FieldNotes
Received atUTC timestamp
OutcomeOne of: Accepted, Duplicate, TenantDisabled, WrongTriggerType, InvalidSecret, InvalidSignature, RateLimited, PayloadTooLarge, CapacityExceeded, OrchestrationFailed, CreditExhausted (402), EntitlementLapsed (402), LaunchFailed (503)
HTTP status returnedWhat your sender saw
Remote IPThe request's source IP address as observed by StackJack
User agentTruncated to 256 characters
Payload bytes receivedRaw body size, before payload-mode filtering
RunThe new run for Accepted; duplicate receipts retain the original run ID separately so a redelivery still resolves to the first run.
DetailRedacted reason text — never contains secrets or signature digests

Retention: receipts are currently kept indefinitely — there is no automatic purge. The portal's log view shows the most recent receipts (up to 500 per query).

Troubleshooting checklist

  1. Getting 404? Confirm the automation is active and not archived, and that you copied the full current URL (a rotation changes it). A 404 can also mean automations are disabled for your tenant — contact StackJack support.
  2. Getting 401 with no signature header? Your URL secret is stale — re-copy the URL.
  3. Getting SJ-WH-SIG-MISSING-SECRET? Rotate the secrets once to provision a signing secret, then update the sender with the new values.
  4. Getting SJ-WH-SIG-INVALID? If a receipt exists, check its detail line first — it says whether the header could not be parsed at all or whether it parsed and the digest simply differed. A digest mismatch means the secret or the signed bytes differ: sign the raw body exactly as sent, not a re-serialized or pretty-printed copy of it (a reformatted body is a different byte sequence and can never match).
  5. Getting 429s? Check whether you're over 10/minute for this automation (a successfully saved receipt shows RateLimited). Because this check runs before deduplication, a rate-limited identical resend has no original run ID; do not assume an earlier delivery is absent. A CapacityExceeded receipt is the capacity fallback; honor its Retry-After. Normal capacity pressure returns a queued 202, not 429.
  6. Getting 500? Do not retry blindly. A Pending run may already exist and can later execute even though the response has no run ID. Check run history and receipts, then contact support with the automation ID and response time if the outcome remains unclear. Retrying accepts duplicate-work risk.
  7. Agent didn't receive expected data? Check the payload mode and, for selected fields, that your dotted paths match the actual JSON structure — non-matching paths are silently omitted.
  8. Got 200 with duplicate: true? Do not send again. Use the returned original run ID to inspect the execution that already accounts for this delivery.