Skip to main content

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​

EventFires whenAnchor
automation.completedA Flow run reaches a terminal status - including failure.A run
automation.turn_completedAn Agent finishes one turn.A session message

An agent or flow emits the completion event matching its kind, so a subscription that omits events subscribes to that one. automation.failed is a third event a Flow webhook can select: it fires in addition to automation.completed when a run ends failed or timed_out, so a subscriber wanting only failures selects it explicitly. An Agent turn has no failure event - a turn that recorded an error still arrives as automation.turn_completed with status: "failed" - so selecting automation.failed on an Agent webhook is refused with 400 VALIDATION rather than registered as something that would never fire.

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.

By default, anything in a payload that looks like a password, key or token is hidden before the delivery is signed and sent. Treat additional fields as expected: parse leniently and ignore what you do not recognize.

Sending values unmasked​

Some workflows exist to hand a credential back - a flow that rotates a key and returns the new one to your system, for instance. Hiding it would quietly break that flow at your end. For those, turn on Send values unmasked when you add or edit the webhook: its deliveries then go out exactly as the run produced them. The setting applies to that one webhook; every other webhook stays masked.

Only use it for an address you control, because the values arrive as-is. Anyone who can manage the agent's or flow's webhooks can change it, every change is recorded in the audit log, and a read-only support session cannot turn it on.

Verify the signature​

Register a secret - 8 characters or more - and every delivery carries:

HeaderValue
X-Yekar-TimestampUnix seconds at send time.
X-Yekar-Signaturesha256= 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 Webhooks tab marks an unsigned webhook Unsigned, and its menu offers Add signing secret, which can generate a strong secret for you; give the same value to the receiving system.

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.