Outbound Webhooks (v1)
| Field | Value |
|---|---|
| Status | Deferred. Not included in any current tier. |
| Owner | Unassigned |
| Target repos | attunelogic-api, attunelogic-service |
| Related | Monetization & Tier Packaging (v1), Public API (v1), Feature Roadmap Index |
| Last updated | 2026-04-23 |
Status banner: Outbound webhooks were removed from the public Growth tier. The platform accepts inbound webhooks today (Stripe) via
src/controllers/webhooks/*, but no outbound delivery pipeline exists.
Why this was removed from v1
- Nothing about outbound delivery is built yet: no endpoint CRUD, no event catalog, no HMAC signing, no delivery queue, no retries, no dead-letter inspection UI.
- "Webhooks" on the previous Growth card implied a production-grade delivery promise we couldn't keep on launch day. Shipping it half-done would have created a support hole (missed deliveries, no retry visibility) that's worse than not shipping at all.
What shipping this requires
Data model
- New
WebhookEndpointmodel scoped byparentCompany:url: stringdescription: stringevents: string[](subscribed event names; validated against the catalog)secret: string(HMAC signing secret; shown once on create, hashed at rest)active: booleanfailureCount: numberdisabledAt: DatecreatedBy,createdAt,updatedAt
- New
WebhookDeliverymodel scoped byparentCompany:endpointId,event,payload(snapshot),status(pending / delivered / failed / dead),attempts: number,lastAttemptAt,nextAttemptAt,responseCode,responseBody(truncated),durationMs.
Event catalog
Initial ship-with-v1 catalog (all scoped by parentCompany):
job.created,job.updated,job.status_changed,job.cancelledleg.status_changedinvoice.created,invoice.finalized,invoice.paiddriver.created,driver.deactivatedlead.created
Each payload wraps the resource with { id, version, event, createdAt, tenant: { id }, data: {...} } so subscribers can idempotency-key on id.
API surface
GET /api/v1/webhooks/endpointsPOST /api/v1/webhooks/endpointsPATCH /api/v1/webhooks/endpoints/:idDELETE /api/v1/webhooks/endpoints/:idGET /api/v1/webhooks/deliveries?endpointId=…POST /api/v1/webhooks/deliveries/:id/retry- Feature gate: existing
requireFeaturemiddleware + tenant-config flagwebhooks.outbound.enabled.
Delivery pipeline
- Event producers publish to an internal event bus (simple: emit → enqueue a job per active matching endpoint).
- Worker fires HTTP POST with:
Content-Type: application/jsonX-AttuneLogic-Signature: t=<unix>,v1=<hmac_sha256>X-AttuneLogic-Event: <event>X-AttuneLogic-Delivery: <deliveryId>
- Retry schedule: 1min, 5min, 30min, 2h, 6h, 12h, 24h (exponential). After 7 failures the endpoint is auto-disabled and the tenant admin is emailed; manual re-enable required.
- Per-endpoint circuit breaker: 10 consecutive failures in 1h trips the breaker for 1h.
Service web UI
Settings → Developers → Webhooks:- Endpoint CRUD (URL, description, subscribed events matrix).
- Signing-secret "reveal once then rotate" UX.
- Delivery log with status, response code, response body preview, and manual retry.
- Event-catalog reference docs surfaced inline.
Security
- Tenant isolation: every read/write hard-scoped by
parentCompany. An endpoint in one tenant can never see deliveries from another. - HMAC signatures are per-endpoint (no shared global secret).
- Outbound SSRF hardening: block RFC1918, loopback, metadata service IPs, and a tenant-configurable allowlist host policy.
- Rate limit: cap per-tenant deliveries at a reasonable ceiling (e.g. 10k/day) to prevent runaway-fanout from killing us or the subscriber.
Tests
- Unit: signature generation + verification round-trip. Retry backoff schedule. Auto-disable threshold.
- Integration: happy-path deliver, 500 → retry, 410 → auto-disable (tenant explicitly rejected), 429 → respect
Retry-After, private-IP target refused. - Tenant isolation: a job event in tenant A never fans out to endpoints owned by tenant B.
Docs
- Customer-facing "Webhooks setup" guide.
- Developer event-reference page (auto-generated from the catalog).
- Runbook: rotating a leaked signing secret.
Dependencies on existing infra
- Feature flag registry —
webhooks.outbound.enabledis already reserved as a future flag in the monetization proposal. - Job queue — existing agenda/bull queue handles retry scheduling.
- AuditLog — already landed; endpoint CRUD + secret rotation are natural audit events.
- Public API (v1) — this proposal and Public API share the developer-settings IA in the service web UI.
Estimated timeline and risk
- Effort: 3 weeks end-to-end for one engineer (backend + UI + docs). Retry + dead-letter is where edge cases live.
- Risk: Medium-high. Delivery reliability is a customer-trust feature; shipping it without alerting + a manual retry UI is not an option.
Open questions
- Do we need per-event filters (e.g. "only job.status_changed where status = delivered") in v1, or is that v2?
- Do we support JSON Schema for payload validation so customers can lint against our shapes?
- Do we publish an "events API" (GET a paginated log of past events) as a supplement to webhooks, for customers who prefer polling?
Cross-references
- Existing inbound webhooks (not outbound):
attunelogic-api/src/controllers/webhooks - Public API spec (co-owned surface): Public API (v1)
- Billing proposal: Monetization & Tier Packaging (v1)
- Roadmap index: Feature Roadmap Index