# Webhooks

> Outbound, signed webhooks and their delivery log.

Outbound, signed webhooks and their delivery log.

| Operation | Method and path |
| --- | --- |
| [List webhooks](#list-webhooks) | `GET /v1/workspaces/{workspace}/webhooks` |
| [Create a webhook](#create-a-webhook) | `POST /v1/workspaces/{workspace}/webhooks` |
| [Get a webhook](#get-a-webhook) | `GET /v1/workspaces/{workspace}/webhooks/{webhook_id}` |
| [Update a webhook](#update-a-webhook) | `PATCH /v1/workspaces/{workspace}/webhooks/{webhook_id}` |
| [Delete a webhook](#delete-a-webhook) | `DELETE /v1/workspaces/{workspace}/webhooks/{webhook_id}` |
| [Rotate the signing secret](#rotate-the-signing-secret) | `POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/rotate-secret` |
| [Send a test event](#send-a-test-event) | `POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/test` |
| [Delivery log](#delivery-log) | `GET /v1/workspaces/{workspace}/webhooks/{webhook_id}/deliveries` |
| [Redeliver an event](#redeliver-an-event) | `POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver` |

## List webhooks

```http
GET /v1/workspaces/{workspace}/webhooks
```

- **Operation:** `listWebhooks`
- **Scopes:** `webhooks:read`
- **Paginated:** `?limit=` and `?cursor=`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | A page of Webhook objects. | [WebhookList](/docs/api/objects/webhook-list/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/webhooks \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "data": [
    {
      "id": "whk_01jbx2m9k7q4r8t5v6w3y0z1pr",
      "workspace_id": "string",
      "url": "https://guv.dev/",
      "description": "string",
      "events": [
        "deployment.created"
      ],
      "project_ids": [
        "string"
      ],
      "enabled": false,
      "secret_prefix": "whsec_Ab3d",
      "last_delivery": {
        "status": "string",
        "at": "2026-10-22T13:05:00Z"
      },
      "created_at": "2026-10-22T13:05:00Z",
      "updated_at": "2026-10-22T13:05:00Z"
    }
  ],
  "next_cursor": "string",
  "has_more": false
}
```

## Create a webhook

```http
POST /v1/workspaces/{workspace}/webhooks
```

The signing secret is returned once.

- **Operation:** `createWebhook`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required; supports `?dry_run=true`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |

**Request body** (`application/json`, required): [WebhookCreate](/docs/api/objects/webhook-create/)

| Field | Type | Description |
| --- | --- | --- |
| `url` * | string (uri) |  |
| `description` | string |  |
| `events` * | array of [WebhookEventType](/docs/api/objects/webhook-event-type/) |  |
| `project_ids` | array of string or null |  |
| `enabled` | boolean | Default `true`. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Dry run. Nothing was changed; this is what would happen. | [Plan](/docs/api/objects/plan/) |
| `201` Created | Created. | [WebhookCreated](/docs/api/objects/webhook-created/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/webhooks \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://guv.dev/",
  "description": "string",
  "events": [
    "deployment.created"
  ],
  "project_ids": [
    "string"
  ],
  "enabled": true
}'
```

**Example response**

```json
{
  "id": "whk_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "workspace_id": "string",
  "url": "https://guv.dev/",
  "description": "string",
  "events": [
    "deployment.created"
  ],
  "project_ids": [
    "string"
  ],
  "enabled": false,
  "secret_prefix": "whsec_Ab3d",
  "last_delivery": {
    "status": "string",
    "at": "2026-10-22T13:05:00Z"
  },
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z",
  "secret": "string"
}
```

## Get a webhook

```http
GET /v1/workspaces/{workspace}/webhooks/{webhook_id}
```

- **Operation:** `getWebhook`
- **Scopes:** `webhooks:read`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [Webhook](/docs/api/objects/webhook/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "id": "whk_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "workspace_id": "string",
  "url": "https://guv.dev/",
  "description": "string",
  "events": [
    "deployment.created"
  ],
  "project_ids": [
    "string"
  ],
  "enabled": false,
  "secret_prefix": "whsec_Ab3d",
  "last_delivery": {
    "status": "string",
    "at": "2026-10-22T13:05:00Z"
  },
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z"
}
```

## Update a webhook

```http
PATCH /v1/workspaces/{workspace}/webhooks/{webhook_id}
```

- **Operation:** `updateWebhook`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required; supports `?dry_run=true`
- **Concurrency:** send the `ETag` you read as `If-Match`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |

**Request body** (`application/json`, required): [WebhookUpdate](/docs/api/objects/webhook-update/)

| Field | Type | Description |
| --- | --- | --- |
| `url` | string (uri) |  |
| `description` | string |  |
| `events` | array of [WebhookEventType](/docs/api/objects/webhook-event-type/) |  |
| `project_ids` | array of string or null |  |
| `enabled` | boolean |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. With `dry_run=true`: a Plan (application/vnd.guv.plan+json) and nothing changes. | [Webhook](/docs/api/objects/webhook/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `412`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X PATCH https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://guv.dev/",
  "description": "string",
  "events": [
    "deployment.created"
  ],
  "project_ids": [
    "string"
  ],
  "enabled": false
}'
```

**Example response**

```json
{
  "id": "whk_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "workspace_id": "string",
  "url": "https://guv.dev/",
  "description": "string",
  "events": [
    "deployment.created"
  ],
  "project_ids": [
    "string"
  ],
  "enabled": false,
  "secret_prefix": "whsec_Ab3d",
  "last_delivery": {
    "status": "string",
    "at": "2026-10-22T13:05:00Z"
  },
  "created_at": "2026-10-22T13:05:00Z",
  "updated_at": "2026-10-22T13:05:00Z"
}
```

## Delete a webhook

```http
DELETE /v1/workspaces/{workspace}/webhooks/{webhook_id}
```

- **Operation:** `deleteWebhook`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required; supports `?dry_run=true`
- **Concurrency:** send the `ETag` you read as `If-Match`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | Dry run. Nothing was changed; this is what would happen. | [Plan](/docs/api/objects/plan/) |
| `204` No Content | Done. No body. | none |

**Errors:** `400`, `401`, `403`, `404`, `409`, `412`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X DELETE https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

## Rotate the signing secret

```http
POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/rotate-secret
```

The old secret keeps working for 24 hours.

- **Operation:** `rotateWebhookSecret`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | OK. | [WebhookSecret](/docs/api/objects/webhook-secret/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr/rotate-secret \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

**Example response**

```json
{
  "secret": "string"
}
```

## Send a test event

```http
POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/test
```

- **Operation:** `testWebhook`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `201` Created | Created. | [WebhookDelivery](/docs/api/objects/webhook-delivery/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr/test \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

**Example response**

```json
{
  "id": "whd_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "webhook_id": "string",
  "event_id": "string",
  "event_type": "deployment.created",
  "attempt": 1,
  "status": "pending",
  "request": {
    "headers": {
      "key": "string"
    },
    "body": "string"
  },
  "response": {
    "status": 1,
    "body_excerpt": "string",
    "duration_ms": 1
  },
  "next_attempt_at": "2026-10-22T13:05:00Z",
  "created_at": "2026-10-22T13:05:00Z",
  "delivered_at": "2026-10-22T13:05:00Z"
}
```

## Delivery log

```http
GET /v1/workspaces/{workspace}/webhooks/{webhook_id}/deliveries
```

- **Operation:** `listWebhookDeliveries`
- **Scopes:** `webhooks:read`
- **Paginated:** `?limit=` and `?cursor=`

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |
| `status` | query | `pending`, `succeeded`, `failed`, `retrying` |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `200` OK | A page of WebhookDelivery objects. | [WebhookDeliveryList](/docs/api/objects/webhook-delivery-list/) |

**Errors:** `400`, `401`, `403`, `404`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr/deliveries \
  -H "Authorization: Bearer $GUV_TOKEN"
```

**Example response**

```json
{
  "data": [
    {
      "id": "whd_01jbx2m9k7q4r8t5v6w3y0z1pr",
      "webhook_id": "string",
      "event_id": "string",
      "event_type": "deployment.created",
      "attempt": 1,
      "status": "pending",
      "request": {
        "headers": {
          "key": "string"
        },
        "body": "string"
      },
      "response": {
        "status": 1,
        "body_excerpt": "string",
        "duration_ms": 1
      },
      "next_attempt_at": "2026-10-22T13:05:00Z",
      "created_at": "2026-10-22T13:05:00Z",
      "delivered_at": "2026-10-22T13:05:00Z"
    }
  ],
  "next_cursor": "string",
  "has_more": false
}
```

## Redeliver an event

```http
POST /v1/workspaces/{workspace}/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
```

- **Operation:** `redeliverWebhook`
- **Scopes:** `webhooks:write`
- **Idempotency-Key:** required

**Parameters**

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `workspace` * | path | string | Workspace id (`ws_…`) or slug. |
| `webhook_id` * | path | string |  |
| `delivery_id` * | path | string |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| `201` Created | Created. | [WebhookDelivery](/docs/api/objects/webhook-delivery/) |

**Errors:** `400`, `401`, `403`, `404`, `409`, `422`, `429`, `500`, as [problem+json](/errors/) with a stable `code`.

**Example**

```bash
curl -X POST https://api.guv.dev/v1/workspaces/acme/webhooks/web_01jbx2m9k7q4r8t5v6w3y0z1pr/deliveries/del_01jbx2m9k7q4r8t5v6w3y0z1pr/redeliver \
  -H "Authorization: Bearer $GUV_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

**Example response**

```json
{
  "id": "whd_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "webhook_id": "string",
  "event_id": "string",
  "event_type": "deployment.created",
  "attempt": 1,
  "status": "pending",
  "request": {
    "headers": {
      "key": "string"
    },
    "body": "string"
  },
  "response": {
    "status": 1,
    "body_excerpt": "string",
    "duration_ms": 1
  },
  "next_attempt_at": "2026-10-22T13:05:00Z",
  "created_at": "2026-10-22T13:05:00Z",
  "delivered_at": "2026-10-22T13:05:00Z"
}
```

## Event delivered to your webhook endpoint

Sent for each subscribed event type. Respond 2xx within 10 seconds. Failed deliveries are retried with exponential backoff for 24 hours and appear in the delivery log.

**Headers Guv sends**

| Header | Description |
| --- | --- |
| `Guv-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` |
| `Guv-Event-Id` |  |
| `Guv-Delivery-Id` |  |

**Body:** [WebhookEvent](/docs/api/objects/webhook-event/)

| Field | Type | Description |
| --- | --- | --- |
| `id` * | string |  |
| `type` * | [WebhookEventType](/docs/api/objects/webhook-event-type/) |  |
| `api_version` | string |  |
| `created_at` * | string (date-time) |  |
| `workspace_id` * | string |  |
| `project_id` | string or null |  |
| `data` * | object | The resource the event is about (for example a Deployment), as returned by the API. |

**Example payload**

```json
{
  "id": "evt_01jbx2m9k7q4r8t5v6w3y0z1ev",
  "type": "deployment.succeeded",
  "api_version": "2026-11-01",
  "created_at": "2026-10-23T14:05:12Z",
  "workspace_id": "ws_01jbx2m9k7q4r8t5v6w3y0z1ws",
  "project_id": "prj_01jbx2m9k7q4r8t5v6w3y0z1pr",
  "data": {
    "id": "dep_01jbx2m9k7q4r8t5v6w3y0z1dp",
    "status": "active",
    "service_name": "web",
    "environment_name": "production"
  }
}
```
