> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fimo.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Send project events to external services and inspect every delivery from the Fimo CLI.

Webhooks send project events to an external HTTPS endpoint. Use the CLI to create configurations, inspect the live event catalog, test deliveries, and verify signed requests.

<Note>
  Webhook commands require **Full Access** to the project. Edit, Comment, and View access cannot read or manage
  webhooks.
</Note>

## See available events

```bash theme={null}
fimo webhooks events
fimo webhooks events form.submitted
```

The command returns the current catalog. This checkout defines these events:

```text theme={null}
form.submitted
publish.started
publish.succeeded
publish.failed
publish.unpublished
agent.run.started
agent.run.completed
agent.run.failed
agent.run.cancelled
agent.review.requested
agent.review.resolved
analytics.report_ready
media.uploaded
media.updated
media.deleted
entry.changed
domain.created
domain.deleted
label.changed
locale.created
locale.default_changed
locale.deleted
environment.created
environment.deleted
environment.merged
branch.restored
```

The list prints the variables available to custom payload templates. Pass one event name to see its description,
variables, and example payload as JSON.

## Create a webhook

Store destination credentials as project secrets, then reference the secret from a header. This example sends selected form data to an HTTPS endpoint:

```bash theme={null}
printf '%s' "$DESTINATION_TOKEN" | fimo secrets add DESTINATION_TOKEN --stdin

fimo webhooks create waitlist-sync \
  --event form.submitted \
  --form waitlist \
  --url https://example.com/hooks/waitlist \
  --header "Authorization=bearer:DESTINATION_TOKEN" \
  --body '{"email":"{{ event.data.submission.data.email }}"}'
```

`fimo secrets add` refuses to replace an existing value. Use `fimo secrets update` when you intend to rotate it.

Header values use one of two explicit forms:

* `Name=secret:KEY` resolves a project secret as the complete header value.
* `Name=bearer:KEY` resolves a project secret and prefixes it with `Bearer `.
* `Name=token:KEY` resolves a project secret and prefixes it with `token `.
* `Name=literal:value` sends a fixed value.

Omit `--body` to send the complete event envelope. A custom body is JSON and can place `{{ event... }}` variables in any value. When a variable is the entire value, objects and arrays keep their JSON type.

For larger templates, read JSON from a file or standard input:

```bash theme={null}
fimo webhooks update <webhook-id> --body @webhook-payload.json
printf '%s' '{"email":"{{ event.data.submission.data.email }}"}' | fimo webhooks update <webhook-id> --body -
```

## React to content changes

`entry.changed` fires after content is accepted in the Environment selected by the webhook. New webhooks keep the legacy `main` behavior by default. Use `--environments all` for every Branch, `--environments production`, `--environments preview`, or a custom Environment ID. Preview includes any unclaimed Branch, including `main` when no custom Environment claims it.

Every complete event payload includes scalar `environment` and `branch` fields. The legacy `env.name` object remains available and contains the same Branch name. Publish events always report `environment: "production"` and `branch: "main"`.

```bash theme={null}
fimo webhooks create search-index \
  --event entry.changed \
  --environments all \
  --content-type article \
  --url https://example.com/hooks/search \
  --body '{"environment":"{{ event.environment }}","branch":"{{ event.branch }}","reason":"{{ event.data.reason }}","changes":"{{ event.data.changes }}"}'
```

Filter with `--action <created|updated|deleted>`, `--content-type <name>`, or `--locale <locale>`. A matching change must satisfy every filter.

Each committed write sends one delivery whose `data.changes` array lists every affected entry, including Branch merges, which arrive with `reason: "environment_merge"`. Large operations are split into numbered batches with a shared `operationId`. Retries always resend the same delivery instead of creating a new one, so your endpoint can deduplicate on the event `id`.

## Manage configurations

```bash theme={null}
fimo webhooks list
fimo webhooks get <webhook-id>

fimo webhooks update <webhook-id> --enabled off
fimo webhooks update <webhook-id> --url https://example.com/hooks/fimo
fimo webhooks update <webhook-id> --default-body

fimo webhooks delete <webhook-id>
```

Webhooks belong to the project, with up to 20 configurations per project. The same configuration is visible from every Environment. The selector is always one scalar value. `main` means the Main Branch only and `all` means every Branch, preserving existing webhook behavior. You can instead select Production, Preview, or one custom Environment. Existing `main`, `all`, and omitted filters keep their stored shape unchanged.

## Test and inspect delivery

```bash theme={null}
fimo webhooks test <webhook-id> --wait
fimo webhooks deliveries <webhook-id> --limit 20
fimo webhooks deliveries <webhook-id> --cursor <next-cursor>
fimo webhooks deliveries <webhook-id> --all-pages
fimo webhooks delivery <webhook-id> <delivery-id>
fimo webhooks retry <webhook-id> <delivery-id>
```

Tests use documented example data unless you pass `--data '{...}'`. Delivery details include the event ID, masked destination, configured header names, request payload excerpt, attempts, HTTP response status, duration, a limited response excerpt, and an error code. `retry` is available after a delivery exhausts its automatic retries.

## Verify signed requests

Signing is on by default. Fimo returns the signing secret once when you create a webhook and sends these headers with every request:

* `X-Fimo-Delivery`: stable delivery ID
* `X-Fimo-Event`: event type
* `X-Fimo-Event-Id`: stable event ID for deduplication across retries and destinations
* `X-Fimo-Test`: `true` for synthetic test deliveries
* `X-Fimo-Timestamp`: timestamp for this attempt
* `X-Fimo-Signature`: HMAC signature for the timestamp and raw request body

Rotate a signing secret if the existing value is exposed:

```bash theme={null}
fimo webhooks rotate-secret <webhook-id>
```

Save the new value immediately. Fimo does not reveal it again.

Verify the signature against the raw request body before parsing JSON. Reject stale timestamps to prevent a captured request from being replayed.

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyFimoWebhook(input: {
  rawBody: string;
  signature: string;
  timestamp: string;
  signingSecret: string;
}): boolean {
  const timestamp = Number(input.timestamp);
  const ageSeconds = Math.abs(Date.now() / 1000 - timestamp);
  if (!Number.isFinite(timestamp) || ageSeconds > 300) {
    return false;
  }

  const digest = createHmac('sha256', input.signingSecret).update(`${input.timestamp}.${input.rawBody}`).digest('hex');
  const expected = Buffer.from(`v1=${digest}`);
  const received = Buffer.from(input.signature);

  return expected.length === received.length && timingSafeEqual(expected, received);
}
```

After verification, use `X-Fimo-Event-Id` as an idempotency key so retries do not process the same event twice.

<Columns cols={2}>
  <Card title="Studio webhooks" icon="webhook" href="/docs/studio/webhooks">
    Create and monitor a webhook visually.
  </Card>

  <Card title="Automate your project" icon="bolt" href="/docs/automate/overview">
    Compare webhooks with agents, schedules, and chat.
  </Card>
</Columns>
