---
title: "Register a webhook endpoint (secret returned once)"
method: POST
path: "/webhooks"
tags: ["Webhooks"]
---

# Register a webhook endpoint (secret returned once)

`POST /webhooks`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/webhooks`
- Auth: HTTP bearer

Registers an HTTPS receiver under the calling application. The `secret` (`whsec_…`) is returned ONCE. Store it to verify signatures. An empty `events` array subscribes to all events; `"*"` also matches all.

## Request body

- object
  - `url` string, uri, required
  - `events` string[]: Event types to subscribe to; empty subscribes to all events, and `"*"` also matches all. Every value listed here has a producer: the enum is asserted against the emit sites in CI, so you can never subscribe to an event that cannot arrive. `connection.records.updated` / `connection.reapply.failed` / `connection.disconnected` cover the managed (async one-click) re-apply and disconnect lifecycle; `connection.disconnected` carries a `removal` object saying what happened to the DNS records. `certificate.issued` / `certificate.renewed` / `certificate.failed` follow the edge's certificate reports, and `certificate.expiring` fires 14, 7 and 3 days before an unrenewed certificate expires and when it expired; each carries a `certificate` object. `domain.record_missing` / `domain.record_restored` carry a `drift` object with the record, what public DNS shows and what was done about it. `connection.records_outdated` fires once per domain a routing change moves (saving a connect target, or `POST /v1/connections/{id}:retarget`), carrying `records.previous` and `records.current`; the domain needs the new records. `certificate.failed` fires once per failure episode, when the edge first reports that issuing or renewing the certificate failed. `secure_status` is sent only where the deployment has Secure on (`SECURE_ENABLED`), once each time a certificate's status changes. Every connection, certificate and routing event also appears on the domain's timeline (`GET /v1/connections/{id}/events`) as a row of the same kind.

## Response `201`

Created (secret returned once)

- WebhookEndpoint
  - `id` string, required
  - `application_id` string
  - `url` string, uri, required
  - `events` string[]
  - `secret` string, nullable: Present ONLY in the creation response (whsec_…). Deliveries are signed with it: each POST carries X-JE-Timestamp (unix seconds) and X-JE-Signature (sha256=hex of HMAC-SHA256 over "<timestamp>.<rawBody>"). Reject deliveries whose timestamp is outside a ~5-minute window.
  - `created_at` string, date-time

## Other responses

- `400`: Error envelope: {code, title, details}.
- `401`: Error envelope: {code, title, details}.
- `503`: Error envelope: {code, title, details}.

---

[API](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api.md) · [All operations](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/revisions/34d89cb7a7cd?raw)
