---
title: "Update a webhook subscription"
method: PATCH
path: "/v1/webhooks/{webhook_id}"
tags: ["Webhooks"]
deprecated: true
---

# Update a webhook subscription

`PATCH /v1/webhooks/{webhook_id}`

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/webhooks/{webhook_id}`
- Auth: HTTP bearer

> **Deprecated.**

**Deprecated.** Use `PATCH /v1/accounts/{account_id}/webhooks/{webhook_id}`, which behaves identically.

Updates the fields present in the body — `url`, `events`, `active`,
`account_relationship` — and leaves the rest untouched.

- **`events`:** replaces the whole list, it does not add to it, so an event left out
  of it stops being delivered.
- **`active`:** setting it to `false` stops deliveries without discarding the delivery
  history. A subscription we turned off ourselves (`deactivated_by: beel`) needs a
  successful test delivery before it can be turned back on. Turning a subscription
  back on counts towards the limit of 10 active subscriptions, and is rejected with
  `400` `WEBHOOK_ACTIVE_SUBSCRIPTION_LIMIT_REACHED` when ten are already active.
- **Signing secret:** not touched here. Rotate it with
  `POST /v1/accounts/{account_id}/webhooks/{webhook_id}/secret`.

**Retires on 9 December 2026.** See the [migration guide](https://docs.beel.es/changelog/resources-under-the-nif) for what moved where and what changes when you switch.

## Path parameters

- `webhook_id` string, uuid, required

## Request body

- UpdateWebhookSubscriptionRequest
  - `url` string, uri, nullable: New HTTPS endpoint URL.
  - `events` WebhookEventTypeEnum[], nullable: New list of event types to subscribe to.
  - `active` boolean, nullable: Enable or disable the webhook subscription.
  - `account_relationship` 'own' | 'managed' | 'all': Which accounts a subscription receives events from — the same vocabulary as the `account_relationship` field in every delivered event envelope. One term to ask for events, the same term to route them on arrival. * `own` (default) — only your own account. * `managed` — only accounts you manage (accounts you provisioned). Whether you actually receive them also depends on the management relationship granting data visibility; a billing-only relationship does not. * `all` — both. A delivered event is always `own` or `managed` (never `all`): its `account_relationship`, alongside `account_id` and `account_external_ref`, tells you which account it belongs to.

## Response `200`

Webhook subscription updated

- object: The envelope every successful JSON response of the BeeL. API is wrapped in. There are no bare resources in v1 and none are planned: the payload always hangs off `data`. - A **single resource** is an object in `data`. - A **collection** hangs off a named key inside `data`, together with its `pagination`, also inside `data` — `data: {invoices: [...], pagination: {...}}`. - A collection carries `pagination` unless its operation declares itself a **closed catalogue**: a fixed, bounded list with nothing to page through. The declaration is explicit in the operation; a missing `pagination` is never something to infer. - `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a documented variant of pagination, not another envelope. Putting the array straight into `data` with `pagination` as its sibling is the shape a v2 would adopt; v1 is not being flipped to it.
  - `success` boolean, required
  - `data` object, required: The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
    - `id` string, uuid, required
    - `url` string, uri, required
    - `events` string[], required
    - `active` boolean, required: Whether the subscription receives events. Turning it off, or BeeL. pausing it, drops the deliveries still waiting for an automatic retry: they are not sent when it is turned back on, and stay in the delivery log for a manual retry.
    - `account_relationship` 'own' | 'managed' | 'all': Which accounts a subscription receives events from — the same vocabulary as the `account_relationship` field in every delivered event envelope. One term to ask for events, the same term to route them on arrival. * `own` (default) — only your own account. * `managed` — only accounts you manage (accounts you provisioned). Whether you actually receive them also depends on the management relationship granting data visibility; a billing-only relationship does not. * `all` — both. A delivered event is always `own` or `managed` (never `all`): its `account_relationship`, alongside `account_id` and `account_external_ref`, tells you which account it belongs to.
    - `deactivated_by` 'owner' | 'beel', nullable: Who turned a subscription off, alongside `active: false`. * `owner` — you did, through the API or the dashboard. Turn it back on whenever you want. * `beel` — we did, because the endpoint stopped accepting anything at all for days. Reactivating requires the endpoint to pass a test delivery first (`POST /v1/webhooks/{webhook_id}/test`), so you unblock yourself without opening a ticket.
    - `deactivated_at` string, date-time, nullable: When the subscription was turned off. Null while it is active.
    - `last_error` string, nullable: Last error seen in the current run of failed deliveries — what to fix. Cleared by the first successful delivery, together with the failure count.
    - `last_error_cause` 'dns' | 'connection' | 'tls' | 'timeout' | 'http_client_error' | 'http_server_error' | 'invalid_url' | 'forbidden_target' | 'unknown', nullable: Why a delivery to your endpoint did not succeed. The `error` field is free text meant for a human reading a log; this field is the machine-readable counterpart, so a change in wording never changes your branching. * `dns` — the endpoint's domain does not resolve. * `connection` — the connection could not be opened (refused, host unreachable). * `tls` — the endpoint's TLS certificate is not valid or not trusted. * `timeout` — the endpoint accepted the connection but did not answer in time. * `http_client_error` — the endpoint answered 4xx. * `http_server_error` — the endpoint answered 5xx. * `invalid_url` — the subscription URL is malformed and has no host to deliver to. * `forbidden_target` — the URL points to a non-public network address, which we never deliver to. * `unknown` — the failure could not be classified. New values may be added over time. Treat any value you do not recognise as `unknown`.
    - `consecutive_failures` integer: Deliveries in a row that never got through. A delivery that exhausted its attempts counts as one. Any successful delivery resets it to zero, so a healthy endpoint with occasional blips stays near zero.
    - `last_used_at` string, date-time, nullable: Last delivery that succeeded. Null if none ever has.
    - `created_at` string, date-time, required
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `400`: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in `error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production. `BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not admit in its current state when that state has no code of its own. It carries no `details`; read the resource again before retrying.
- `401`: Missing or invalid authentication. Like every other error, `message`/`detail` is localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the header is missing or asks for none of those.
- `403`: Authenticated but not allowed. Fourteen causes shared by every operation, told apart by `error.code`; an operation that adds a cause of its own names it in its `403`. You can branch on them exhaustively. - `INSUFFICIENT_SCOPE` — the credential lacks a scope the operation requires; `error.details.missing_scopes` names them as a single comma-separated string (for example `"invoices:write,customers:read"`), not as an array; `required_scopes` has the same shape and lists every scope the operation needs. Retrying will not help: mint a key that holds them. - `COMPANY_READ_ONLY` — the scope is there, but your access level over that NIF only lets you read it. - `ACCOUNT_MANAGEMENT_FORBIDDEN` — the scope is there, but your role over the account, or the management relationship you hold over it, does not cover this operation. - `ACCOUNT_NOT_ACCESSIBLE` — the account is not yours to reach, which is also the answer when it does not exist, so existence is never disclosed. - `ACTIVE_COMPANY_NOT_ACCESSIBLE` — the same, for a NIF: the company in the path, or the one named by `BeeL-Active-Company`, is not one this credential may reach — and again, this is also the answer when it does not exist. - `COMPANY_ACCESS_REVOKED` — your access to the NIF was withdrawn while the request was already in flight, so the write was rejected and nothing was recorded. Retrying will not help until the access is granted again. - `LIVE_CREDENTIAL_REQUIRED` — the operation changes the real account and the call came from a test API key (`beel_sk_test_…`). Use your live key or the dashboard. - `FEATURE_NOT_AVAILABLE` — your subscription does not include the entitlement the operation needs; `error.details.feature_code` names it. This gate runs **before** the scope gate, so for such an operation you never see `INSUFFICIENT_SCOPE` first. - `NO_ACTIVE_ACCOUNT` — the credential does not resolve to an account, so no scope can be evaluated against one. Fail-closed, not a permission that can be granted to you. - `OPERATION_REQUIRES_SESSION` — the operation is available only from the web session; no API key and no OAuth2 token can perform it, whatever scopes it holds. - `OPERATION_REQUIRES_API_KEY` — the reverse: the operation needs a scope that only an API key can hold (such as `accounts:write`), so a dashboard session never satisfies it. - `API_KEY_LIVE_NOT_AVAILABLE` — the call came with a live API key (`beel_sk_live_…`) and the account has no Live mode yet. It is never degraded to Test: use your test key, or activate Live first. - `ACCOUNT_REQUIRES_FOCUS_SWITCH` — dashboard session only: you are a member of the account in the path, but it is not the account in focus. Switch focus to it and retry. - `REQUEST_BLOCKED` — the edge security filter stopped the request before the API saw its body, usually because a text field carries something that looks like markup or a script (HTML tags, `<script>`). Review the body and retry.
- `404`: The resource addressed by the path does not exist, or is not one this credential can see — the two are answered identically, so existence is never disclosed. `error.code` names the kind of resource that was missing, so a client can tell which of several ids in a path failed to resolve: - `INVOICE_NOT_FOUND` — the `{invoice_id}` (invoices, proformas and their sub-resources). - `RECURRING_NOT_FOUND` — the `{recurring_invoice_id}`. - `CLIENT_NOT_FOUND` — the `{customer_id}`. - `INVOICE_CLIENT_NOT_FOUND` — the `customer_id` referenced by an invoice body does not resolve to a customer of the issuing company. - `PRODUCT_NOT_FOUND` — the `{product_id}`. - `SERIES_NOT_FOUND` — the `{series_id}`. - `WEBHOOK_SUBSCRIPTION_NOT_FOUND`, `DELIVERY_LOG_NOT_FOUND` — the `{webhook_id}` and the `{delivery_id}`. - `PROFILE_NOT_FOUND` — the credential's account has no company profile yet: its holder has not completed onboarding, so there is no issuer to act as. - `NOT_FOUND` — the generic fallback, for the few resources that carry no code of their own. Some resources declare a more specific `404` response of their own (`CONNECTION_NOT_FOUND`, `MEMBER_NOT_FOUND`, `GRANT_NOT_FOUND`, `INVITATION_NOT_FOUND`, `REQUEST_LOG_NOT_FOUND`); it is documented on the operation. A `404` with `ENDPOINT_NOT_FOUND` is a different answer: the **path itself** does not exist in this API (a typo in the route, or a resource that was never here). It says nothing about any resource. When the path exists but not with that method, the answer is `405`, not `404`.
- `422`: The body parses but a value is not acceptable. Branch on `error.code`: - `VALIDATION_ERROR` — a field fails validation; `error.details` maps each offending property to what is wrong with it. - `URL_MUST_BE_HTTPS` — `url` does not use `https`. - `WEBHOOK_URL_INVALID` — `url` is not a valid absolute URL. - `URL_TARGET_NOT_ALLOWED` — `url` points to an internal network address: a private, loopback or link-local range, either as a literal IP or through a host name that resolves to one. Use an endpoint reachable from the internet. A host name that does not resolve yet is accepted, and deliveries to an internal address are refused anyway. - `WEBHOOK_REACTIVATION_REQUIRES_LIVE_ENDPOINT` — on update only: reactivating a subscription that BeeL paused sends a test delivery first, and it failed. Fix the endpoint and retry; nothing was changed.
- `default`: Any status code the operation does not list above. Every operation declares it, so a generated client always has a branch to fall into and never loses the cause of a failure it did not anticipate. This is where the transport-level answers land — `405`, `406`, `415` and `429` — together with any status a future version of the API starts returning. All of them carry the same error envelope as the codes listed explicitly, so `error.code` is what tells them apart: switching on the status code alone is not enough. See «Transport-level errors» in the API description for when each one is produced. A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active subscription. `error.details` says why and what to do: `current_status` (absent if it never had one), `required_permission` and `action`. A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a dependency of BeeL itself (the database, the email provider) was momentarily unavailable and nothing was done. Retry after those seconds. A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the operation depends on failed or did not answer in time. It is a transient condition — retry with the same `Idempotency-Key` where the operation accepts one. One exception to the envelope: a failure of the network edge that never reaches the application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not BeeL's — it may not even be JSON. Treat those as "no answer", and retry.

---

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