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.

bash
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"]
  }'
Important:The signing secret is shown only at creation. If you lose it, delete the webhook and create a new one — there is no endpoint to retrieve it again.

Events

Three events are emitted. A run created with a per-run webhook_url also delivers to that URL.

run.completedThe agent run finished successfully. data.output holds the result.
run.failedThe run failed. data.error holds the failure message.
run.triage_requiredThe run paused for human review. data.triage_item_id references the pending triage item.

Payload

Each delivery is a POST with this body shape:

json
{
  "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:

bash
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/json

Signature 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.

typescript
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.

typescript
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.