# Webhooks API

> Register HTTPS endpoints for signed lifecycle events and inspect their deliveries.

- [List webhooks](#list-webhooks): `GET /v1/webhooks`
- [Create a webhook](#create-a-webhook): `POST /v1/webhooks`
- [Get a webhook](#get-a-webhook): `GET /v1/webhooks/{webhook_id}`
- [Change a webhook](#change-a-webhook): `PATCH /v1/webhooks/{webhook_id}`
- [Delete a webhook](#delete-a-webhook): `DELETE /v1/webhooks/{webhook_id}`
- [Rotate a webhook secret](#rotate-a-webhook-secret): `POST /v1/webhooks/{webhook_id}/rotate-secret`
- [List webhook deliveries](#list-webhook-deliveries): `GET /v1/webhooks/{webhook_id}/deliveries`

## List webhooks

`GET /v1/webhooks`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | `integer` | No |  |
| `cursor` | query | `string (uuid)` | No |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WebhookPage`](/docs/api/schemas#webhookpage) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | A role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/webhooks" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "items": [
    {
      "created_at": "2026-10-12T05:51:01Z",
      "created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "enabled": false,
      "events": [
        "machine.ready"
      ],
      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "name": "ci",
      "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "secret_rotated_at": "2026-10-12T05:51:01Z",
      "updated_at": "2026-10-12T05:51:01Z",
      "url": "https://hooks.example.com/vmyard"
    }
  ],
  "next_cursor": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f"
}
```

## Create a webhook

`POST /v1/webhooks`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Request body** (`application/json`, [`CreateWebhookRequest`](/docs/api/schemas#createwebhookrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `events` | [`WebhookEventDto[]`](/docs/api/schemas#webhookeventdto) | Yes | at least 1 items. |
| `name` | `string` | Yes | 1–64 characters. |
| `url` | `string` | Yes | `https` on port 443, without credentials or a fragment, naming a public host. Redirects are never followed. at most 2048 characters. |
| `enabled` | `boolean` | No | default `true`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `201` | The signing secret is returned only in this response | [`WebhookSecretDto`](/docs/api/schemas#webhooksecretdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | CSRF failure, a role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | The organization already has 10 endpoints | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `webhooks_unavailable`: no secret keys are configured, and secrets are never stored in plaintext | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/webhooks" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ci",
  "url": "https://hooks.example.com/vmyard",
  "events": [
    "machine.ready",
    "machine.error"
  ]
}'
```

```json title="Response 201"
{
  "secret": "whsec_…",
  "webhook": {
    "created_at": "2026-10-12T05:51:01Z",
    "created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "enabled": false,
    "events": [
      "machine.ready"
    ],
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "ci",
    "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "secret_rotated_at": "2026-10-12T05:51:01Z",
    "updated_at": "2026-10-12T05:51:01Z",
    "url": "https://hooks.example.com/vmyard"
  }
}
```

## Get a webhook

`GET /v1/webhooks/{webhook_id}`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WebhookDto`](/docs/api/schemas#webhookdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | A role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "created_at": "2026-10-12T05:51:01Z",
  "created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "enabled": false,
  "events": [
    "machine.ready"
  ],
  "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "name": "ci",
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "secret_rotated_at": "2026-10-12T05:51:01Z",
  "updated_at": "2026-10-12T05:51:01Z",
  "url": "https://hooks.example.com/vmyard"
}
```

## Change a webhook

`PATCH /v1/webhooks/{webhook_id}`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Request body** (`application/json`, [`UpdateWebhookRequest`](/docs/api/schemas#updatewebhookrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean \| null` | No |  |
| `events` | [`WebhookEventDto[] \| null`](/docs/api/schemas#webhookeventdto) | No | at least 1 items. |
| `name` | `string \| null` | No | 1–64 characters. |
| `url` | `string \| null` | No | at most 2048 characters. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | Queued deliveries of a disabled endpoint wait until it is enabled again | [`WebhookDto`](/docs/api/schemas#webhookdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | CSRF failure, a role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X PATCH "$VMYARD_API_URL/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": false
}'
```

```json title="Response 200"
{
  "created_at": "2026-10-12T05:51:01Z",
  "created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "enabled": false,
  "events": [
    "machine.ready"
  ],
  "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "name": "ci",
  "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "secret_rotated_at": "2026-10-12T05:51:01Z",
  "updated_at": "2026-10-12T05:51:01Z",
  "url": "https://hooks.example.com/vmyard"
}
```

## Delete a webhook

`DELETE /v1/webhooks/{webhook_id}`

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `204` | Deleted together with its queued and past deliveries | — |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | CSRF failure, a role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X DELETE "$VMYARD_API_URL/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

## Rotate a webhook secret

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

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | The old secret stops signing immediately, including retries of queued deliveries | [`WebhookSecretDto`](/docs/api/schemas#webhooksecretdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | CSRF failure, a role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `webhooks_unavailable`: no secret keys are configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/webhooks/$WEBHOOK_ID/rotate-secret" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "secret": "whsec_…",
  "webhook": {
    "created_at": "2026-10-12T05:51:01Z",
    "created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "enabled": false,
    "events": [
      "machine.ready"
    ],
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "ci",
    "organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "secret_rotated_at": "2026-10-12T05:51:01Z",
    "updated_at": "2026-10-12T05:51:01Z",
    "url": "https://hooks.example.com/vmyard"
  }
}
```

## List webhook deliveries

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

- **API key:** `Authorization: Bearer vmy_…` with preset `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Owner or Admin.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | `integer` | No | 1-100, default 50; newest first. |
| `webhook_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Organization context. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | Recent deliveries to this endpoint, newest first, with their last attempt | [`WebhookDeliveryPage`](/docs/api/schemas#webhookdeliverypage) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | A role other than owner/admin, or an API key without full_access | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/webhooks/$WEBHOOK_ID/deliveries" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

```json title="Response 200"
{
  "items": [
    {
      "attempt_count": 0,
      "created_at": "2026-10-12T05:51:01Z",
      "delivered_at": "2026-10-12T05:51:01Z",
      "event_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "event_type": "machine.ready",
      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "last_attempt_at": "2026-10-12T05:51:01Z",
      "last_error": "string",
      "last_status_code": 0,
      "machine_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "next_attempt_at": "2026-10-12T05:51:01Z",
      "operation_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "status": "pending"
    }
  ]
}
```
