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.changedandadapter.disabledevents
The Engine pushes signed JSON events to URLs you register. Manage subscriptions with a tenant
key holding webhooks:manage.
Events
| Event | When | data |
|---|---|---|
run.completed | A run finished (succeeded or failed) | {runId, flowId, status, trigger, startedAt, finishedAt, anyTriggered, targets: [{sourceId, name, isTriggered, probability, color}], error} |
flow.triggered | A target crossed into its triggered state and passed the alert gate | {runId, flowId, flowName, sourceId, targetName, hint, triggerType, probability, message} |
flow.reset | A target left its triggered state and passed the reset gate | same shape |
alert.sent | One alert delivery to one contact on one channel | {deliveryId, runId, flowId, contactId, channel, triggerType, status: sent | failed | skipped_unverified, targetNames, error} |
report.generated | A 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.verified | An email or phone passed the Engine’s verification | {subjectId, channel} |
sources.synced | A targets adapter sync reconciled your source catalog | {adapterId, total, created, updated, disabled, reenabled} |
ping | POST /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.
| Method | Path | What |
|---|---|---|
GET | /v1/webhooks/events | Event 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-secret | New secret, shown once; signs every attempt from now on |
POST | /v1/webhooks/{id}/ping | 202: 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}/redeliver | 202: new attempt chain, same envelope id |