ALL SYSTEMS · NOMINAL
UTC --:--:--
Docs·Concepts·Outgoing webhooks

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.