Milestone 14: Integrations (webhooks and public API)
Purpose: Tenants with entitlement can configure webhooks and use the public REST API for tickets, messages, inboxes, tags (read and limited write; no outbound reply via API).
Exit state: WebhookEndpoint CRUD; event fan-out with retries and delivery log; Public API REST with API key auth and rate limit; OpenAPI spec.
Spec reference: §2.8 Integrations, §4.8 Webhooks, §4.9 Rate limits, §5.16 WebhookEndpoint, §5.17 WebhookDelivery, §5.18 ApiKey, §6.10 settings/webhooks, settings/api.
Prerequisites: M13. Entitlements (webhooks = Business+, public_api = Business+) from M10.
14.1 Webhooks
Tasks
-
WebhookEndpoint CRUD
- tRPC: list, create, update, delete. Table: tenant_id, url, secret (store encrypted or hashed), events (array of event types), enabled. Section 5.16. On create: generate secret; show once in UI ("Copy secret — you won't see it again"). Settings → Webhooks: list endpoints (URL, enabled, events, last delivery status); Add endpoint (URL, events multi-select); Edit/Delete. Gate by webhooks entitlement. Section 6.10.
-
Event fan-out
- When events occur (ticket.created, ticket.updated, ticket.assigned, ticket.status_changed, ticket.closed, message.received, message.sent, inbox.error): for each webhook_endpoint with that event in events[] and enabled=true, enqueue Inngest job with payload (event, event_id, tenant_id, created_at, data per Section 2.8). Payload and envelope per spec; HMAC X-InboxOps-Signature (SHA-256 of body with secret). Section 2.8.
-
Delivery job
- Inngest function: POST to endpoint URL with JSON body and signature header. On response: store webhook_deliveries row (endpoint_id, event, event_id, payload, response_status, response_body snippet, attempt, succeeded, delivered_at). Retry with exponential backoff; max 10 attempts; schedule per Section 2.8 (1m, 2m, 4m, ...). On success 2xx mark succeeded; on failure retry; after 10 failures mark failed and stop. Section 4.9.
-
Delivery log UI
- Settings → Webhooks: per endpoint, show last deliveries (table or expandable); status, response code, time. Optional: "Test" button to send test payload. Section 6.10. Retention: last 100 or 30 days; prune in cron (M16 or here).
Acceptance criteria
| Criterion | Status |
|---|---|
| Admin can add webhook endpoint with URL and events; secret shown once; endpoint receives POST on events. | Pending |
| Payload and signature match Section 2.8; signature verifiable with secret. | Pending |
| Retries and delivery log work; failed deliveries visible in UI. | Pending |
| Webhooks gated by Business plan (or webhooks entitlement). | Pending |
| Webhook delivery log updates in real time when new deliveries complete (SSE or refetch so latest status visible without manual refresh). | Pending |
14.2 Public API
Tasks
-
API key CRUD
- tRPC: list (name, key_prefix, last_used_at, created_at); create (name → generate key, store key_hash and key_prefix, return full key once); revoke (set revoked_at). Section 5.18. Key format: ixk_ + random segment; prefix first 8 chars in UI. Settings → API: list keys, Create (name, show key once), Revoke. Gate by public_api entitlement. Section 6.10.
-
REST API routes
- Base path /api/v1. Middleware: extract Authorization Bearer token from header; hash and lookup api_keys; validate not revoked; load tenant_id; check tenant_entitlements for public_api; rate limit (e.g. 100 req/min per tenant, DB sliding window). Section 4.4, 2.8. Attach tenantId to request.
-
Endpoints (Section 2.8)
- GET /tickets (query: status, inbox_id, assignee_id, priority, page, per_page max 100). GET /tickets/:id (id = UUID or ticket_number). POST /tickets (body: subject, inbox_id, priority?, assignee_id?). PATCH /tickets/:id (subject, status, priority, assignee_id, inbox_id). GET /tickets/:id/messages (exclude internal notes). POST /tickets/:id/messages (body: body_html or body_text, type: internal_note — no outbound reply). GET /tickets/:id/tags, POST /tickets/:id/tags (tag_id or tag_name), DELETE /tickets/:id/tags/:tag_id. GET /inboxes, GET /inboxes/:id. GET /tags. All tenant-scoped; JSON responses; errors
{ error: { code, message } }. Pagination meta: page, per_page, total. Section 2.8.
- GET /tickets (query: status, inbox_id, assignee_id, priority, page, per_page max 100). GET /tickets/:id (id = UUID or ticket_number). POST /tickets (body: subject, inbox_id, priority?, assignee_id?). PATCH /tickets/:id (subject, status, priority, assignee_id, inbox_id). GET /tickets/:id/messages (exclude internal notes). POST /tickets/:id/messages (body: body_html or body_text, type: internal_note — no outbound reply). GET /tickets/:id/tags, POST /tickets/:id/tags (tag_id or tag_name), DELETE /tickets/:id/tags/:tag_id. GET /inboxes, GET /inboxes/:id. GET /tags. All tenant-scoped; JSON responses; errors
-
Rate limit
- 100 req/min per tenant; 429 with Retry-After. Section 2.8. Implement in middleware or per-route (DB or in-memory counter with TTL).
-
OpenAPI and docs
- Generate or maintain OpenAPI spec; serve at /api/v1/openapi.json; optional docs UI at /api/v1/docs. Section 2.8.
Acceptance criteria
| Criterion | Status |
|---|---|
| API key created and stored hashed; full key shown once; prefix in list; revoke invalidates key. | Pending |
| All documented endpoints work with Bearer key; tenant scoped; 401/403/429 correct. | Pending |
| No outbound reply via API (POST message with type internal_note only); document in spec. | Pending |
| OpenAPI spec and docs available; public_api entitlement required. | Pending |
| Ready for M15 (Marketing site). | Pending |
Milestone 14 sign-off
| Criterion | Status |
|---|---|
| All tasks in 14.1–14.2 complete. | Pending |
| All acceptance criteria met. | Pending |
| Webhooks and Public API functional and gated by plan. | Pending |
| E2E: Webhook CRUD and API key UI (see INDEX — Testing strategy). | Pending |
| Ready for M15 (Marketing site). | Pending |