# Register a webhook trigger

POST

/v1/webhooks

```sh
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", "runAsProject": true, "toolsAllowedUnattended": [ "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 workspace 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 workspace’s default project. Needs `write` on the project and `execute` on the flow. `toolsAllowedUnattended` names the tools whose calls in its runs needn’t wait for a person’s yes, though a tool asks for one in a run no person started: naming it takes `admin` on the project, each id must be a tool the flow calls, and it’s audited (`trigger-tools-allowed-unattended-set`).

## Authorizations

- **[bearerAuth](https://docs.kindgi.com/v0.5.0/reference/api.md#bearerauth)**

## Parameters

### Header Parameters

**Idempotency-Key**

string

\>= 1 characters

Caller-supplied idempotency key, scoped to the caller: the same key from someone else in the workspace 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). A request whose key store can’t be read before it runs is answered `503 idempotency-store-unavailable` with `Retry-After`: nothing ran, so retry with the same key (from 0.3.0).

## Request Bodyrequired

application/json

object

**flowId**

required

string

\>= 1 characters

**flowVersion**

required

string

\>= 1 characters

**projectId**

The trigger’s project; absent: the workspace’s default project.

string format: uuid

**config**

object

**input**

**hmacSecretName**

required

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 workspace 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).

string

\>= 1 characters

**generateSecret**

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

boolean

**signature**

One of:

- **object**

  object

  **kind**

  required

  string

  Allowed values: hmac-sha256

  **encoding**

  required

  string

  Allowed values: hex base64

  **header**

  required

  The header that carries the signature (matched case-insensitively).

  string

  \>= 1 characters <= 100 characters

  **prefix**

  Stripped from the header’s value before decoding (e.g. `sha256=`).

  string

  \>= 1 characters <= 32 characters

- **object**

  object

  **kind**

  required

  string

  Allowed values: standard-webhooks

  **toleranceSeconds**

  integer

  \>= 1 <= 3600

**deliveryIdHeader**

The header whose value, with the body, dedupes deliveries. Not with `standard-webhooks`, which dedupes on its `webhook-id`.

string

\>= 1 characters <= 100 characters

**bodyLimitBytes**

integer

\>= 1024 <= 1048576

**rateLimitPerMinute**

integer

\>= 1 <= 6000

**label**

string

**runAsServiceAccountId**

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. With an authorization store, that’s checked when the account is named: an account that can’t is refused (`409 trigger-owner-cannot-run-target`). Not a project’s own account (409 `service-account-project-owned`: name it with `runAsProject`). Absent: the caller owns it.

string

\>= 1 characters

**runAsProject**

`true`: the trigger’s runs act as its project’s own service account, made the first time with the `editor` role on the project, so it’s shared. It runs only what the project’s editors can. With an authorization store, that’s checked when it’s named: if it can’t run the flow, the request is refused (`409 trigger-owner-cannot-run-target`). Takes `admin` on the trigger’s project (without an authorization store, a tenant-admin key), not `act_as`. Not with `runAsServiceAccountId`, and not without a project (400). A runtime that can’t make the account answers 501 `trigger-operation-unsupported`. `false` or absent: as `runAsServiceAccountId` says.

boolean

**toolsAllowedUnattended**

The tools whose calls in this trigger’s runs needn’t wait for a person’s yes, though a tool asks for one in a run no person started (`needsApprovalWhenUnattended`). Naming it, `[]` included, takes `admin` on the trigger’s project. Each tool by its id, with no wildcard or pattern, and only tools this trigger’s flow or agent calls. It lifts only that wait: the workspace’s approval rules, the agent’s own rules, the permission check (as the trigger’s owner) and the tool’s own limits all still apply.

Array\<string>

<= 100 items unique items

## Responses

### 201

Webhook registered.

application/json

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

**triggerId**

required

string

**webhookId**

required

The routable id in the receive URL. Route-minted (a random UUID); unique per workspace. Not a secret: the signature is what a request is checked by.

string

**receiveUrl**

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

string format: uri

**flowId**

required

string

\>= 1 characters

**flowVersion**

required

string

\>= 1 characters

**projectId**

required

The trigger’s project: its runs are this project’s.

string format: uuid

**owner**

required

Who its runs act as. A person: a personal trigger, which only they or a project admin change. A service account: a shared trigger. Only a take-over (`POST …/owner`) changes it. Checked again at every delivery.

object

**kind**

required

string

Allowed values: user service

**id**

required

string

**displayName**

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.

string

**isProjectAccount**

Set when the owner is the trigger’s project’s own service account (`runAsProject`): the trigger is shared, and runs as the project. Show it as “This project”. Absent otherwise.

boolean

**input**

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

**hmacSecretName**

required

The signing secret, by name (written with `POST /v1/secrets`, or made by `generateSecret`). Read in the env the deployment serves, at `hmacSecretScope`.

string

\>= 1 characters

**hmacSecretScope**

One of:

- **object**

  One of:

  - **object**

    object

    **kind**

    required

    string

    Allowed value: tenant

  - **object**

    object

    **kind**

    required

    string

    Allowed value: org

    **orgId**

    required

    string format: uuid

  - **object**

    object

    **kind**

    required

    string

    Allowed value: project

    **projectId**

    required

    string format: uuid

- **null**

  null

**signingSecret**

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.

string

**signature**

required

One of:

- **object**

  object

  **kind**

  required

  string

  Allowed values: hmac-sha256

  **encoding**

  required

  string

  Allowed values: hex base64

  **header**

  required

  The header that carries the signature (matched case-insensitively).

  string

  \>= 1 characters <= 100 characters

  **prefix**

  Stripped from the header’s value before decoding (e.g. `sha256=`).

  string

  \>= 1 characters <= 32 characters

- **object**

  object

  **kind**

  required

  string

  Allowed values: standard-webhooks

  **toleranceSeconds**

  integer

  \>= 1 <= 3600

**deliveryIdHeader**

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`).

string

**bodyLimitBytes**

required

The largest body it takes (default 262144).

integer

\>= 1024 <= 1048576

**rateLimitPerMinute**

required

Accepted deliveries a minute (default 600); past it, `429`.

integer

\>= 1 <= 6000

**label**

required

string | null

**status**

required

Lifecycle status. Only `active` triggers fire. Tombstoned rows are excluded from every read path.

string

Allowed values: active paused

**statusReason**

Why the runtime paused it (repeated refused or failed starts; a sender’s failed proofs never count), when it did.

string

**version**

The trigger’s newest version (`GET …/versions`): one more for each change to what it runs, when, as whom or with what. Absent until its first version.

integer

\>= 1

**suppressedRefusals**

Refusals and skipped deliveries this minute past the 20 recorded in its history, which only counted.

object

**since**

required

string format: date-time

**count**

required

integer

\>= 1

**toolsAllowedUnattended**

The tools whose calls in this trigger’s runs don’t wait for a person’s yes, though a tool asks for one in a run no person started (`needsApprovalWhenUnattended`). Absent: none. Each tool by its id, with no wildcard or pattern, and only tools this trigger’s flow or agent calls. It lifts only that wait: the workspace’s approval rules, the agent’s own rules, the permission check (as the trigger’s owner) and the tool’s own limits all still apply.

Array\<string>

<= 100 items unique items

**lastFiredAt**

required

string | null format: date-time

**createdAt**

required

string format: date-time

**updatedAt**

required

string format: date-time

##### Example

```json
{
  "owner": {
    "kind": "user",
    "isProjectAccount": true
  },
  "hmacSecretScope": {
    "kind": "tenant"
  },
  "signature": {
    "kind": "hmac-sha256",
    "encoding": "hex"
  },
  "status": "active"
}
```

### 400

A body that fails validation (`bad-input`), `runAsProject` with `runAsServiceAccountId` or for a trigger with no project (`bad-input`), or `provider-key-refused`: `hmacSecretName` is a model provider’s key, or `reserved-name-prefix`: it’s under the reserved `kindgi.` prefix. Or `reference-out-of-scope`: the flow isn’t registered in the trigger’s project, its org or the workspace, 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).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 401

Missing / malformed / expired / revoked bearer token.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 409

`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 `runAsServiceAccountId` names an unregistered account (`service-account-unregistered`), a project’s own account (`service-account-project-owned`: name it with `runAsProject`), or one that can’t run the flow (`trigger-owner-cannot-run-target`). Or, with `runAsProject`, the project’s own account can’t run the flow either (`trigger-owner-cannot-run-target`; the account stays, for the next `runAsProject`), or an active account someone made already has the project account’s name, `project-<projectId>` (`service-account-name-taken`: a tenant admin can unregister it). Or the route-minted webhookId collided (astronomically rare).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 413

The request body is over the API’s limit (`request-too-large`; `details.maxBytes` says how much).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 500

Server error (unmapped domain code or framework crash).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 501

`trigger-operation-unsupported`: `runAsProject` on a runtime that can’t make a project’s own account.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 503

The `Idempotency-Key` store couldn’t be read before the request ran (`idempotency-store-unavailable`), so nothing was changed; `Retry-After` (whole seconds) says when to try again with the same key.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

## See also

- [`GET /v1/webhooks`: List webhook triggers](https://docs.kindgi.com/v0.5.0/reference/api/operations/webhookslist.md)
- [Error code `bad-input`](https://docs.kindgi.com/v0.5.0/reference/errors.md#bad-input)
- [Error code `idempotency-store-unavailable`](https://docs.kindgi.com/v0.5.0/reference/errors.md#idempotency-store-unavailable)
- [Error code `provider-key-refused`](https://docs.kindgi.com/v0.5.0/reference/errors.md#provider-key-refused)
- [Error code `reference-out-of-scope`](https://docs.kindgi.com/v0.5.0/reference/errors.md#reference-out-of-scope)
- [Error code `request-too-large`](https://docs.kindgi.com/v0.5.0/reference/errors.md#request-too-large)
- [Error code `reserved-name-prefix`](https://docs.kindgi.com/v0.5.0/reference/errors.md#reserved-name-prefix)
- [Error code `secret-name-invalid`](https://docs.kindgi.com/v0.5.0/reference/errors.md#secret-name-invalid)
- [Error code `secret-write-conflict`](https://docs.kindgi.com/v0.5.0/reference/errors.md#secret-write-conflict)
- [Error code `service-account-name-taken`](https://docs.kindgi.com/v0.5.0/reference/errors.md#service-account-name-taken)
- [Error code `service-account-project-owned`](https://docs.kindgi.com/v0.5.0/reference/errors.md#service-account-project-owned)
- [Error code `service-account-unregistered`](https://docs.kindgi.com/v0.5.0/reference/errors.md#service-account-unregistered)
- [Error code `trigger-operation-unsupported`](https://docs.kindgi.com/v0.5.0/reference/errors.md#trigger-operation-unsupported)
- [Error code `trigger-owner-cannot-run-target`](https://docs.kindgi.com/v0.5.0/reference/errors.md#trigger-owner-cannot-run-target)
- [Every error code](https://docs.kindgi.com/v0.5.0/reference/errors.md)
