# Webhooks

Signed outbound events (crawl finished, export ready, recommendations ready, signal detected). Managed by team owners and admins.

## List webhooks

`GET /v1/webhooks`

The team's webhook subscriptions. Secrets are never returned here.


- Required scopes: `webhooks:read`
- Rate-limit cost: 1 unit(s)

### Responses

- `200`: OK.
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl "https://api.nessflow.com/v1/webhooks" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN"
```

### Response example

```json
{
    "data": [
        {
            "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
            "url": "https://exemple.fr/",
            "description": "string",
            "events": [
                "crawl.completed"
            ],
            "active": true,
            "disabled_reason": "consecutive_failures",
            "disabled_at": "2026-10-01T09:30:00Z",
            "consecutive_failures": 42,
            "last_delivery_at": "2026-10-01T09:30:00Z",
            "secret_rotation_ends_at": "2026-10-01T09:30:00Z",
            "created_at": "2026-10-01T09:30:00Z"
        }
    ]
}
```

## Create a webhook

`POST /v1/webhooks`

Subscribes an HTTPS URL to events. The response carries the signing `secret`, shown
this one time only. The URL is refused when it does not resolve to a public address
(`webhook.url_refused`). Counts against `api.webhooks.max`.


- Required scopes: `webhooks:write`
- Rate-limit cost: 1 unit(s)

### Request body

- `url` (string, required): An HTTPS URL that resolves to a public address.
- `events` (string[], required) [crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected]
- `description` (string | null, optional)

### Responses

- `201`: OK.
- `400`: Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `422`: Invalid query or body values (`validation_failed`), with `errors` by field.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl -X POST "https://api.nessflow.com/v1/webhooks" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://exemple.fr/","events":["crawl.completed"],"description":"string"}'
```

### Response example

```json
{
    "data": {
        "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "url": "https://exemple.fr/",
        "description": "string",
        "events": [
            "crawl.completed"
        ],
        "active": true,
        "disabled_reason": "consecutive_failures",
        "disabled_at": "2026-10-01T09:30:00Z",
        "consecutive_failures": 42,
        "last_delivery_at": "2026-10-01T09:30:00Z",
        "secret_rotation_ends_at": "2026-10-01T09:30:00Z",
        "created_at": "2026-10-01T09:30:00Z",
        "secret": "string"
    }
}
```

## Read a webhook

`GET /v1/webhooks/{webhook}`

One subscription, its state and its failure counter.


- Required scopes: `webhooks:read`
- Rate-limit cost: 1 unit(s)

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.

### Responses

- `200`: OK.
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN"
```

### Response example

```json
{
    "data": {
        "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "url": "https://exemple.fr/",
        "description": "string",
        "events": [
            "crawl.completed"
        ],
        "active": true,
        "disabled_reason": "consecutive_failures",
        "disabled_at": "2026-10-01T09:30:00Z",
        "consecutive_failures": 42,
        "last_delivery_at": "2026-10-01T09:30:00Z",
        "secret_rotation_ends_at": "2026-10-01T09:30:00Z",
        "created_at": "2026-10-01T09:30:00Z"
    }
}
```

## Update a webhook

`PATCH /v1/webhooks/{webhook}`

Changes the URL, the events or the description, or enables and disables the
subscription. Enabling it again clears an automatic disablement and its failure
counter.


- Required scopes: `webhooks:write`
- Rate-limit cost: 1 unit(s)

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.

### Request body

- `url` (string, optional)
- `events` (string[], optional) [crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected]
- `description` (string | null, optional)
- `active` (boolean, optional)

### Responses

- `200`: OK.
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `422`: Invalid query or body values (`validation_failed`), with `errors` by field.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl -X PATCH "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://exemple.fr/","events":["crawl.completed"],"description":"string","active":true}'
```

### Response example

```json
{
    "data": {
        "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "url": "https://exemple.fr/",
        "description": "string",
        "events": [
            "crawl.completed"
        ],
        "active": true,
        "disabled_reason": "consecutive_failures",
        "disabled_at": "2026-10-01T09:30:00Z",
        "consecutive_failures": 42,
        "last_delivery_at": "2026-10-01T09:30:00Z",
        "secret_rotation_ends_at": "2026-10-01T09:30:00Z",
        "created_at": "2026-10-01T09:30:00Z"
    }
}
```

## Delete a webhook

`DELETE /v1/webhooks/{webhook}`

Deletes the subscription and its delivery log. Never refused by billing.


- Required scopes: `webhooks:write`
- Rate-limit cost: 1 unit(s)

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.

### Responses

- `204`: Deleted.
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl -X DELETE "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN"
```

## Rotate the signing secret

`POST /v1/webhooks/{webhook}/rotate-secret`

Issues a new signing secret, returned this one time. For 24 hours, deliveries carry
one `v1` signature per secret, so the receiver can switch without dropping events.


- Required scopes: `webhooks:write`
- Rate-limit cost: 1 unit(s)

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.

### Responses

- `200`: OK.
- `400`: Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl -X POST "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/rotate-secret" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN"
```

### Response example

```json
{
    "data": {
        "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "url": "https://exemple.fr/",
        "description": "string",
        "events": [
            "crawl.completed"
        ],
        "active": true,
        "disabled_reason": "consecutive_failures",
        "disabled_at": "2026-10-01T09:30:00Z",
        "consecutive_failures": 42,
        "last_delivery_at": "2026-10-01T09:30:00Z",
        "secret_rotation_ends_at": "2026-10-01T09:30:00Z",
        "created_at": "2026-10-01T09:30:00Z",
        "secret": "string"
    }
}
```

## Send a test event

`POST /v1/webhooks/{webhook}/test`

Queues a signed `webhook.test` event (`202`), even to a disabled subscription, so a fix
can be checked before enabling it again. It is never retried and never counts as a
failure. Read the result in the delivery log.


- Required scopes: `webhooks:write`
- Rate-limit cost: 1 unit(s)
- Idempotency-Key accepted

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.

### Responses

- `202`: OK.
- `400`: Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl -X POST "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/test" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

### Response example

```json
{
    "data": {
        "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "event_id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
        "event_type": "string",
        "status": "pending",
        "attempts": 42,
        "response_status": 200,
        "error_reason": "refused_address",
        "duration_ms": 42,
        "next_attempt_at": "2026-10-01T09:30:00Z",
        "delivered_at": "2026-10-01T09:30:00Z",
        "created_at": "2026-10-01T09:30:00Z"
    }
}
```

## List deliveries

`GET /v1/webhooks/{webhook}/deliveries`

The delivery log, most recent first, kept 30 days. Only the status, the duration and a
stable reason are kept, never the receiver's response body.


- Required scopes: `webhooks:read`
- Rate-limit cost: 1 unit(s)

### Parameters

- `webhook` (path, string (uuid), required): The webhook identifier.
- `limit` (query, integer, optional): Items per page, 1 to 100 (default 25).
- `cursor` (query, string, optional): Opaque cursor from `meta.next_cursor`. It is bound to the request that issued it; reusing it with other filters returns `400 pagination.invalid_cursor`.

### Responses

- `200`: OK.
- `401`: Missing, invalid or expired token, or a member who left the team.
- `403`: The plan does not include the API (`api.not_in_plan`) or this verb
(`api.write_not_in_plan`), or the token lacks a scope
(`token.missing_scope`).

- `404`: No such resource for the token's team. Resources of another team answer 404 as well, never 403.
- `422`: Invalid query or body values (`validation_failed`), with `errors` by field.
- `429`: The plan rate limit is exhausted for one window.

### Request example (cURL)

```bash
curl "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/deliveries" \
  -H "Authorization: Bearer $NESSFLOW_TOKEN"
```

### Response example

```json
{
    "data": [
        {
            "id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
            "event_id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
            "event_type": "string",
            "status": "pending",
            "attempts": 42,
            "response_status": 200,
            "error_reason": "refused_address",
            "duration_ms": 42,
            "next_attempt_at": "2026-10-01T09:30:00Z",
            "delivered_at": "2026-10-01T09:30:00Z",
            "created_at": "2026-10-01T09:30:00Z"
        }
    ],
    "meta": {
        "limit": 100,
        "has_more": true,
        "next_cursor": "string"
    }
}
```
