Inbox Payload (POST /api/v1/inbox)
The receiver-side webhook for external commitments. Documented here from the integrator's perspective: "I am Jira; I want my ticket to become a Shrubbery."
For the full HTTP reference (status codes, sample request/response, Scalar surface), see POST /api/v1/inbox. This page focuses on the payload contract and the byte-for-byte mapping to the runtime Zod schema.
Schema (source of truth)
The schema below is the same inboxPayloadSchema the route handler uses at request time. It lives in src/lib/schemas/webhook-payloads.ts and emits the OpenAPI fragment at content/docs/_generated/openapi.json. No manual sync.
import { z } from 'zod';
export const inboxPayloadSchema = z.object({
assignerEmail: z.string().email(),
assigneeEmail: z.string().email(),
description: z.string().trim().min(1).max(2000),
deadline: z.string().datetime().nullable(),
externalSystem: z.string().trim().min(1).max(32),
externalId: z.string().trim().min(1).max(128),
});Fields
| Field | Type | Required | Description |
|---|---|---|---|
assignerEmail | string (RFC 5322) | yes | Must resolve to a Shrubbery user and match the token owner. Otherwise 403 forbidden. |
assigneeEmail | string (RFC 5322) | yes | Must resolve to an existing Shrubbery user. Email matching is case-insensitive (ILIKE). |
description | string (1..2000) | yes | Objective of the commitment. Trimmed server-side; whitespace-only fails validation. |
deadline | ISO-8601 datetime | null | yes | Absolute deadline. null means "no deadline tracked" — the Shrubbery still counts toward Flow Score on completion. Relative phrases ("next Friday") are not accepted here; resolve them on your end first. |
externalSystem | string (1..32) | yes | Short slug identifying the source. Convention: lowercase, no spaces. Examples: jira, linear, github, slack. |
externalId | string (1..128) | yes | Stable identifier within the external system. Examples: Jira key (PROJ-123), GitHub issue node (I_kw…). Used together with externalSystem for uniqueness. |
Uniqueness rule
The database enforces UNIQUE (external_system, external_id, assigner). Two different leads may independently track the same Jira ticket without collision — each gets their own Shrubbery row. The same lead replaying the same (externalSystem, externalId) returns 409 duplicate_external_ref.
What happens after a successful POST
- The route handler verifies the Bearer token (server-side hash lookup against
public.api_tokens). - Both emails are resolved against
public.users. - A row is inserted into
public.shrubberieswithstatus = 'Pending_Handshake', the resolved assigner / assignee UUIDs, the(externalSystem, externalId)mapping, and the description as bothextracted_taskand (left null on)raw_text. - An audit event is appended to
public.shrubbery_eventswithactor_kind = 'api_token', the token ID, and the external mapping echoed inpayload. - Inngest fires the
handshakeRequestedEvent; the assignee receives a Nudge through their preferred channel. - The handler returns
201 Createdwith{ id }.
The Shrubbery is immediately visible in the assignee's Accord. It does not appear in the assigner's Garden Draft column — inbox-minted rows skip the Draft step because the token mint already expressed intent.
Worked examples
Jira → Shrubbery
{
"assignerEmail": "lead@example.com",
"assigneeEmail": "knight@example.com",
"description": "Investigate the flaky CI run for project ALPHA",
"deadline": "2026-06-15T17:00:00.000Z",
"externalSystem": "jira",
"externalId": "ALPHA-417"
}Linear → Shrubbery
{
"assignerEmail": "lead@example.com",
"assigneeEmail": "knight@example.com",
"description": "Spike: evaluate Pagefind for the docs portal",
"deadline": null,
"externalSystem": "linear",
"externalId": "ENG-2031"
}Slack message → Shrubbery (no upstream tracker)
{
"assignerEmail": "lead@example.com",
"assigneeEmail": "knight@example.com",
"description": "Draft the Q3 board update by EOW",
"deadline": "2026-09-05T16:00:00.000Z",
"externalSystem": "slack",
"externalId": "C0123ABC/p1726820400.123456"
}The externalId here is the Slack channel + message-timestamp tuple; pick whatever scheme makes future sync calls unambiguous. The pair is opaque to Shrubbery beyond the uniqueness constraint.
What this payload does not carry
- Priority / labels / tags — Shrubbery v1 does not track these. The commitment is binary (active vs not); reliability metrics are the surface.
- Multiple assignees — one Knight per row. If the upstream issue has multiple assignees, mint one Shrubbery per assignee.
- Attachments — file attachments on Shrubberies are tracked as a backlog item ("Rich Context Attachments"). Until they ship, link to the upstream ticket via the
(externalSystem, externalId)mapping, which surfaces as a deep link on the Detail Sheet viasrc/lib/external-links.ts. - Custom fields — no extension point in v1. Sub-process the upstream payload on your side; only the canonical fields above are stored.
Last updated: 17 May 2026