Webhooks
Event schema, signing model, retry behavior. Reference for what every payload contains.
The setup flow is in Teams: Webhook delivery. This page is the reference for what's actually delivered.
Headers
Every delivery includes:
| Header | Purpose |
|---|---|
X-Viewport-Event | Event family, e.g., inbox_item.created. |
X-Viewport-Delivery | ULID, unique per attempt. Use for idempotency. |
X-Viewport-Signature | t=<unix_ts>,v1=<hex_hmac_sha256> |
X-Viewport-Workspace | Source workspace ULID. |
User-Agent | Viewport-Webhooks/1.0 |
Signature scheme: HMAC-SHA256(secret, "{t}.{raw_body}"). Reject if t is older than 300s. See Teams: Webhook delivery for code.
Common payload envelope
{
"type": "inbox_item.created",
"id": "evt_01J7…",
"occurred_at": "2026-05-11T14:42:00Z",
"workspace": {
"id": "01J7M8N9P0Q1R2S3T4U5V6W7X8",
"slug": "acme",
"name": "Acme Engineering"
},
"actor": {
"type": "user" | "system" | "agent",
"id": "usr_…" | null,
"email": "alice@acme.dev" | null,
"kind": "agent_request" | null // for system/agent actors
},
"subject": { /* event-specific */ }
}Event reference
inbox_item.*
inbox_item.created:
"subject": {
"type": "inbox_item",
"id": "ibx_01J7…",
"kind": "plan_review" | "approval_gate" | "context_candidate",
"session_id": "ses_01J7…",
"preview": "Refactor auth middleware to bearer tokens.",
"expires_at": "2026-05-11T15:42:00Z",
"decision_url": "https://app.getviewport.com/inbox/ibx_01J7…",
"audience": ["team:platform"]
}inbox_item.resolved:
"subject": {
"type": "inbox_item",
"id": "ibx_01J7…",
"decision": "approve" | "deny",
"decided_at": "…",
"decider": { "type": "user", "id": "usr_…", "email": "alice@acme.dev" },
"note": "string or null",
"source": "web" | "mobile" | "slack" | "webhook" | "auto"
}inbox_item.dismissed, inbox_item.timed_out: same shape minus decider and decision.
plan.*
plan.submitted, plan.approved, plan.denied, plan.revised:
"subject": {
"type": "plan",
"id": "pln_01J7…",
"title": "Migrate auth middleware",
"session_id": "ses_…",
"files_changed": 9,
"lines_changed_lte": 240,
"url": "https://app.getviewport.com/plans/pln_01J7…"
}approval_gate.*
approval_gate.requested, approval_gate.approved, approval_gate.denied:
"subject": {
"type": "approval_gate",
"id": "agt_01J7…",
"inbox_item_id": "ibx_…",
"workflow_id": "wfd_…",
"node": "publish-release",
"question": "Tag and push v3.4.0?",
"tags": ["release", "destructive"]
}context.*
context.candidate_proposed, context.candidate_approved, context.candidate_discarded:
"subject": {
"type": "context_candidate",
"id": "ccx_01J7…",
"vault_id": "vlt_…",
"title": "Recovery for failed migrations",
"preview_first_line": "When db:migrate fails twice...",
"proposed_by_session_id": "ses_…",
"rationale_preview": "…"
}Vault content itself is never in the webhook. Only the candidate metadata. Decryption is client-side.
workflow_run.*
workflow_run.started, workflow_run.step_started, workflow_run.step_completed, workflow_run.failed, workflow_run.completed:
"subject": {
"type": "workflow_run",
"id": "wfr_01J7…",
"workflow_id": "wfd_…",
"trigger": "manual" | "schedule" | "webhook" | "push",
"status": "running" | "completed" | "failed" | "cancelled",
"node_id": "step-7" | null,
"duration_ms": 12450 | null,
"url": "https://app.getviewport.com/workflows/runs/wfr_…"
}audit.*
Full audit event subset is delivered when subscribed. See Audit log.
member.*
member.invited, member.accepted, member.role_changed, member.removed:
"subject": {
"type": "membership",
"user_id": "usr_…",
"email": "alice@acme.dev",
"role": "admin",
"previous_role": "member" | null,
"team_grants": ["team:platform"]
}machine.*
machine.paired, machine.revoked:
"subject": {
"type": "machine",
"id": "mch_…",
"install_id": "ins_…",
"name": "mehr-mbp",
"os": "darwin",
"version": "0.2.1"
}Retry policy
| Attempt | Delay from initial |
|---|---|
| Initial | 0 |
| Retry 1 | +5s |
| Retry 2 | +30s |
| Retry 3 | +5m |
| Retry 4 | +30m |
| Retry 5 | +2h |
After 5 failed retries, the delivery is parked. Resend manually from the dashboard.
Best practices
- Be idempotent. Same
X-Viewport-Delivery= same event. Replays happen. - Acknowledge fast. Return 2xx in under 5s. If you need to do heavy work, queue it and return 2xx immediately.
- Validate signatures. Don't skip this. The endpoint URL is not secret.
- Log every delivery. When something's wrong, the delivery id is the only debug primitive.