Webhooks
A webhook is how Yekar.AI tells you that accepted work finished. Entry endpoints return 202 before execution, so a webhook - not the fire response - is the normal way to learn the outcome. The session stream exists to reconcile when a delivery did not arrive.
Webhooks are registered in the product, not through this API: an agent's or flow's subscriptions live on its own Webhooks tab. Registration takes a name, a publicly reachable URL, an optional signing secret, and the events you want.
Events
| Event | Fires when | Anchor |
|---|---|---|
automation.completed | A Flow run reaches a terminal status - including failure. | A run |
automation.turn_completed | An Agent finishes one turn. | A session message |
An agent or flow only ever emits the event matching its kind, so a subscription that omits events subscribes to the event matching that agent's or flow's own kind.
What does not fire
- Draft test drives and Test runs. Their tools execute for real, but announcing unpublished instructions to a subscriber that cannot tell it was a test is not something the platform does.
- Eval replays.
Delivery
Yekar.AI sends a POST with a JSON body and Content-Type: application/json.
{
"event": "automation.completed",
"automation": { "id": "9f21ab04-…", "name": "Create return" },
"run": {
"id": "6ea94e74-…",
"status": "succeeded",
"output": { "returnId": "ret_1042" },
"error": null
},
"firedAt": "2026-08-01T09:31:04.221Z"
}
{
"event": "automation.turn_completed",
"automation": { "id": "84cfee8e-…", "name": "Ask returns agent" },
"session": {
"id": "0b1f3c9a-…",
"messageId": "30c7d984-…",
"status": "succeeded",
"output": "The return was created.",
"error": null
},
"firedAt": "2026-08-01T09:31:04.221Z"
}
status on a turn is derived from the message: a turn that recorded an error is failed, anything else is succeeded. A turn's output is the assistant's text, parsed to JSON when the reply is JSON - so an Agent with an output contract gives you an object, not a JSON string pretending to be one.
output is capped at 64 KB. Past that it is replaced by { "$truncated": true, "note": …, "prefix": … } carrying the first 1 KB. Read the run or session for the full value.
Payloads are scrubbed for credential material before signing. Treat additional fields as expected: parse leniently and ignore what you do not recognize.
Verify the signature
Register a secret - 8 characters or more - and every delivery carries:
| Header | Value |
|---|---|
X-Yekar-Timestamp | Unix seconds at send time. |
X-Yekar-Signature | sha256= followed by the hex HMAC-SHA256 of <timestamp>.<body> |
Sign the exact bytes you received, not a re-serialized copy - key order and whitespace are part of the input.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const timestamp = headers["x-yekar-timestamp"];
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const received = (headers["x-yekar-signature"] ?? "").replace(/^sha256=/, "");
if (received.length !== expected.length) return false;
// Reject stale timestamps too, so a captured delivery cannot be replayed later.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
return timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
A webhook registered without a secret is unsigned. It still carries X-Yekar-Timestamp, but you have no way to establish that the request came from Yekar.AI - register a secret for anything that acts on the payload.
The secret is write-only. After you save it, the product shows only its last four characters; there is no read-back. Replace it by saving a new one.
Retries
Return a 2xx promptly. Anything else - including a timeout after 30 seconds - is a failed attempt.
A failed delivery is retried up to 5 times with exponential backoff starting around 30 seconds, spreading attempts over roughly an hour. Each attempt is recorded as its own row on the webhook's Deliveries page with its HTTP status, duration, and error. You can also redeliver a delivery by hand from there.
Deliveries are at-least-once, so make your endpoint idempotent. A delivery that already succeeded is never re-sent automatically, but a crash between your response and our recording of it can produce a duplicate. Use the anchor id - run.id, or session.messageId - as the deduplication key.
A delivery is marked skipped, not retried, when its webhook has been disabled or deleted since it was queued, or when the run or message it describes no longer exists.
URL requirements
The URL must be http or https and must resolve entirely to public addresses. A hostname with any private address among its answers is rejected - a partially-private round robin is still a way in. This is re-checked at send time, not only at registration, so a host that later points somewhere private stops being delivered to.