Webhooks
Get told when credits run low or a subscription changes instead of polling. Deliveries are signed, retried, and never slow down your API calls.
Register an endpoint
An endpoint must be a public https URL. A key can have up to 5. The signing secret is returned once, so store it now.
curl https://api.helixhumancapital.in/v1/webhooks \
-H "Authorization: Bearer $HELIX_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/helix", "events": ["credits.low", "credits.exhausted"]}'r = requests.post("https://api.helixhumancapital.in/v1/webhooks",
headers={"Authorization": f"Bearer {KEY}"},
json={"url": "https://example.com/hooks/helix", "events": ["credits.low", "credits.exhausted"]})
secret = r.json()["secret"] # shown onceconst r = await fetch("https://api.helixhumancapital.in/v1/webhooks", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com/hooks/helix", events: ["credits.low", "credits.exhausted"] }),
})
const { secret } = await r.json() // shown onceOmit events (or send ["*"]) to receive everything. Manage endpoints with GET /v1/webhooks and DELETE /v1/webhooks/{id}.
Events
| Type | When | data |
|---|---|---|
credits.low | Remaining credits fall to 20% of the period allowance. Once per period. | allowance, remaining, periodEnds |
credits.exhausted | Remaining credits reach zero. Once per period. | allowance, remaining, periodEnds |
subscription.renewed | A payment succeeded and a new period started. | plan, periodEnds, orderId |
subscription.expiring | The period is about to end. | periodEnds |
subscription.expired | The period and grace window have ended. | periodEnded |
webhook.test | You called the test endpoint. | a sample payload |
Delivery format
content-type: application/json
user-agent: HelixAPI-Webhooks/1
helix-event: credits.low
helix-delivery: 0f6e… (unique per event; use it to de-duplicate)
helix-signature: t=1800000000,v1=5257a869…
{
"id": "0f6e…",
"type": "credits.low",
"created": 1800000000,
"data": { "allowance": 3000, "remaining": 600, "periodEnds": "2026-12-09T17:15:28.318Z" }
}Reply with any 2xx within 8 seconds to acknowledge. Redirects are not followed.
Verify the signature
v1 is the hex HMAC-SHA256 of <t>.<raw request body> using your endpoint secret. Verify against the raw bytes of the body before parsing JSON, compare in constant time, and reject timestamps more than 5 minutes old to stop replays.
import crypto from "node:crypto"
import express from "express"
const app = express()
app.post("/hooks/helix", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("helix-signature") ?? ""
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
const expected = crypto.createHmac("sha256", process.env.HELIX_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString("utf8")}`).digest("hex")
const ok = age < 300 && parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
if (!ok) return res.status(400).end()
const event = JSON.parse(req.body)
// …handle event.type, de-duplicate on event.id…
res.status(200).end()
})import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/helix")
def helix():
raw = request.get_data() # raw bytes, before any JSON parsing
parts = dict(p.split("=", 1) for p in request.headers.get("helix-signature", "").split(","))
expected = hmac.new(os.environ["HELIX_WEBHOOK_SECRET"].encode(),
f"{parts.get('t')}.".encode() + raw, hashlib.sha256).hexdigest()
fresh = abs(time.time() - float(parts.get("t", 0))) < 300
if not (fresh and hmac.compare_digest(parts.get("v1", ""), expected)):
abort(400)
event = json.loads(raw)
return "", 200Retries and shut-off
- A non-2xx reply, timeout or network error is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours: seven attempts in total.
- Deliveries can arrive more than once and out of order. De-duplicate on
helix-delivery/id. - An endpoint that fails 30 times in a row is switched off. Re-register it once fixed.
- Destinations on private, loopback or link-local addresses, and plain
http, are refused.
Test and debug
POST /v1/webhooks/{id}/test sends a signed webhook.test immediately. GET /v1/webhooks/{id}/deliveries lists the last 25 deliveries with status, attempts and the last response code or error.