# Webhooks API reference

> HTML version: https://storeshot.art/docs/api/webhooks/

Signed HTTPS callbacks when generations and sets finish. Contract 1.18.0; base URL `https://api.storeshot.art/v1`. Schemas: https://storeshot.art/docs/api/schemas.md

## GET /webhooks

**List webhooks** (operation `listWebhooks`). Auth: API key with scope `webhooks:read` or `webhooks:write`, or access token.

Requires `webhooks:read` or `webhooks:write`. An API key sees the webhooks of its own environment; an access token sees both (or one with `?environment=`). Secrets are never returned.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `environment` | query | `ApiKeyEnvironment` | no |  |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Webhooks, newest first. | `WebhookList` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "data": [
    {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "environment": "live",
      "url": "https://hooks.example.com/…shot",
      "events": [],
      "description": "string",
      "enabled": true,
      "creditsLowThreshold": 0,
      "secret": {
        "hint": "whsec_…Xy7Q",
        "rotatedAt": "2026-10-21T12:00:00.000Z",
        "previousExpiresAt": "2026-10-21T12:00:00.000Z"
      },
      "createdAt": "2026-10-21T12:00:00.000Z",
      "updatedAt": "2026-10-21T12:00:00.000Z"
    }
  ]
}
```

## POST /webhooks

**Create a webhook** (operation `createWebhook`). Auth: API key with scope `webhooks:write`, or access token.

Requires `webhooks:write` (not a default scope). Registers an `https://` endpoint (port 443, public addresses only) for the given events of one environment; at most 10 per environment. The signing secret is in this response only.

**When to use.** Instead of polling, when you run an https endpoint that can receive callbacks; agents in a terminal usually poll.

**Credits.** Free: no credits.

**Request body** (`application/json`, required, schema `CreateWebhookRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | `string` | yes | An `https://` URL on port 443, without credentials, at most 2048 characters, whose host resolves only to public addresses (checked now and before every delivery). Shown masked afterwards. |
| `events` | `array of WebhookEventType` | yes | The business events to deliver (1–4, unique). |
| `description` | `string` | no | Free text, at most 200 characters. |
| `enabled` | `boolean` | no | Default `true`. |
| `creditsLowThreshold` | `integer` | no | Default 5. |
| `environment` | `ApiKeyEnvironment` | no | Which events the webhook receives: `live` or `test` (test-key generations and sets). Default: the API key environment (access tokens: `live`). An API key can only create webhooks of its own environment. |

Example request body:

```json
{
  "url": "https://hooks.example.com/storeshot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 1,
  "environment": "live"
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `201` | Created. Store `secret.value` now; it is never shown again. | `WebhookWithSecret` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `413` | `PAYLOAD_TOO_LARGE`. | `ErrorEnvelope` | `X-Request-Id` |
| `422` | `WEBHOOK_URL_REJECTED`: the host is not public or does not resolve (`details.reason`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `503` | `SERVICE_UNAVAILABLE`: secrets cannot be issued right now. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 201 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "environment": "live",
  "url": "https://hooks.example.com/…shot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 0,
  "secret": {
    "hint": "whsec_…Xy7Q",
    "rotatedAt": "2026-10-21T12:00:00.000Z",
    "previousExpiresAt": "2026-10-21T12:00:00.000Z",
    "value": "string"
  },
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z"
}
```

## GET /webhooks/{id}

**Get a webhook** (operation `getWebhook`). Auth: API key with scope `webhooks:read` or `webhooks:write`, or access token.

Requires `webhooks:read` or `webhooks:write`. The secret is never returned.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The webhook. | `Webhook` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "environment": "live",
  "url": "https://hooks.example.com/…shot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 0,
  "secret": {
    "hint": "whsec_…Xy7Q",
    "rotatedAt": "2026-10-21T12:00:00.000Z",
    "previousExpiresAt": "2026-10-21T12:00:00.000Z"
  },
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z"
}
```

## DELETE /webhooks/{id}

**Delete a webhook** (operation `deleteWebhook`). Auth: API key with scope `webhooks:write`, or access token.

Requires `webhooks:write`. Deletes the webhook with its delivery log; pending deliveries are dropped.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `204` | Deleted. | — | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

## PATCH /webhooks/{id}

**Update a webhook** (operation `updateWebhook`). Auth: API key with scope `webhooks:write`, or access token.

Requires `webhooks:write`. Send any of `url`, `events`, `description`, `enabled`, `creditsLowThreshold`. A new `url` is checked like on create. While a webhook is disabled it gets no new events, and a pending delivery reaching its next attempt is canceled.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |

**Request body** (`application/json`, required, schema `UpdateWebhookRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | `string` | no | An `https://` URL on port 443, without credentials, at most 2048 characters, whose host resolves only to public addresses (checked now and before every delivery). Shown masked afterwards. |
| `events` | `array of WebhookEventType` | no | The business events to deliver (1–4, unique). |
| `description` | `string` | no | Free text, at most 200 characters. |
| `enabled` | `boolean` | no |  |
| `creditsLowThreshold` | `integer` | no | `credits.low` fires when the live balance falls below this many credits (1–10000). |

Example request body:

```json
{
  "url": "https://hooks.example.com/storeshot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 1
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The updated webhook. | `Webhook` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `413` | `PAYLOAD_TOO_LARGE`. | `ErrorEnvelope` | `X-Request-Id` |
| `422` | `WEBHOOK_URL_REJECTED`: the host is not public or does not resolve (`details.reason`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "environment": "live",
  "url": "https://hooks.example.com/…shot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 0,
  "secret": {
    "hint": "whsec_…Xy7Q",
    "rotatedAt": "2026-10-21T12:00:00.000Z",
    "previousExpiresAt": "2026-10-21T12:00:00.000Z"
  },
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z"
}
```

## POST /webhooks/{id}/rotate-secret

**Rotate the signing secret** (operation `rotateWebhookSecret`). Auth: API key with scope `webhooks:write`, or access token.

Requires `webhooks:write`. Issues a new secret (in this response only). For `gracePeriodSeconds` (default 24 hours) deliveries carry a signature with each secret, so receivers can switch without dropping events.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |

**Request body** (`application/json`, optional, schema `RotateWebhookSecretRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `gracePeriodSeconds` | `integer` | no | How long the previous secret keeps signing deliveries next to the new one (0–604800, default 86400). 0 drops it at once. |

Example request body:

```json
{
  "gracePeriodSeconds": 0
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Rotated. Store `secret.value` now; it is never shown again. | `WebhookWithSecret` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `413` | `PAYLOAD_TOO_LARGE`. | `ErrorEnvelope` | `X-Request-Id` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `503` | `SERVICE_UNAVAILABLE`: secrets cannot be issued right now. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "environment": "live",
  "url": "https://hooks.example.com/…shot",
  "events": [
    "generation.succeeded"
  ],
  "description": "string",
  "enabled": true,
  "creditsLowThreshold": 0,
  "secret": {
    "hint": "whsec_…Xy7Q",
    "rotatedAt": "2026-10-21T12:00:00.000Z",
    "previousExpiresAt": "2026-10-21T12:00:00.000Z",
    "value": "string"
  },
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z"
}
```

## POST /webhooks/{id}/test

**Send a test event** (operation `testWebhook`). Auth: API key with scope `webhooks:write`, or access token.

Requires `webhooks:write`. Queues a signed `webhook.test` event to this webhook (whatever its `events`); follow it in `GET /webhooks/{id}/deliveries`. At most 10 per webhook per hour.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `202` | Queued. | `WebhookTestResult` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `WEBHOOK_STATE_CONFLICT`: the webhook is disabled (`details.action` `test`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 202 response:

```json
{
  "eventId": "evt_3f2a…",
  "deliveryId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
}
```

## GET /webhooks/{id}/deliveries

**List deliveries** (operation `listWebhookDeliveries`). Auth: API key with scope `webhooks:read` or `webhooks:write`, or access token.

Requires `webhooks:read` or `webhooks:write`. Deliveries of this webhook, newest first, with every attempt (status code, latency, error class and the start of the response body). Kept 30 days after they finish.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Webhook id. An unknown or malformed id is 404. |
| `limit` | query | `integer` | no | 1–100, default 20. Default: 20 |
| `cursor` | query | `string` | no | `nextCursor` of the previous page. |
| `status` | query | `WebhookDeliveryStatus` | no |  |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | A page of deliveries. | `WebhookDeliveryList` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "data": [
    {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "eventId": "evt_3f2a…",
      "eventType": "generation.succeeded",
      "status": "pending",
      "attempts": 0,
      "nextAttemptAt": "2026-10-21T12:00:00.000Z",
      "createdAt": "2026-10-21T12:00:00.000Z",
      "finishedAt": "2026-10-21T12:00:00.000Z",
      "log": [
        {}
      ]
    }
  ],
  "nextCursor": "string"
}
```

## Webhook events

Sent as signed `POST` requests to your webhook URL.

### `generation.succeeded`

**A generation succeeded** (`webhook_generation_succeeded`). Payload schema `GenerationSucceededEvent`.

Sent as a signed `POST` to your webhook URL (at least once, unordered). Answer any 2xx within 10 seconds; anything else is retried 7 times over about 23 hours. Verify `Storeshot-Signature` over the raw body before parsing it.

Headers: `storeshot-signature`, `storeshot-event-id`, `storeshot-event-type`, `storeshot-delivery-id`, `storeshot-delivery-attempt`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Event id (`evt_…`), identical on every attempt: deduplicate on it. |
| `type` | `string` | yes | One of: "generation.succeeded" |
| `created` | `string` | yes | RFC 3339 UTC timestamp. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `apiVersion` | `string` | yes |  |
| `data` | `WebhookGenerationData` | yes |  |

Example payload:

```json
{
  "id": "string",
  "type": "generation.succeeded",
  "created": "2026-10-21T12:00:00.000Z",
  "environment": "live",
  "apiVersion": "1.8.0",
  "data": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "status": "queued",
    "environment": "live",
    "inputs": [
      {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
        "sourceUrl": "string"
      }
    ],
    "store": "app_store",
    "device": "app_store_1290x2796",
    "size": {
      "width": 1,
      "height": 1
    },
    "orientation": "portrait",
    "app": {
      "name": "string",
      "description": "string"
    },
    "caption": "string",
    "palette": [
      {}
    ],
    "logo": {
      "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
    },
    "metadata": {},
    "creditsCharged": 0,
    "creditsRefunded": 0,
    "result": {
      "image": {
        "contentType": "image/png",
        "width": 1290,
        "height": 2796
      },
      "thumbnail": {
        "contentType": "image/webp"
      },
      "caption": "string",
      "palette": []
    },
    "error": {
      "code": "GENERATION_FAILED",
      "message": "string"
    },
    "createdAt": "2026-10-21T12:00:00.000Z",
    "startedAt": "2026-10-21T12:00:00.000Z",
    "finishedAt": "2026-10-21T12:00:00.000Z",
    "deletedAt": "2026-10-21T12:00:00.000Z",
    "screenshotSet": {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "position": 1,
      "attempt": 1
    }
  }
}
```

### `generation.failed`

**A generation failed or was canceled** (`webhook_generation_failed`). Payload schema `GenerationFailedEvent`.

Sent as a signed `POST` to your webhook URL (at least once, unordered). Answer any 2xx within 10 seconds; anything else is retried 7 times over about 23 hours. Verify `Storeshot-Signature` over the raw body before parsing it.

Headers: `storeshot-signature`, `storeshot-event-id`, `storeshot-event-type`, `storeshot-delivery-id`, `storeshot-delivery-attempt`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Event id (`evt_…`), identical on every attempt: deduplicate on it. |
| `type` | `string` | yes | One of: "generation.failed" |
| `created` | `string` | yes | RFC 3339 UTC timestamp. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `apiVersion` | `string` | yes |  |
| `data` | `WebhookGenerationData` | yes |  |

Example payload:

```json
{
  "id": "string",
  "type": "generation.failed",
  "created": "2026-10-21T12:00:00.000Z",
  "environment": "live",
  "apiVersion": "1.8.0",
  "data": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "status": "queued",
    "environment": "live",
    "inputs": [
      {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
        "sourceUrl": "string"
      }
    ],
    "store": "app_store",
    "device": "app_store_1290x2796",
    "size": {
      "width": 1,
      "height": 1
    },
    "orientation": "portrait",
    "app": {
      "name": "string",
      "description": "string"
    },
    "caption": "string",
    "palette": [
      {}
    ],
    "logo": {
      "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
    },
    "metadata": {},
    "creditsCharged": 0,
    "creditsRefunded": 0,
    "result": {
      "image": {
        "contentType": "image/png",
        "width": 1290,
        "height": 2796
      },
      "thumbnail": {
        "contentType": "image/webp"
      },
      "caption": "string",
      "palette": []
    },
    "error": {
      "code": "GENERATION_FAILED",
      "message": "string"
    },
    "createdAt": "2026-10-21T12:00:00.000Z",
    "startedAt": "2026-10-21T12:00:00.000Z",
    "finishedAt": "2026-10-21T12:00:00.000Z",
    "deletedAt": "2026-10-21T12:00:00.000Z",
    "screenshotSet": {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "position": 1,
      "attempt": 1
    }
  }
}
```

### `screenshot_set.completed`

**A screenshot set reached a final status** (`webhook_screenshot_set_completed`). Payload schema `ScreenshotSetCompletedEvent`.

Sent as a signed `POST` to your webhook URL (at least once, unordered). Answer any 2xx within 10 seconds; anything else is retried 7 times over about 23 hours. Verify `Storeshot-Signature` over the raw body before parsing it.

Headers: `storeshot-signature`, `storeshot-event-id`, `storeshot-event-type`, `storeshot-delivery-id`, `storeshot-delivery-attempt`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Event id (`evt_…`), identical on every attempt: deduplicate on it. |
| `type` | `string` | yes | One of: "screenshot_set.completed" |
| `created` | `string` | yes | RFC 3339 UTC timestamp. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `apiVersion` | `string` | yes |  |
| `data` | `WebhookScreenshotSetData` | yes |  |

Example payload:

```json
{
  "id": "string",
  "type": "screenshot_set.completed",
  "created": "2026-10-21T12:00:00.000Z",
  "environment": "live",
  "apiVersion": "1.8.0",
  "data": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "status": "queued",
    "environment": "live",
    "app": {
      "name": "string",
      "description": "string"
    },
    "orientation": "portrait",
    "palette": [
      {}
    ],
    "paletteSource": "given",
    "logo": {
      "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
    },
    "metadata": {},
    "screens": [
      {
        "position": 1,
        "input": {},
        "caption": "string"
      }
    ],
    "stores": [
      {
        "devices": [],
        "featureGraphic": {}
      }
    ],
    "images": 1,
    "counts": {
      "queued": 0,
      "running": 0,
      "succeeded": 0,
      "failed": 0,
      "canceled": 0
    },
    "creditsCharged": 0,
    "creditsRefunded": 0,
    "children": [
      {
        "position": 1,
        "device": "string",
        "size": {},
        "featureGraphic": true,
        "generationId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
        "attempt": 1,
        "error": {}
      }
    ],
    "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
    "createdAt": "2026-10-21T12:00:00.000Z",
    "updatedAt": "2026-10-21T12:00:00.000Z",
    "finishedAt": "2026-10-21T12:00:00.000Z",
    "deletedAt": "2026-10-21T12:00:00.000Z",
    "preview": {
      "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
      "store": "app_store",
      "device": "app_store_1290x2796",
      "thumbnail": {
        "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
        "contentType": "image/webp"
      },
      "urlsExpireAt": "2026-10-24T12:15:00.000Z"
    }
  }
}
```

### `credits.low`

**The live credit balance fell below the threshold** (`webhook_credits_low`). Payload schema `CreditsLowEvent`.

Sent as a signed `POST` to your webhook URL (at least once, unordered). Answer any 2xx within 10 seconds; anything else is retried 7 times over about 23 hours. Verify `Storeshot-Signature` over the raw body before parsing it.

Headers: `storeshot-signature`, `storeshot-event-id`, `storeshot-event-type`, `storeshot-delivery-id`, `storeshot-delivery-attempt`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Event id (`evt_…`), identical on every attempt: deduplicate on it. |
| `type` | `string` | yes | One of: "credits.low" |
| `created` | `string` | yes | RFC 3339 UTC timestamp. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `apiVersion` | `string` | yes |  |
| `data` | `object` | yes |  |
| `data.balances` | `object` | yes |  |
| `data.balances.starter` | `integer` | yes |  |
| `data.balances.subscription` | `integer` | yes |  |
| `data.balances.purchased` | `integer` | yes |  |
| `data.total` | `integer` | yes |  |
| `data.threshold` | `integer` | yes |  |

Example payload:

```json
{
  "id": "string",
  "type": "credits.low",
  "created": "2026-10-21T12:00:00.000Z",
  "environment": "live",
  "apiVersion": "1.8.0",
  "data": {
    "balances": {
      "starter": 0,
      "subscription": 0,
      "purchased": 0
    },
    "total": 0,
    "threshold": 0
  }
}
```

### `webhook.test`

**Test event (POST /webhooks/{id}/test)** (`webhook_webhook_test`). Payload schema `WebhookTestEvent`.

Sent as a signed `POST` to your webhook URL (at least once, unordered). Answer any 2xx within 10 seconds; anything else is retried 7 times over about 23 hours. Verify `Storeshot-Signature` over the raw body before parsing it.

Headers: `storeshot-signature`, `storeshot-event-id`, `storeshot-event-type`, `storeshot-delivery-id`, `storeshot-delivery-attempt`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Event id (`evt_…`), identical on every attempt: deduplicate on it. |
| `type` | `string` | yes | One of: "webhook.test" |
| `created` | `string` | yes | RFC 3339 UTC timestamp. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `apiVersion` | `string` | yes |  |
| `data` | `object` | yes |  |
| `data.webhookId` | `string` | yes | Format: uuid |
| `data.message` | `string` | yes |  |

Example payload:

```json
{
  "id": "string",
  "type": "webhook.test",
  "created": "2026-10-21T12:00:00.000Z",
  "environment": "live",
  "apiVersion": "1.8.0",
  "data": {
    "webhookId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "message": "string"
  }
}
```
