# Webhooks API

> Create, list, get, update, delete, rotate the secret of, test, and redeliver a project's outgoing webhooks over the integrator API.

Outgoing webhooks belong to a project. The API creates and manages them, sends test events, and lists and redelivers each webhook's deliveries. For the events and payloads a receiver gets, read [Outgoing webhooks for developers](https://docs.probara.net/en/developers/webhooks/). For the behavior in the app, read the [Webhooks guide](https://docs.probara.net/en/guides/webhooks/).

## Access and limits

- Reads need the `integrations.manage` permission (held by `owner`/`admin`).
- Writes need the same permission and a plan that includes webhooks, **Pro** or **Enterprise**. On other plans, writes return `403 forbidden`.
- Writes are limited to 120 per hour for each organization. Above the limit, the API returns `429` with `too_many_requests`.
- `{projectId}` is the project **code**, such as `CHK`, the same as on the other project routes.
- Write requests accept the optional `Idempotency-Key` header. See [Idempotency-Key](https://docs.probara.net/en/developers/api/idempotency/).
- A project has at most 20 webhooks, so the list is not paged.

## Endpoints

| Method   | Path                                                                                      | Success                                  |
| -------- | ----------------------------------------------------------------------------------------- | ---------------------------------------- |
| `GET`    | `/api/v1/projects/{projectId}/webhooks`                                                   | `200` list of webhooks                   |
| `POST`   | `/api/v1/projects/{projectId}/webhooks`                                                   | `201` webhook and its secret             |
| `GET`    | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}`                                     | `200` webhook                            |
| `PATCH`  | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}`                                     | `200` webhook                            |
| `DELETE` | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}`                                     | `204`                                    |
| `POST`   | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}/rotate-secret`                       | `200` new secret                         |
| `POST`   | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}/test`                                | `201` test delivery                      |
| `GET`    | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries`                          | `200` page of deliveries                 |
| `GET`    | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries/{deliveryUlid}`           | `200` delivery with request and response |
| `POST`   | `/api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries/{deliveryUlid}/redeliver` | `202` new delivery                       |

## List

`GET /api/v1/projects/{projectId}/webhooks`

Returns every webhook in the project as `{ items }`. Each item follows the [webhook fields](#webhook-fields).

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks"
```

## Create

`POST /api/v1/projects/{projectId}/webhooks`

### Request body

| Field       | Required | Notes                                                                                                                                                                                                                               |
| ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | yes      | 1 to 120 characters                                                                                                                                                                                                                 |
| `url`       | yes      | A public `https://` URL, with no credentials, IP address or local hostname, up to 2,000 characters. Redirects are not followed.                                                                                                     |
| `events`    | see note | Unique event names from the catalog. Required unless `allEvents` is `true`.                                                                                                                                                         |
| `allEvents` | no       | `true` subscribes the webhook to every event, including events added later. Defaults to `false`.                                                                                                                                    |
| `enabled`   | no       | Defaults to `true`.                                                                                                                                                                                                                 |
| `method`    | no       | `POST` (default), `PUT` or `PATCH`                                                                                                                                                                                                  |
| `headers`   | no       | Up to 20 objects with `name` and `value`. Each name appears once, regardless of case. Names Probara sets itself, and names starting with `cf-`, are refused. A value is up to 4,096 characters, with no line breaks.                |
| `payload`   | no       | `{"format":"standard"}` (default), or `{"format":"custom","contentType":"...","template":"..."}`. `contentType` defaults to `application/json`. A `template` is 1 to 16,384 characters, and a JSON template must render valid JSON. |
| `secret`    | no       | A `whsec_` secret that encodes 24 to 64 bytes in base64. Omit it to have Probara generate one.                                                                                                                                      |

### Response

`201` with the [webhook fields](#webhook-fields) and a `secret` field. The secret is returned only here and by [rotate](#rotate-the-secret). Store it then.

An invalid body returns `422 validation_failed`, for example no events without `allEvents`, a URL the endpoint rules refuse, an invalid template, or a project that already has 20 webhooks.

### Example

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Release alerts","url":"https://hooks.example.com/probara","allEvents":true}' \
  "https://app.probara.net/api/v1/projects/CHK/webhooks"
```

## Get

`GET /api/v1/projects/{projectId}/webhooks/{webhookUlid}`

Returns one webhook. An unknown `webhookUlid` returns `404 not_found`.

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J"
```

## Update

`PATCH /api/v1/projects/{projectId}/webhooks/{webhookUlid}`

Partial update. At least one field is required. To change the secret, use [rotate](#rotate-the-secret).

### Request body

| Field       | Required | Notes                                                                                                                                                                   |
| ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | no       | 1 to 120 characters                                                                                                                                                     |
| `url`       | no       | The same rules as on create                                                                                                                                             |
| `events`    | no       | A non-empty list of event names, unless `allEvents` is `true`                                                                                                           |
| `allEvents` | no       | `true` or `false`                                                                                                                                                       |
| `enabled`   | no       | `true` or `false`. Enabling a webhook resets its count of failed deliveries.                                                                                            |
| `method`    | no       | `POST`, `PUT` or `PATCH`                                                                                                                                                |
| `headers`   | no       | The same rules as on create. The list replaces the stored headers: a name left out is removed. A header without `value` keeps the value already stored under that name. |
| `payload`   | no       | The same rules as on create                                                                                                                                             |

### Example

```bash
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}' \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J"
```

## Delete

`DELETE /api/v1/projects/{projectId}/webhooks/{webhookUlid}`

Returns `204` on success, and deletes the webhook's delivery history with it. An unknown `webhookUlid` returns `404 not_found`.

### Example

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J"
```

## Rotate the secret

`POST /api/v1/projects/{projectId}/webhooks/{webhookUlid}/rotate-secret`

Replaces the signing secret. The response returns the new secret once. The current secret stops verifying requests as soon as you rotate it, so update your receiver with the new secret right away.

### Response

`200` with `{ "secret": "whsec_...", "secretHint": "..." }`. `secretHint` holds the last four characters of the new secret.

### Example

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J/rotate-secret"
```

## Send a test

`POST /api/v1/projects/{projectId}/webhooks/{webhookUlid}/test`

Sends one sample event to the webhook, with `test` set to `true`. The request is attempted once, synchronously, with no retries. A test never counts toward the failure count.

### Request body

| Field       | Required | Notes                                                                                                                                                                |
| ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventType` | no       | An event name from the catalog. The sample event has this type. Defaults to the webhook's first subscribed event, or `test_case.created` when `allEvents` is `true`. |

### Response

`201` with a [delivery detail](#delivery-fields). `409 conflict` when the webhook is disabled, or when the sample body is over 256 KiB.

### Example

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"eventType":"test_run.closed"}' \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J/test"
```

## List deliveries

`GET /api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries`

Page-based pagination.

### Query parameters

| Parameter  | Notes                                    |
| ---------- | ---------------------------------------- |
| `page`     | Page number (1-based, default `1`)       |
| `pageSize` | Items per page (default `50`, max `200`) |
| `status`   | `pending`, `succeeded` or `failed`       |

### Response

`200` with `{ items, page, pageSize, total }`. Each item follows the [delivery fields](#delivery-fields).

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J/deliveries?status=failed"
```

## Get a delivery

`GET /api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries/{deliveryUlid}`

Returns the [delivery fields](#delivery-fields) plus the request Probara sent and the response it received. Custom header values in `request.headers` appear as `[redacted]`.

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J/deliveries/01JC8Z3P9Q8R7S6T5V4W3X2Y1Z"
```

## Redeliver a delivery

`POST /api/v1/projects/{projectId}/webhooks/{webhookUlid}/deliveries/{deliveryUlid}/redeliver`

Sends the same event again as a new delivery, with the same event ID and body, to the webhook's current URL, method and headers. The new delivery's `redeliveryOf` holds the ULID of the delivery it resends. Redeliveries count toward the failure count.

### Response

`202` with the new [delivery fields](#delivery-fields). `409 conflict` when the webhook is disabled, or when the delivery's body was never sent because it was over 256 KiB.

### Example

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/CHK/webhooks/01JC8Z3N0A1B2C3D4E5F6G7H8J/deliveries/01JC8Z3P9Q8R7S6T5V4W3X2Y1Z/redeliver"
```

## Webhook fields

Create, get, list and update responses return these fields. Times are Unix epoch milliseconds.

| Field                 | Type                                       | Notes                                                                                                                         |
| --------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `ulid`                | string (ULID)                              | Server-assigned identifier                                                                                                    |
| `projectUlid`         | string (ULID)                              | The project this webhook belongs to                                                                                           |
| `name`                | string                                     | Display name                                                                                                                  |
| `url`                 | string                                     | Endpoint URL                                                                                                                  |
| `enabled`             | boolean                                    | Whether events are sent                                                                                                       |
| `disabledReason`      | `manual`, `failures` or `null`             | Why the webhook is off: `manual` when a person turned it off, `failures` when Probara switched it off after failed deliveries |
| `disabledAt`          | number or `null`                           | When it was switched off                                                                                                      |
| `events`              | string[]                                   | Subscribed event names                                                                                                        |
| `allEvents`           | boolean                                    | `true` when the webhook receives every event                                                                                  |
| `method`              | string                                     | `POST`, `PUT` or `PATCH`                                                                                                      |
| `headers`             | `{ name }[]`                               | Header names only. Values are write-only.                                                                                     |
| `payload`             | object                                     | The payload settings, as sent on create                                                                                       |
| `secretHint`          | string                                     | The last four characters of the signing secret                                                                                |
| `consecutiveFailures` | number                                     | Failed deliveries in a row                                                                                                    |
| `lastDeliveryAt`      | number or `null`                           | When the most recent delivery was made                                                                                        |
| `lastDeliveryStatus`  | `pending`, `succeeded`, `failed` or `null` | Status of the most recent delivery                                                                                            |
| `lastSuccessAt`       | number or `null`                           | When the last successful delivery was made                                                                                    |
| `createdAt`           | number                                     | When the webhook was created                                                                                                  |
| `updatedAt`           | number                                     | When the webhook was last changed                                                                                             |

## Delivery fields

Delivery responses return these fields. Times are Unix epoch milliseconds.

| Field            | Type                               | Notes                                                                                                                       |
| ---------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `ulid`           | string (ULID)                      | Delivery identifier                                                                                                         |
| `webhookUlid`    | string (ULID)                      | The webhook                                                                                                                 |
| `eventId`        | string (ULID)                      | The event. Equal to the `webhook-id` header.                                                                                |
| `eventType`      | string                             | The event name                                                                                                              |
| `test`           | boolean                            | `true` for a test delivery                                                                                                  |
| `redeliveryOf`   | string (ULID) or `null`            | The delivery this one resends                                                                                               |
| `status`         | `pending`, `succeeded` or `failed` | Current status                                                                                                              |
| `attempts`       | number                             | Attempts made so far                                                                                                        |
| `createdAt`      | number                             | When the delivery was created                                                                                               |
| `lastAttemptAt`  | number or `null`                   | When the last attempt was made                                                                                              |
| `nextAttemptAt`  | number or `null`                   | When the next retry is due                                                                                                  |
| `responseStatus` | number or `null`                   | HTTP status of the last attempt                                                                                             |
| `durationMs`     | number or `null`                   | How long the last attempt took                                                                                              |
| `error`          | string or `null`                   | Why the delivery failed without a `2xx` response, such as a timeout, a DNS or TLS error, a redirect, or a body over 256 KiB |

A delivery detail adds two objects:

| Field      | Type             | Notes                                                                                                                                  |
| ---------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `request`  | object           | `method`, `url`, `headers` and `body` that Probara sent. Custom header values appear as `[redacted]`.                                  |
| `response` | object or `null` | `status`, `headers` and `body` the endpoint returned. The body is kept up to 4,096 bytes, and `bodyTruncated` says whether it was cut. |

## Errors

| Status | Code                | When                                                                                                                                                           |
| ------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `unauthorized`      | The token is missing, invalid or revoked                                                                                                                       |
| `403`  | `forbidden`         | The token lacks `integrations.manage`, or a write is made on a plan without webhooks                                                                           |
| `404`  | `not_found`         | The webhook or delivery is unknown                                                                                                                             |
| `409`  | `conflict`          | A test or a redelivery is requested for a disabled webhook, a redelivery is requested for a delivery whose body was never sent, or a test body is over 256 KiB |
| `422`  | `validation_failed` | The body or a query parameter is invalid, including a URL the endpoint rules refuse, or a project already has 20 webhooks                                      |
| `429`  | `too_many_requests` | The write rate limit was exceeded                                                                                                                              |

## Audit events

Creating, updating and deleting a webhook, rotating its secret, and switching it off automatically emit `webhook.created`, `webhook.updated`, `webhook.deleted`, `webhook.secret_rotated` and `webhook.auto_disabled` on the organization activity feed. See [Audit events](https://docs.probara.net/en/developers/api/audit-events/).
