# 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" }'
```

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

- **[bearerAuth](https://docs.kindgi.com/v0.1.10/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 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

application/json

object

**flowId**

required

string

\>= 1 characters

**flowVersion**

required

string

\>= 1 characters

**projectId**

The trigger’s project; absent: the tenant’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 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).

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. Absent: the caller owns it.

string

\>= 1 characters

## 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 tenant. 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: whoever registered it, until an admin takes it over (`POST …/owner`). 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

**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

**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

**lastFiredAt**

required

string | null format: date-time

**createdAt**

required

string format: date-time

**updatedAt**

required

string format: date-time

##### Example

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

### 400

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

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 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"
  }
}
```

## See also

- [`GET /v1/webhooks`: List webhook triggers](https://docs.kindgi.com/v0.1.10/reference/api/operations/webhookslist.md)
- [Error code `bad-input`](https://docs.kindgi.com/v0.1.10/reference/errors.md#bad-input)
- [Error code `provider-key-refused`](https://docs.kindgi.com/v0.1.10/reference/errors.md#provider-key-refused)
- [Error code `reference-out-of-scope`](https://docs.kindgi.com/v0.1.10/reference/errors.md#reference-out-of-scope)
- [Error code `request-too-large`](https://docs.kindgi.com/v0.1.10/reference/errors.md#request-too-large)
- [Error code `reserved-name-prefix`](https://docs.kindgi.com/v0.1.10/reference/errors.md#reserved-name-prefix)
- [Error code `secret-name-invalid`](https://docs.kindgi.com/v0.1.10/reference/errors.md#secret-name-invalid)
- [Error code `secret-write-conflict`](https://docs.kindgi.com/v0.1.10/reference/errors.md#secret-write-conflict)
- [Every error code](https://docs.kindgi.com/v0.1.10/reference/errors.md)
