Developer Manual
Webhooks
Register a webhook and eClips POSTs a signed JSON payload whenever a subscribed event fires. Every delivery is signed with HMAC-SHA256 over the raw body, so you can verify it came from eClips before trusting it.
Register a webhook
Register with POST /v1/webhooks (scope webhooks:write) or the SDK's createWebhook(url, events). The url must be https:// and events must be non-empty. The signing secret is returned once.
curl -X POST https://api.eclips.tech/v1/webhooks \
-H "Authorization: Bearer $ECLIPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/eclips",
"events": ["run.completed", "run.failed", "run.triage_required"]
}'Events
Three events are emitted. A run created with a per-run webhook_url also delivers to that URL.
| run.completed | The agent run finished successfully. data.output holds the result. |
| run.failed | The run failed. data.error holds the failure message. |
| run.triage_required | The run paused for human review. data.triage_item_id references the pending triage item. |
Payload
Each delivery is a POST with this body shape:
{
"event": "run.completed",
"run_id": "a1b2c3d4-0000-4000-8000-000000000000",
"agent_type": "procurement_specialist",
"organization_id": "org_…",
"timestamp": "2026-06-06T09:00:42.000Z",
"data": {
"output": { "recommended": "Dell" },
"confidence_score": 0.94,
"requires_triage": false,
"triage_item_id": null,
"error": null
}
}Alongside the body, every delivery carries these headers, and the sender enforces a 10-second timeout:
X-eClips-Signature: sha256=<hex HMAC of the raw body>
X-eClips-Event: run.completed
X-eClips-Timestamp: 2026-06-06T09:00:42.000Z
Content-Type: application/jsonSignature verification
The X-eClips-Signature header is sha256=<hex> — an HMAC-SHA256 of the raw request body keyed with your webhook secret. Always compute the HMAC over the exact bytes received (before any JSON parsing) and compare in constant time.
import crypto from "crypto";
// eClips signs every delivery with HMAC-SHA256 over the RAW request body,
// keyed with the secret returned when you created the webhook.
function verify(rawBody: string, signatureHeader: string, secret: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const provided = signatureHeader.replace(/^sha256=/, "");
// Constant-time compare to avoid timing leaks.
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Worked example
Example · An Express receiver
Capture the raw body, verify the signature, then branch on the event. Respond 200 within the 10-second window so the delivery is recorded as successful.
import express from "express";
const app = express();
// Capture the RAW body — signature verification needs the exact bytes.
app.post("/webhooks/eclips", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const signature = req.header("X-eClips-Signature") ?? "";
if (!verify(raw, signature, process.env.ECLIPS_WEBHOOK_SECRET!)) {
return res.status(401).send("bad signature");
}
const payload = JSON.parse(raw);
switch (payload.event) {
case "run.completed": /* handle result */ break;
case "run.failed": /* handle failure */ break;
case "run.triage_required": /* route to a human */ break;
}
res.status(200).send("ok"); // respond within 10s
});A failed delivery (non-2xx or timeout) is logged as failed with its HTTP status — the agents:read-readable organization dashboard surfaces recent attempts.