Webhook delivery
Subscribe your own consumer (Linear, PagerDuty, custom dashboard, anything) to inbox and audit events with signed delivery.
If Slack isn't enough. Or you want events in your own system. Set up a webhook endpoint. Viewport POSTs a signed JSON payload to your URL on every subscribed event.
What you can subscribe to
| Event family | Examples |
|---|---|
inbox_item.* | created, resolved, dismissed, timed_out |
plan.* | submitted, approved, denied, revised |
approval_gate.* | requested, approved, denied |
context.* | candidate_proposed, candidate_approved, candidate_discarded |
workflow_run.* | started, step_started, step_completed, failed, completed |
audit.* | All audit events. Pick when you want a SIEM-style firehose. |
member.* | invited / removed / role_changed |
machine.* | paired / revoked |
You can subscribe per-event-family or to a wildcard (audit.*).
Create an endpoint
Settings → Integrations → Webhooks → New endpoint.
Name: Linear bridge
URL: https://linear-bridge.acme.dev/viewport
Events: inbox_item.created, plan.approved, plan.denied
Secret: auto-generated (rotate any time)Save. The platform POSTs a webhook.test event immediately to confirm reachability.
Payload shape
POST /viewport HTTP/1.1
Content-Type: application/json
X-Viewport-Event: inbox_item.created
X-Viewport-Delivery: dlv_01J7…
X-Viewport-Signature: t=1715000000,v1=sha256_hex_signature
X-Viewport-Workspace: 01J7M8N9P0Q1R2S3T4U5V6W7X8
{
"type": "inbox_item.created",
"id": "evt_01J7…",
"occurred_at": "2026-05-11T14:42:00Z",
"workspace": {
"id": "01J7M8N9P0Q1R2S3T4U5V6W7X8",
"slug": "acme",
"name": "Acme Engineering"
},
"actor": {
"type": "system",
"kind": "agent_request"
},
"subject": {
"type": "inbox_item",
"id": "ibx_01J7…",
"kind": "plan_review",
"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…"
}
}Signature verification
Every payload is signed. Compute the expected signature:
signed_payload = "{timestamp}.{raw_body}"
expected = HMAC-SHA256(secret, signed_payload)Compare against v1=… in X-Viewport-Signature. Reject if mismatch, or if t is older than 5 minutes (clock skew + replay protection).
Example (Node):
import crypto from 'node:crypto';
function verify(req) {
const sig = req.headers['x-viewport-signature'];
const [tPart, v1Part] = sig.split(',');
const t = tPart.replace('t=', '');
const v1 = v1Part.replace('v1=', '');
const expected = crypto
.createHmac('sha256', process.env.VIEWPORT_WEBHOOK_SECRET)
.update(`${t}.${req.rawBody}`)
.digest('hex');
return (
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) &&
Math.abs(Date.now() / 1000 - Number(t)) < 300
);
}Retries
If your endpoint returns non-2xx or times out (default 5s):
- 1st retry: 5s after the initial attempt.
- 2nd retry: 30s.
- 3rd retry: 5 min.
- 4th retry: 30 min.
- 5th retry: 2 hours.
After the 5th failed retry, the delivery is parked. You'll see it in Settings → Integrations → Webhooks → Failed deliveries with the response body for debugging.
To replay a failed delivery: click Resend in the UI, or:
POST /api/workspaces/{workspace}/webhooks/{endpoint}/deliveries/{delivery}/resendIdempotency
Every delivery has an X-Viewport-Delivery id. If you re-receive it (because of retries or replays), treat it as the same event. The consumer is expected to be idempotent.
Rate limits
Viewport limits webhook deliveries to 100 requests/second per endpoint. Bursts above that are queued. If you sustain over 100rps you'll see deliveries lag; contact us for higher limits.
Use cases
- Linear bridge: create a Linear issue when a plan is denied with a comment.
- PagerDuty: page on-call when an approval gate times out for a
prodworkflow. - Custom dashboard: aggregate session activity, mean-time-to-decide, by team or by repo.
- SIEM ingest: subscribe to
audit.*and forward to Splunk / Datadog.
Where to go next
- Reference: Webhooks. Full event schema reference.
- Slack integration. The simpler path if Slack is enough.
- Audit log.