Outgoing webhooks
Subscribe to events from your tenant — calibration runs completing, drift detections, billing state changes. HMAC-SHA256 signed deliveries, exponential backoff retry, dead-letter queue for unrecoverable failures.
Register a webhook
Mint webhook endpoints from the tenant portal at console.goable.io/portal/webhooks. Click "New endpoint", paste the destination URL, pick the events you want to receive, and submit.
The response surfaces a signing secret shown once. Copy it immediately into your destination's environment — the portal won't show it again. Use it to verify every incoming payload (see Signature verification below). If you lose it, revoke the endpoint and create a new one.
Event types
outcome.createdA tenant reported an outcome via POST /v1/score/:id/outcome (or POST /v1/outcomes). Payload: { tenantId, sessionId?, outcomeType, activitySlug?, detail }drift.firedThe drift monitor opened a new event for a cell (severity = watch / warning / critical). Payload: { tenantId, eventId, activity, subSpotSlug, horizonH, severity, cusumValue, threshold, baselineBss, currentBss, nDaysInDecline, recalibrationTriggered }drift.resolvedAn open drift event returned to baseline and was auto-resolved. Payload: { tenantId, eventId, activity, subSpotSlug, horizonH, severity, firedAt, resolvedAt, currentBss, baselineBss }recommendation.completedPOST /v1/recommend-spot returned. Payload: { tenantId, activity, regionCenter, radiusKm, window, topK, totalCandidates, rankedCandidates, allGated, results[<=5], personalizationApplied, latencyMs }billing.subscription_updatedStripe subscription state changed (plan upgrade/downgrade, status active/suspended, payment recovery). Payload: { tenantId, stripeCustomerId, stripeSubscriptionId, stripeEventType, plan?, status? }calibration.completedReserved — the Python calibration pipeline writes runs directly to the database; outbound notification activates once the engine accumulates the cohort threshold (≥150 paired outcomes per cell).Delivery
Every delivery POSTs JSON to your registered URL with these headers:
Content-Type: application/json User-Agent: goable-webhook/1.0 X-Goable-Event: drift.fired X-Goable-Delivery: 8e3b2a… X-Goable-Signature: 9f4c2a… (raw hex, HMAC-SHA256)
Your endpoint must respond with a 2xx within 5 seconds. Anything else is recorded as a failure (today: one shot, no retry queue — a worker process will land that later; for now, treat 5xx as terminal). 200 with empty body is fine; we don't parse the response.
Signature verification
Verify every payload. Without signature checks a malicious third party could POST forged events to your endpoint. The signature is HMAC-SHA256 over the raw request body, hex-encoded.
Reference implementation in Node.js:
import { createHmac, timingSafeEqual } from "node:crypto" function verify(rawBody: string, header: string, secret: string) { const expected = createHmac("sha256", secret) .update(rawBody) .digest("hex") const a = Buffer.from(expected) const b = Buffer.from(header) return a.length === b.length && timingSafeEqual(a, b) } if (!verify(rawBody, req.headers["x-goable-signature"] as string, process.env.SIGNING_SECRET!) ) { return res.status(401).send("bad sig") }
For other languages: HMAC-SHA256 the raw request body with the signing secret; hex-encode and compare against the X-Goable-Signature header using constant-time comparison. The deduplication id is on the top-level id of the JSON body — store it to drop replays.
Retries
Today: best-effort, one shot. The deliverer fires once per endpoint with a 5-second timeout; non-2xx or timeout is recorded as a failure on the endpoint's delivery counters but the event is not retried automatically. A durable retry queue + dead-letter visibility ships with the worker-process rollout (tracked in the followups doc). For now, treat your endpoint like a payments webhook — keep it fast, idempotent, and respond 2xx even on internal failure if you've queued the work locally.
Delivery state per endpoint: last_delivery_at, last_delivery_status, last_failure_at, consecutive_failures. After 100 consecutive failures the endpoint is auto-suspended; reactivate from the portal.
Revoking
Revoke a webhook from the portal at /portal/webhooks — click the endpoint, then "Revoke". Revocation is immediate: in-flight deliveries finish, no new ones queue. The signing secret is invalidated; if you re-register the same URL you get a brand-new secret.