Register a webhook trigger
const url = 'https://example.com/v1/webhooks';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"flowId":"example","flowVersion":"example","projectId":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","config":{"input":"example"},"hmacSecretName":"example","generateSecret":true,"signature":{"kind":"hmac-sha256","encoding":"hex","header":"example","prefix":"example"},"deliveryIdHeader":"example","bodyLimitBytes":1,"rateLimitPerMinute":1,"label":"example","runAsServiceAccountId":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/v1/webhooks \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "flowId": "example", "flowVersion": "example", "projectId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "config": { "input": "example" }, "hmacSecretName": "example", "generateSecret": true, "signature": { "kind": "hmac-sha256", "encoding": "hex", "header": "example", "prefix": "example" }, "deliveryIdHeader": "example", "bodyLimitBytes": 1, "rateLimitPerMinute": 1, "label": "example", "runAsServiceAccountId": "example" }'A signed request to the trigger’s receiveUrl (POST /v1/hooks/{tenantId}/{webhookId}) starts a run of its flow, as its owner: the caller, until an admin takes it over. The route mints webhookId (a random UUID). The signing secret is written first (POST /v1/secrets), and hmacSecretName names it; a model provider’s key is refused (400 provider-key-refused). Where it’s read is recorded on the trigger (hmacSecretScope): the first of its project, the project’s org and the tenant that has it and that the caller administers, else the broadest they administer; the receiver reads it there only. Or generateSecret: true: the server makes it, stores it as hmacSecretName in the trigger’s own project (a name the project already has is refused, 409 secret-write-conflict), and returns it once as signingSecret. signature says how the sender signs (default: a hex HMAC-SHA256 of the raw body in X-Kindgi-Signature); deliveryIdHeader names the header whose value, with the body, dedupes deliveries. A trigger that names no projectId goes in the tenant’s Default project. Needs write on the project and execute on the flow.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Caller-supplied idempotency key, scoped to the caller: the same key from someone else in the tenant is their own request (from 0.1.5). Retries with the same key return the original response byte-identical (per docs/API-ROUTE-CONVENTIONS.md §3.1). A retry sent while the first request still runs is answered 409 idempotency-key-in-flight with Retry-After, on a runtime whose store holds keys (from 0.1.5); retry after it, and you get the first request’s answer. An answer that carries a secret (a new API key or public run token, a session token, a generated signing secret) isn’t kept: a retry gets 409 idempotency-key-replay-withheld, with the first request’s status and when it succeeded (from 0.1.5).
Request Bodyrequired
Section titled “Request Bodyrequired”object
The trigger’s project; absent: the tenant’s Default project.
object
The signing secret, by name: written first with POST /v1/secrets, and read at the first of the trigger’s project, its org and the tenant that has it and that you administer (hmacSecretScope); or the name generateSecret stores a new one under. Never a model provider’s key. For WooCommerce, use letters and digits only (it HTML-decodes the secret before signing). Under kindgi dev, generateSecret writes it to an env file, so the name is capital letters, digits and underscores, not starting with a digit (secret-name-invalid otherwise).
Make a new signing secret, store it as hmacSecretName in the trigger’s own project (in the env the deployment serves), and return it once as signingSecret. Needs only write on the project. A name the project already has is refused (secret-write-conflict, 409), never overwritten. Letters and digits, or whsec_… for standard-webhooks.
The header whose value, with the body, dedupes deliveries. Not with standard-webhooks, which dedupes on its webhook-id.
A service account the trigger’s runs act as (its owner), named by id, instead of the caller. Needs act_as on that account — a tenant admin holds it — and the account must itself hold execute on the flow. Absent: the caller owns it.
Responses
Section titled “Responses”Webhook registered.
A webhook trigger: a signed request to its receiveUrl starts a run of its flow, as its owner. The signing secret is never on it, only its name.
object
The routable id in the receive URL. Route-minted (a random UUID); unique per tenant. Not a secret: the signature is what a request is checked by.
Where the sender posts: {public URL}/v1/hooks/{tenantId}/{webhookId}. Absent when the deployment has no public URL configured (KINDGI_PUBLIC_URL); it’s never taken from a request’s Host.
The trigger’s project: its runs are this project’s.
Who its runs act as: whoever registered it, until an admin takes it over (POST …/owner). Checked again at every delivery.
object
The owner’s name at the time of the response: the person’s display name, or the service account’s name. Absent when it can’t be read (no directory, a removed account) and from a runtime before Kindgi 0.1.6: show the id then.
The flow’s input on every delivery, in place of the event. Absent: the event (the body, parsed as JSON for a JSON content type, else its text).
The signing secret, by name (written with POST /v1/secrets, or made by generateSecret). Read in the env the deployment serves, at hmacSecretScope.
Only in the answer to a register with generateSecret: the new signing secret, shown this once, to paste into the sender. It’s stored as hmacSecretName in the trigger’s own scope; no later read shows it.
The header whose value, with the body, is a delivery’s dedupe key (WooCommerce: X-WC-Webhook-Delivery-ID, which is per second, not per event, so the body counts too). Absent: deliveries aren’t deduped (standard-webhooks dedupes on its own webhook-id).
The largest body it takes (default 262144).
Accepted deliveries a minute (default 600); past it, 429.
Lifecycle status. Only active triggers fire. Tombstoned rows are excluded from every read path.
Why the runtime paused it (repeated refused or failed starts; a sender’s failed proofs never count), when it did.
Refusals and skipped deliveries this minute past the 20 recorded in its history, which only counted.
object
Example
{ "owner": { "kind": "user" }, "hmacSecretScope": { "kind": "tenant" }, "signature": { "kind": "hmac-sha256", "encoding": "hex" }, "status": "active"}A body that fails validation (bad-input), or provider-key-refused: hmacSecretName is a model provider’s key. Or reference-out-of-scope: the flow isn’t registered in the trigger’s project, its org or the tenant, or flowVersion isn’t a version that project runs (details.issues, at /flowId, whose message names the version), and nothing is registered. Or, with generateSecret, a name the secrets store can’t write, refused before anything is stored: secret-name-invalid (a name that isn’t 1 to 255 ASCII letters, digits, ., - and _ starting with a letter, digit or _, with any . between two non-. characters, or, under kindgi dev, isn’t capital letters, digits and underscores, starts with a digit, or is KINDGI_*; the message says the rule) or reserved-name-prefix.
object
object
Stable machine-readable discriminant. Values match domain error codes (see docs/API-ROUTE-CONVENTIONS.md §4.3).
Optional, kind-specific.
object
Server-assigned request id; also echoed via X-Request-Id header.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": {}, "requestId": "example" }}Missing / malformed / expired / revoked bearer token.
object
object
Stable machine-readable discriminant. Values match domain error codes (see docs/API-ROUTE-CONVENTIONS.md §4.3).
Optional, kind-specific.
object
Server-assigned request id; also echoed via X-Request-Id header.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": {}, "requestId": "example" }}secret-write-conflict: with generateSecret, the trigger’s project already has a secret of that name (it’s never overwritten, and no trigger is left). Or the route-minted webhookId collided (astronomically rare).
object
object
Stable machine-readable discriminant. Values match domain error codes (see docs/API-ROUTE-CONVENTIONS.md §4.3).
Optional, kind-specific.
object
Server-assigned request id; also echoed via X-Request-Id header.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": {}, "requestId": "example" }}The request body is over the API’s limit (request-too-large; details.maxBytes says how much).
object
object
Stable machine-readable discriminant. Values match domain error codes (see docs/API-ROUTE-CONVENTIONS.md §4.3).
Optional, kind-specific.
object
Server-assigned request id; also echoed via X-Request-Id header.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": {}, "requestId": "example" }}Server error (unmapped domain code or framework crash).
object
object
Stable machine-readable discriminant. Values match domain error codes (see docs/API-ROUTE-CONVENTIONS.md §4.3).
Optional, kind-specific.
object
Server-assigned request id; also echoed via X-Request-Id header.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": {}, "requestId": "example" }}