Get a webhook when a run finishes
Kindgi signs each request with a secret that your receiver knows too. The secret lives with your other secrets, and the endpoint refers to it by name: Kindgi never stores it with the endpoint.
1. Store a signing secret
Section titled “1. Store a signing secret”A signing secret is whsec_ and the base64 of at least 24 random bytes.
Generate one:
import { generateWebhookSecret } from '@kindgi/sdk/webhooks';
console.log(generateWebhookSecret());from kindgi import webhooks
print(webhooks.generate_secret())Store it as a secret, under a name (the CLI asks for the value and doesn't echo it):
kindgi secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant Set ACME_WEBHOOK_SECRET at tenant in local.In development, the local environment is the pack's env files: this writes
.env.local, and a line in .env works too. In a deployment, store it in
that deployment's secrets store, under its environment's name (see
kindgi secrets). Your receiver needs the
same value, from its own configuration.
2. Register the endpoint
Section titled “2. Register the endpoint”There's no CLI command for webhook endpoints; register one from your app (or any HTTP client) with the client:
const endpoint = await kindgi.webhookEndpoints.create({ url: 'http://localhost:8787/hooks/kindgi', events: ['run.finished'], secretRef: { envName: 'local', name: 'ACME_WEBHOOK_SECRET' }, filter: { flowIds: ['acme.review-order'] }, // optional});console.log(endpoint.endpointId, endpoint.url);5c1d2267-4dc8-4bd6-81e8-940e620405ea http://localhost:8787/hooks/kindgiendpoint = kindgi.webhook_endpoints.create( url="http://localhost:8788/hooks/kindgi", events=["run.finished"], secret_ref={"envName": "local", "name": "ACME_WEBHOOK_SECRET"}, filter={"flowIds": ["acme.review-order"]}, # optional)print(endpoint.endpoint_id, endpoint.url)07fec45a-f938-4694-8ac3-f72f38a938a4 http://localhost:8788/hooks/kindgi-
urlis your receiver. Use https in production; plain http is for development. Inkindgi dev,localhostreaches your machine, not the runtime's container. -
events:run.finished. -
secretRefnames the secret: the environment it's in (localin development) and its name. Kindgi checks that it exists and is strong enough, and keeps only the name:{"error":{"code":"webhook-secret-not-found","message":"No secret \"NOPE_SECRET\" in \"local\": store it first, then register the endpoint",…}} -
filternarrows which runs reach the endpoint:flowIds(any version of these flows),projectId, andincludeDryRuns(dry runs are left out unless it'strue). Without a filter, every run you start does.
Only runs your app or the CLI started send run.finished; the runs inside
them (an agent step's turn) don't.
3. What your endpoint receives
Section titled “3. What your endpoint receives”When a run of acme.review-order ends, your URL gets a POST:
POST /hooks/kindgicontent-type: application/jsonuser-agent: Kindgi-Webhooks/1webhook-id: d7bc2805-587c-4830-a36f-fddeb0b7a423webhook-timestamp: 1791058584webhook-signature: v1,bj8Mj6s7ln2Gd1sDRQi8+Wax2SOFHovsdMIwvNhDrgw={ "id": "d7bc2805-587c-4830-a36f-fddeb0b7a423", "type": "run.finished", "createdAt": "2026-10-03T20:16:24.554Z", "data": { "run": { "id": "c24869c1-9243-4531-920b-d657e05eac0b", "projectId": "bc654965-c4fb-4667-85f1-33e46f4a4f0c", "flowId": "acme.check-order-stock", "flowVersion": "0.1.0", "status": "completed", "dryRun": false, "failureMessage": null, "createdAt": "2026-10-03T20:16:24.465Z", "completedAt": "2026-10-03T20:16:24.554Z" } }}statusiscompleted,failedorcancelled;failureMessagesays why when it failed.- The event carries the run's identity and outcome, never its input or
output. Fetch the run (
runs.get) for its output. webhook-idis the event's id, the same on every retry.
Delivery
Section titled “Delivery”Answer with a 2xx status. Any other answer, or none, is retried with backoff
(a minute later, then five minutes, then longer). A delivery is at least
once: the same event can arrive twice, so deduplicate on webhook-id.
Verify a webhook has receivers that check the
signature and do that.