# Management API

Create sources and gateways, read events, replay deliveries and stream live traffic over HTTP.

Source: https://docs.webhooker.eu/reference/api/

Everything the dashboard does is available over HTTP. The base URL is
`https://app.webhooker.eu`, all paths are under `/api/v1`, and every request and
response is JSON.

An OpenAPI description of the authenticated surface is served at
`/api/openapi.json`.

## Authentication

Send an [API key](/account/api-keys/) as a bearer token:

```bash
curl https://app.webhooker.eu/api/v1/sources/ \
  -H "Authorization: Bearer $WEBHOOKER_API_KEY"
```

A key is bound to one workspace, so no workspace parameter is needed. Keys act
with member permissions: they cannot manage members, roles, plans or other keys.

Requests are metered per workspace per minute by plan. Over budget returns `429`
with `Retry-After`.

## Conventions

- **Ids** are UUIDs.
- **Timestamps** are RFC 3339 in UTC.
- **Lists** return `{"items": [...]}`, and paginated lists add `"total"`.
  Paginated endpoints take `page` (1-based) and `limit` (default 50, max 200).
- **PATCH** is a partial update: an omitted field is left unchanged. Where
  clearing is meaningful, an explicit `null` clears the field.
- **Secrets** come back masked as `***`. Submitting the mask is rejected — send
  the real value to change it, or omit the field to keep it.
- **Errors** use `{"error": {"code": "...", "message": "..."}}`. See
  [error codes](/reference/errors/).

## Identity

### `GET /api/v1/me`

The authenticated principal and its active workspace. The cheapest way to check
a key.

```json
{
  "auth": "api_key",
  "workspace": { "id": "…", "name": "Acme", "plan": "pro" }
}
```

### `GET /api/v1/plans`

The public plan catalogue with prices, limits and features. No authentication
required.

### `GET /api/v1/status`

Service health. No authentication required.

## Sources

### `POST /api/v1/sources/`

```json
{ "name": "stripe-prod", "color": "#5E8BFF" }
```

`name` is required; `color` is optional and assigned from the palette when
omitted. Returns `201`:

```json
{
  "id": "0190a0b1-c2d3-7e4f-8a9b-0c1d2e3f4a5b",
  "name": "stripe-prod",
  "description": null,
  "color": "#5E8BFF",
  "token": "6b225n04u5kmyg",
  "ingest_path": "/in/6b225n04u5kmyg",
  "verification_config": { "provider": "none" },
  "response_config": null,
  "status": "active",
  "created_at": "2026-09-19T10:31:00Z"
}
```

The ingest URL is the API host plus `ingest_path`.

### `GET /api/v1/sources/`

Paginated. `q` filters by name. Each item adds `event_count`.

### `GET /api/v1/sources/{id}`

One source.

### `PATCH /api/v1/sources/{id}`

Any of `name`, `description`, `color`, `status` (`active` or `paused`),
`verification_config`, `response_config`.

```json
{
  "verification_config": { "provider": "stripe", "secret": "whsec_…" }
}
```

Verification variants:

| `provider` | Fields |
| --- | --- |
| `none` | — |
| `stripe`, `github`, `shopify` | `secret` |
| `generic_hmac` | `secret`, `header`, `algorithm` (`sha256`/`sha1`), `encoding` (`hex`/`base64`), optional `timestamp_header`, optional `tolerance_secs` (default 300) |
| `basic_auth` | `username`, `password` |
| `api_key` | `header`, `key` |

`response_config` takes `{ "status": 200, "content_type": "json" \| "text", "body": "…" }`,
where the body may contain `{{request_id}}`. Send `null` to restore the default
response. Status must be 100–599.

### `DELETE /api/v1/sources/{id}`

Moves the source to the trash. `204`.

### `GET /api/v1/sources/trash` · `POST /api/v1/sources/{id}/restore`

List trashed sources (paginated, `q` supported) and restore one. Trashed sources
are purged after 7 days. Restoring re-checks your plan's source cap.

## Destinations

### `POST /api/v1/destinations/`

```json
{
  "name": "billing-api",
  "url": "https://api.acme.dev/hooks/webhooker",
  "timeout_ms": 10000,
  "auth_config": { "type": "hmac", "secret": "a-long-random-string" },
  "custom_headers": { "X-Environment": "production" },
  "retry_policy": { "intervals_seconds": [30, 120, 600, 3600, 14400] }
}
```

Only `name` and `url` are required. Returns `201` with the destination,
`auth_config` masked, plus `status`, `circuit_state` and `circuit_reopen_at`.

| Field | Rules |
| --- | --- |
| `url` | Absolute `http`/`https`, public host. Private and internal addresses are rejected. |
| `timeout_ms` | 1–300000. Default 10000. |
| `auth_config` | `{"type":"none"}`, `{"type":"hmac","secret":…}`, `{"type":"api_key","header":…,"key":…}`, `{"type":"basic_auth","username":…,"password":…}` |
| `custom_headers` | Flat string map, sent verbatim. |
| `retry_policy` | 1–10 intervals, each 1–86400 seconds. Default `[30,120,600,3600,14400]`. |

### `GET /api/v1/destinations/` · `GET /api/v1/destinations/{id}`

List or read.

### `PATCH /api/v1/destinations/{id}`

Any of `name`, `url`, `timeout_ms`, `auth_config`, `custom_headers`,
`retry_policy` (`null` restores the default), `status` (`active` or `paused`).
A present `auth_config` replaces the whole config — send `{"type":"none"}` to
turn signing off.

### `DELETE /api/v1/destinations/{id}`

`204`. Its connections stop receiving events.

## Connections

A connection is the source-to-destination half of a [gateway](/guides/create-gateway/).

### `POST /api/v1/connections/`

```json
{
  "source_id": "…",
  "destination_id": "…",
  "filter_rules": {
    "operator": "and",
    "rules": [{ "path": "body.type", "op": "eq", "value": "invoice.paid" }]
  },
  "transformation": {
    "set_headers": [{ "name": "X-Trace-Id", "value": "{{event.id}}" }],
    "rename_headers": [{ "from": "Content-Type", "to": "X-Original-Content-Type" }],
    "remove_headers": ["x-environment"],
    "merge_body_fields": { "received_via": "webhooker" }
  }
}
```

Both ids must belong to your workspace. A source and a destination can only be
connected once — a duplicate returns `409`. Filter and transformation are
optional; syntax is in
[filters and transformations](/deliver/filters-and-transformations/).

### `GET /api/v1/connections/` · `GET /api/v1/connections/{id}`

List or read.

### `GET /api/v1/sources/{id}/connections`

The connections of one source, paginated, each with a `destination` summary
including its `circuit_state`. `q` filters by destination name.

### `PATCH /api/v1/connections/{id}`

`enabled`, `filter_rules`, `transformation`. An explicit `null` clears a filter
or a transformation.

### `DELETE /api/v1/connections/{id}`

`204`. The destination itself is kept.

## Events

### `GET /api/v1/events/`

Paginated, newest first.

| Parameter | Meaning |
| --- | --- |
| `source_id` | One source. |
| `verification_status` | `verified`, `failed` or `skipped`. |
| `received_after`, `received_before` | RFC 3339 bounds. |
| `q` | Substring match on the event's public id. |
| `page`, `limit` | Pagination. |

Each item carries `method`, `content_type`, `verification_status`, `body_size`,
`received_at` and the delivery counters `delivery_count`, `delivered_count`,
`failed_count`, `pending_count`.

### `GET /api/v1/events/{id}`

The full event: `headers`, `body` (plus `body_base64` when the payload is not
valid UTF-8), `body_size`, `received_at`, `expires_at`, and `deliveries` — each
with its status, `attempt_count`, `next_attempt_at` and an `attempts` array of
`attempt_number`, `request_url`, `response_status`, `response_body`,
`error_message`, `latency_ms`, `attempted_at`.

## Replay

### `POST /api/v1/events/{id}/resend`

```json
{ "connection_ids": ["…"] }
```

Omit the body to resend to every connection of the event's source, disabled ones
included. Returns `202` with `{"created": n}`.

### `POST /api/v1/deliveries/resend-bulk`

```json
{
  "connection_id": "…",
  "statuses": ["exhausted", "failed"],
  "since": "2026-09-18T00:00:00Z",
  "until": "2026-09-19T00:00:00Z"
}
```

`statuses` defaults to `["exhausted"]`; `since` and `until` are optional.
Returns `202` with `{"created": n}`.

## Dead letters

### `GET /api/v1/sources/{id}/dlq`

Paginated `exhausted` and `failed` deliveries with `destination_name`,
`attempt_count`, `last_error`, `last_response_status` and timestamps.

| Parameter | Meaning |
| --- | --- |
| `status` | Comma-separated terminal statuses. Defaults to `exhausted,failed`. |
| `received_after`, `received_before` | RFC 3339 bounds on the event's arrival time. |
| `page`, `limit` | Pagination. |

### `GET /api/v1/sources/{id}/dlq/summary`

Per-connection counts of `exhausted` and `failed`.

## Live streams

Both endpoints are Server-Sent Events, capped per workspace by plan.

### `GET /api/v1/sources/{id}/tail`

Metadata per event: ids, method, `received_at`, `content_type`, `body_size`,
`verification_status`. Cheap enough to leave open on a dashboard.

### `GET /api/v1/sources/{id}/stream`

The full payload per event, including `headers` and `body` — what the CLI uses
to replay webhooks against localhost.

```bash
curl -N https://app.webhooker.eu/api/v1/sources/$SOURCE_ID/stream \
  -H "Authorization: Bearer $WEBHOOKER_API_KEY"
```

## Alert channels

### `POST /api/v1/alert-configs/`

```json
{ "channel": "email", "config": { "to": "ops@acme.dev" }, "source_id": null }
```

`channel` is `email` (config requires `to`) or `telegram` (requires `token` and
`chat_id`). `source_id` scopes the channel to one source; omit or `null` for
workspace wide. Returns `201`.

### `GET /api/v1/alert-configs/` · `PATCH /api/v1/alert-configs/{id}` · `DELETE /api/v1/alert-configs/{id}`

List, update (`config`, `enabled`) and delete.

## Stats

- `GET /api/v1/stats/overview` — dashboard metrics for a window. Optional
  `source_ids` (comma-separated), `received_after`, `received_before`. Histogram
  buckets are hourly up to 48 hours, daily up to 90 days, monthly beyond.
- `GET /api/v1/stats/volume-by-source` — event volume per source.
- `GET /api/v1/stats/by-source` — per-source delivery breakdown.

## API keys and workspaces

These endpoints exist, but they are governed by role rather than by the resource
rules above, and the ones that change anything are closed to API keys.

**Readable with a key:**

- `GET /api/v1/api-keys/` — the workspace's keys by name, prefix, creation date
  and last use. No secret material is ever returned.
- `GET /api/v1/workspaces` — the workspaces the key's owner belongs to.
- `GET /{id}/members` · `GET /{id}/invites` — admin or owner, evaluated against
  the key owner's role in that workspace.

**Session only, and closed to keys:**

- `POST /api/v1/api-keys/`, `DELETE /api/v1/api-keys/{id}` — admin or owner. A
  key cannot mint or revoke keys, including its own.
- `PATCH|DELETE /api/v1/workspaces/{id}`, `POST /{id}/transfer`, `/{id}/switch`,
  `/{id}/leave`, `PATCH|DELETE /{id}/members/{user_id}`, `POST /{id}/invites`,
  `DELETE /{id}/invites/{invite_id}`, `POST /api/v1/invites/accept`.

Renaming a workspace needs admin; changing its plan, transferring it or deleting
it needs owner. See [workspaces and team](/account/workspaces/) for the full
matrix.
