ShrubberyDocs
Sign in

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

FieldTypeRequiredDescription
assignerEmailstring (RFC 5322)yesMust resolve to a Shrubbery user and match the token owner. Otherwise 403 forbidden.
assigneeEmailstring (RFC 5322)yesMust resolve to an existing Shrubbery user. Email matching is case-insensitive (ILIKE).
descriptionstring (1..2000)yesObjective of the commitment. Trimmed server-side; whitespace-only fails validation.
deadlineISO-8601 datetime | nullyesAbsolute 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.
externalSystemstring (1..32)yesShort slug identifying the source. Convention: lowercase, no spaces. Examples: jira, linear, github, slack.
externalIdstring (1..128)yesStable 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

  1. The route handler verifies the Bearer token (server-side hash lookup against public.api_tokens).
  2. Both emails are resolved against public.users.
  3. A row is inserted into public.shrubberies with status = 'Pending_Handshake', the resolved assigner / assignee UUIDs, the (externalSystem, externalId) mapping, and the description as both extracted_task and (left null on) raw_text.
  4. An audit event is appended to public.shrubbery_events with actor_kind = 'api_token', the token ID, and the external mapping echoed in payload.
  5. Inngest fires the handshakeRequestedEvent; the assignee receives a Nudge through their preferred channel.
  6. The handler returns 201 Created with { 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 via src/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