Webhooks Overview
Two perspectives on webhooks live in Shrubbery:
- Receiver webhooks — external systems POST to Shrubbery to mint commitments and drive state changes. Documented under this section.
- Sender webhooks (outbound) — Shrubbery POSTs to external systems on Shrubbery-side state changes. Planned but not yet implemented; see Outbound Webhooks (planned).
This page covers the cross-cutting contract for both sides: authentication, retry semantics, signature posture, and idempotency.
Receiver-side contract
When you POST to /api/v1/inbox or /api/v1/sync:
- Authentication.
Authorization: Bearer <token>where the token was minted in Settings → API Tokens. The token owner becomes the implicit assigner / row owner; cross-user mutation is not possible from a single token. See API Getting Started for the mint flow. - Idempotency.
/api/v1/inboxenforces uniqueness on(externalSystem, externalId, assigner). Replaying the same payload returns409 duplicate_external_ref; the original Shrubbery is untouched./api/v1/syncis idempotent by design — replaying a transition into the current status is a no-op200 OK. - Retry guidance. Retry only on
5xx.4xxfailures are deterministic for the same payload; retrying without changes will fail identically. On429from/api/gather(not in scope here, but mentioned for completeness), honour theRetry-Afterheader. - Signature verification. Not used. Shrubbery authenticates the request with the Bearer token, not a body signature. The token is the trust root; protect it accordingly. If you need request-body integrity over the wire, ensure TLS is enforced end-to-end (no proxy that re-signs or rewrites). Future per-token HMAC signing is tracked in the backlog under "Per-token IP allowlists" and "Per-token scopes" entries.
- Transport. HTTPS in production. HTTP is accepted only on
localhostduring development. - Content type.
application/jsonfor both request and response. Any other type returns400.
Sender-side contract (planned)
When Shrubbery POSTs to your endpoint (not implemented today):
- Authentication. The plan is HMAC-signed payloads using a shared secret you register per endpoint, similar to GitHub webhooks. Header:
X-Shrubbery-Signature: sha256=<hex-hmac>. Your endpoint computes the HMAC over the raw request body using the shared secret and constant-time-compares against the header. - Retry semantics. Backed by Inngest. On non-2xx response, exponential backoff up to 24 hours. Permanent failure after the back-off envelope expires.
- Event types. First planned event is
shrubbery.completed(mirror of inbound sync). Future events:shrubbery.refused,shrubbery.renegotiated. Each event carries the Shrubberyid, the new status, the(externalSystem, externalId)mapping, and an event timestamp.
See Outbound Webhooks (planned) for the proposed payload shapes and the backlog reference.
Audit attribution
Every state-changing webhook call (inbound) appends a row to public.shrubbery_events with:
actor_kind = 'api_token'token_id = <the calling token's UUID>user_id = <the token owner>payload = <the relevant subset of the request body>
The Detail Sheet's Timeline surfaces this attribution; users see "via API token (Jira integration)" rather than "you" on the audit row. The distinction matters for the Dossier — a Knight whose work was auto-completed by an external system reads differently from a Knight who self-marked complete in the Accord.
CFS arithmetic does not distinguish actor kinds; whether you complete a Shrubbery via the UI or via /api/v1/sync, it counts the same way against Flow Score. See CFS Rules for the full inclusion logic.
Endpoint reference
- Inbox payload shape —
POST /api/v1/inbox, the inbound commitment. - Outbound completed (planned) — proposed shape; not yet implemented.
For the full request/response surface with embedded Scalar, see API Reference Overview.
Last updated: 17 May 2026