---
title: "Change a webhook endpoint"
method: PUT
path: "/v1/webhooks/{id}"
tags: ["Webhooks"]
---

# Change a webhook endpoint

`PUT /v1/webhooks/{id}`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/webhooks/{id}`
- Auth: one of:
  - API key in header `X-Api-Key`
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.kawaa.com/oauth/authorize`, token URL `https://api.kawaa.com/oauth/token`, scopes: `webhooks:write`)

Change an endpoint. Only the fields sent are changed. With a dashboard session, changing `url` or `events` needs a sign-in within the last 15 minutes; an older session answers `403 REAUTHENTICATION_REQUIRED`. Sending the URL and events the endpoint already has is not a change. The same goes for `is_active: true` on an endpoint with a `disabled_reason`: turning back on an endpoint Kawaa switched off trusts its destination again. An API key or a connected app is not held to this. `is_active: true` clears `disabled_reason`, and is refused with `409 CONFLICT` if Kawaa switched the endpoint off while the request was in flight (a revoke, password change or lock that landed after it was authenticated); sending it again afterwards turns it on.

**A scoped key needs more than `webhooks:write` here.** A subscription is a way to receive what its events carry, so each requested event also requires the scope that owns that data: `verification.completed` needs `verification:read`, `verification.failed` needs `verification:read`, `job.completed` needs `verification:read`, `job.failed` needs `verification:read`, `credits.low` needs `account:read`, `credits.exhausted` needs `account:read`, `blacklist.detected` needs `deliverability:read`, `dmarc.failure` needs `deliverability:read`. On an account that can have sub-accounts, `credits.low` and `credits.exhausted` additionally require `white_label:read`, because those deliveries name the sub-account and its label. Missing any of them is a `403` naming each event and the scope it needed, and no webhook is created. An unrestricted key needs none of this. `x-kawaa-event-scopes` carries the same table in machine-readable form.

## Path parameters

- `id` string, required

## Request body

- object: Send only what you are changing. The first key that is neither writable nor a known response field is refused with 400, so a misspelling is caught, while a round trip of a fetched endpoint object is deliberately accepted. The five known response fields below are the no-op half of that: present is fine, changing them is not possible. They are intentionally not marked `readOnly`, because generated request models would otherwise omit them and break the supported round trip.
  - `url` string: Where Kawaa POSTs the callback. HTTPS only: anything else is 400 WEBHOOK_URL_INVALID. No `format: uri` or anchored scheme `pattern` is declared on this REQUEST field: the URL is parsed first, which tolerates surrounding whitespace and normalizes the protocol, so `" https://x"` and `"HTTPS://x"` are both accepted while either constraint refuses them. HTTPS is required, checked on the PARSED protocol, not on the string you sent. Before storing it, Kawaa replaces the request string with that parsed URL's canonical string, so every response URL is a well-formed URI.
  - `events` string[]: Omit the field to leave the subscription alone. Sending it EMPTY is not "unsubscribe from everything": the API answers `400 At least one event is required`. Delete the endpoint, or set `is_active: false`, to stop delivery.
  - `description` string, nullable: A note for your own reference. Send `null` to clear the stored one.
  - `is_active` boolean: Pause or reactivate the webhook. The update allow-lists url, description, events and is_active; anything else is a 400. Turning back on an endpoint that has a `disabled_reason` clears it, and from a dashboard session needs a sign-in within the last 15 minutes.
  - `webhook_id` unknown
  - `created_at` unknown
  - `updated_at` unknown
  - `last_triggered_at` unknown
  - `failure_count` unknown
  - `disabled_reason` unknown

## Response `200`

Updated.

- object
  - `success` unknown, required
  - `data` object, required
    - `webhook` object, required: The endpoint as it now stands, including a description cleared in this same request: the response reports the stored value for every field, so there is no need to re-read after an update. No `secret`: a signing secret appears only in the successful response that creates or explicitly rotates it.
      - `webhook_id` string, required
      - `url` string, uri, nullable, required: The canonical callback URL. This is `null` only when the update omitted `url` and the stored legacy destination was malformed; send a valid URL in another PUT to repair it.
      - `description` string, nullable, required
      - `events` string[], required
      - `is_active` boolean, required
      - `disabled_reason` 'api_key_revoked' | 'app_disconnected' | 'null', nullable: As on the listing: why Kawaa switched the endpoint off, or `null`. Sending `is_active: true` clears it.
      - `updated_at` string, date-time, required: When this change landed. The update response carries only this timestamp: `created_at` is not returned here; read it from the listing or the single-webhook route.
    - `message` string, required

## Other responses

- `400`: The request is malformed, or names a field this endpoint does not accept. Retrying it unchanged will fail identically.
- `401`: No credential, or one that is not accepted. `code` distinguishes `API_KEY_SUSPENDED` (subscription ended; restartable) from `API_KEY_REVOKED`, `SESSION_EXPIRED` and `OAUTH_TOKEN_EXPIRED` (a connected app's one-hour access token lapsed: refresh it and retry, or sign in again if no refresh token was issued or the refresh is refused).
- `403`: Refused, and **the code says which of three problems it is**, and they need different remedies: - `INSUFFICIENT_SCOPE`: the credential is valid but not scoped for this route. With an API key, issue one that carries the scope named in the message. With an OAuth access token, connect the app again asking for that scope as well as the ones it has, unless the message says a connected app can never be given it; then use an API key with every scope the message names, or have the account owner do it in the dashboard. - `PLAN_UPGRADE_REQUIRED`: the account's plan does not include the feature. **No key of any scope reaches it**, so issuing another is wasted work; the remedy is a plan change. Returned by the verification routes for `options.enrich`, and by the finder, the monitors, the audit log, auto-clean and the white-label routes. - `PLAN_LIMIT_EXCEEDED`: the plan includes the feature and the account is at its ceiling (webhook endpoints, team seats, integrations, auto-clean lists). Delete one or upgrade. - `FORBIDDEN`: refused for a reason none of the above covers. `ErrorCode` is an open set, so branch on the code with a default rather than exhaustively.
- `404`: No such resource on this account.
- `409`: `CONFLICT`: nothing was changed. One of: the request turns the endpoint on (`is_active: true`), and Kawaa switched it off while the request was in flight because an API key or connected app that created it, or that changed its URL or events or turned it on, was revoked or disconnected (a password change, reset or lock included): check where it sends, then send the request again to turn it on. Or the request changes `url` or `events`, or turns the endpoint on, and the account's credentials changed while it was in flight (a password change, a reset, a lock, a white-label key reset, or for a dashboard session signing out every session): send it again if your credential still works. Or another request changed the endpoint after this one read it: read it again, then send your change again if you still want it.
- `429`: Too many requests. When the server knows how long to wait, `Retry-After` says so and `error.retry_after_seconds` carries the same number; the `X-RateLimit-*` headers say where you stand.
- `503`: The webhook URL's hostname could not be resolved right now (`WEBHOOK_DNS_TEMPORARY_FAILURE`). This is a TRANSIENT DNS failure, not a rejection of the URL; a permanent one is `400 WEBHOOK_URL_INVALID`. Nothing was created or changed. Retry.
- `default`: Any status this operation does not list. Kawaa's own errors carry the error envelope. The gateway in front of the API can answer any operation with its own bare body (`GatewayError`): a `503` after about 29 seconds, when the request may still have run and been charged, a `413` for a body over about 6 MB, or a `400` for a path with malformed percent-encoding.

---

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