Viewport daemon, relay, and hosted runtime are available in alpha. Surfaces may change.
VIEWPORT
Reference

Webhooks

Event schema, signing model, retry behavior. Reference for what every payload contains.

The outbound setup flow is in Teams: Webhook delivery. This page is the reference for payloads Viewport sends to your own systems.

Inbound provider events that start workflow runs are configured from Workflows -> Triggers. Those routes use provider-specific event normalization before they queue a run; they are separate from the outbound delivery payloads below.

Headers

Every delivery includes:

HeaderPurpose
X-Viewport-EventEvent family, e.g., inbox_item.created.
X-Viewport-DeliveryULID, unique per attempt. Use for idempotency.
X-Viewport-Signaturet=<unix_ts>,v1=<hex_hmac_sha256>
X-Viewport-WorkspaceSource workspace ULID.
User-AgentViewport-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_…",
  "event_id": "evt_01J7…",
  "payload_digest": "sha256:…",
  "proposed_by_session_id": "ses_…"
}

Vault content itself is never in the webhook. Only candidate metadata is sent. Decryption happens on a trusted edge that has a key grant.

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

AttemptDelay from initial
Initial0
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.

Where to go next

On this page