---
title: "Register a webhook endpoint"
method: POST
path: "/api/v1/webhook-endpoints"
tags: ["Webhooks"]
---

# Register a webhook endpoint

`POST /api/v1/webhook-endpoints`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/webhook-endpoints`
- Auth: API key in header `Authorization`

Registers an `https://` URL to receive job events, and answers `201` with the endpoint.

**The response is the only place the signing secret is ever readable** (`secret`).
Store it; a lost secret is rotated, not recovered. Every delivery carries
`ImageStep-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is
`HMAC-SHA256("<t>.<raw body>", secret)`: compute it over the raw body, compare in
constant time, and reject a `t` outside your tolerance window.

Omit `events`, or send `[]`, to receive every job-level event; per-item events are opt-in.

The URL must be on port 443 — the only port deliveries can reach — and resolve to a public
address. The address is checked now and again before every delivery attempt, against what
the host resolves to then, and redirects are not followed.

## Headers

- `Idempotency-Key` string

## Request body

- EndpointRequest: Webhook endpoint create/update payload
  - `url` string: Where to POST events: an `https://` URL on port 443 whose host resolves to a public address. Required on create; on update, send it only to change it.
  - `events` string[]: Event types to receive. Omit it, or send `[]`, for every job-level event (`job.completed`, `job.failed`); the per-item events (`job.item.completed`, `job.item.failed`) are opt-in, because a 500-item job would otherwise be 500 deliveries nobody asked for. Any other value is `400 invalid_param`.
  - `description` string: Free-text label shown in listings
  - `enabled` boolean: Update only — ignored on create, where an endpoint starts enabled. `false` pauses deliveries; `true` resumes them and clears an automatic disable.

## Response `201`

Registered and enabled. The one answer that carries `secret` in the clear.

## Other responses

- `400`: - `invalid_param` on `url` — missing - `invalid_param` on `url` — too long, not `https://`, a port other than 443, no host, or the host does not resolve to a public address (loopback, private, link-local and the other non-public ranges are refused) - `invalid_param` on `events` — a type that is not a subscribable event; `details.received` is the one refused - `invalid_param` on `description` — too long
- `409`: Conflict — `error.code` is `idempotency_key_reuse` (the key already means a different request) or `request_in_progress` (the first request under this key has not settled yet; retryable).
- `422`: `resource_limit_exceeded` — the account already has 10 endpoints; delete one first
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
