Skip to Content

Webhooks

Available

  • Subscriptions (/v1/webhooks)
  • Signed deliveries
  • Retries with a queryable delivery log
  • Redelivery
  • Ping and secret rotation
  • 7-day auto-disable

Planned

  • source.status.changed and adapter.disabled events

The Engine pushes signed JSON events to URLs you register. Manage subscriptions with a tenant key holding webhooks:manage.

Events

EventWhendata
run.completedA run finished (succeeded or failed){runId, flowId, status, trigger, startedAt, finishedAt, anyTriggered, targets: [{sourceId, name, isTriggered, probability, color}], error}
flow.triggeredA target crossed into its triggered state and passed the alert gate{runId, flowId, flowName, sourceId, targetName, hint, triggerType, probability, message}
flow.resetA target left its triggered state and passed the reset gatesame shape
alert.sentOne alert delivery to one contact on one channel{deliveryId, runId, flowId, contactId, channel, triggerType, status: sent | failed | skipped_unverified, targetNames, error}
report.generatedA report was frozen and its link emailed (or skipped / errored){reportId, eventId, flowId, runId, flowName, scheduleId, mode, link, expiresAt, status, reason, recipients}
user.verified / contact.verifiedAn email or phone passed the Engine’s verification{subjectId, channel}
sources.syncedA targets adapter sync reconciled your source catalog{adapterId, total, created, updated, disabled, reenabled}
pingPOST /v1/webhooks/{id}/ping{subscriptionId, url}

GET /v1/webhooks/events returns this catalog. Subscribe to specific names or to "*".

Delivery

POST https://your.endpoint/growth-engine Content-Type: application/json User-Agent: GrowthEngine-Webhooks/1 X-GrowthEngine-Signature: t=1756231234,v1=6e9c1f… X-GrowthEngine-Event: flow.triggered X-GrowthEngine-Delivery: 9d2a8846-… {"data":{"flowId":"…","flowName":"Frost","hint":"area","message":"Frost triggered for North 40", "probability":null,"runId":"…","sourceId":"…","targetName":"North 40","triggerType":"trigger"}, "event":"flow.triggered","id":"evt_5447d790…","occurredAt":"2026-08-26T18:29:34.446+00:00", "tenantId":"…"}

Verify: expected = HMAC_SHA256(secret, t + "." + rawBody) (hex), constant-time compare with v1, reject if |now − t| > 300 s. Sign over the raw bytes you received. Any 2xx within 10 seconds is a success; anything else (or a timeout) is retried after ≈1 m, 5 m, 30 m, 2 h and 8 h, then the delivery is dead and stays in the log. Ordering is not guaranteed and redeliveries reuse the envelope id, so dedupe on id.

A subscription that has failed continuously for seven days is disabled automatically (status: disabled, disabledReason); PATCH {"status": "active"} re-enables it. A minimal receiver in Python, standard library only:

import hashlib, hmac, json, os, time from http.server import BaseHTTPRequestHandler, HTTPServer SECRET = os.environ["WEBHOOK_SECRET"].encode() # the whsec_… value shown once at registration def verify(header: str, body: bytes) -> bool: parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p) ts, v1 = parts.get("t", ""), parts.get("v1", "") if not ts.isdigit() or abs(time.time() - int(ts)) > 300: return False expected = hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) class Handler(BaseHTTPRequestHandler): def do_POST(self): body = self.rfile.read(int(self.headers.get("Content-Length", "0"))) if not verify(self.headers.get("X-GrowthEngine-Signature", ""), body): self.send_response(401); self.end_headers(); return event = json.loads(body) # {"id", "event", "tenantId", "occurredAt", "data"} print(event["event"], event["id"]) # dedupe on event["id"]; do real work after replying self.send_response(204); self.end_headers() HTTPServer(("0.0.0.0", 9009), Handler).serve_forever()

Endpoints (Available)

All tenant gate · webhooks:manage.

MethodPathWhat
GET/v1/webhooks/eventsEvent catalog
POST/v1/webhooks{url, events, description?, secret?} → subscription + secret (shown once). url must be https:// and publicly routable (no loopback / private hosts, no credentials); secret optional, ≥ 16 chars, else a whsec_… is generated. 409 beyond 20 active
GET/v1/webhooks[/{id}]List (cursor) / read; never includes the secret
PATCH/v1/webhooks/{id}url, events, description, status: active | disabled
DELETE/v1/webhooks/{id}Remove (its delivery log goes with it)
POST/v1/webhooks/{id}/rotate-secretNew secret, shown once; signs every attempt from now on
POST/v1/webhooks/{id}/ping202: queue a ping event
GET/v1/webhooks/{id}/deliveries?status=Delivery log: pending | succeeded | failed | dead, cursor paginated
GET/v1/webhooks/{id}/deliveries/{deliveryId}The frozen envelope, every attempt ({at, statusCode?, error?, durationMs}), last response excerpt
POST/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver202: new attempt chain, same envelope id