# Webhooks

Source: https://codeherder.com/docs/webhooks/

Receive a signed JSON payload whenever chosen events happen in your workspace.

A **webhook** lets an external system react to activity in your CodeHerder workspace in real time. You register an HTTPS URL and choose which event types you care about; CodeHerder POSTs a signed JSON payload to that URL each time a matching event occurs.

Typical uses: trigger a Slack notification when a task is blocked, fire a CI pipeline when a task is done, push cost data to a billing dashboard, page on-call when a webhook itself goes silent.

This page covers CodeHerder sending payloads out to you. If you want the other direction, an external system sending CodeHerder a payload that comments on a task, creates one, or reports a merge outcome, see [Inbound webhooks](https://codeherder.com/docs/inbound-webhooks/) instead.

## What it does (and does not do)

A webhook subscription is a **URL + event filter + signing secret**. CodeHerder delivers at-least-once — your endpoint may receive the same event more than once (automatic retries can re-send a delivery after a transient failure). It is your endpoint’s responsibility to handle duplicates gracefully, typically by recording which delivery IDs or event IDs you have already processed.

Delivery is fully asynchronous and never blocks the action that triggered the event. If your endpoint is slow or unavailable, CodeHerder queues retries automatically — see [Retries and delivery guarantees](https://codeherder.com/docs/webhooks/#retries-and-delivery-guarantees) below for the schedule.

## Who can manage webhooks

Webhooks are available on the Starter plan and above.

- **Owners and admins** can create, edit, delete, test, redeliver missed events for, and enable or disable (pause/resume) webhook subscriptions.
- **Members** can view the webhook list and delivery history, but cannot make changes. Members see each endpoint URL shortened to its scheme and host, with the hint “Only admins see the full URL”. The CLI shows the same shortened URL.

## Creating a webhook

### In the web app

Open **Settings → Webhooks** and click **+ New webhook**.

Fill in:

- **Endpoint URL** — an `https://` URL that accepts POST requests. Internal or private-network URLs are not accepted.
- **Description** (optional) — a human label for the subscription.
- **Event types** — check each event you want to receive, or click **Select all**. Selecting all is fine — your endpoint receives everything and filters on its end. See the [Event catalog](https://codeherder.com/docs/webhooks/#event-catalog) below for the full list.
- **Delivery format** — **CodeHerder (default)** or **OCSF**. See [Choosing what a delivery contains](https://codeherder.com/docs/webhooks/#choosing-what-a-delivery-contains).
- **Payload** — **Metadata only (default)** or **Full**. With metadata only, deliveries carry no event details.

After you click **Create webhook**, a banner appears showing your **signing secret**. Copy it immediately — it is shown exactly once and cannot be retrieved later. If you lose it, delete the subscription and create a new one.

### From the CLI

```
ch webhook create --url https://example.com/hooks/codeherder \
                  --events task.created,task.status_changed \
                  [--description "My webhook"] \
                  [--payload-mode full] [--format ocsf]
```

The create command prints the signing secret once. Store it securely (for example, as an environment variable on your receiving server).

`--payload-mode` takes `metadata_only` (the default) or `full`. `--format` takes `codeherder` (the default) or `ocsf`. Without `--payload-mode full`, your endpoint receives no event details. See [Choosing what a delivery contains](https://codeherder.com/docs/webhooks/#choosing-what-a-delivery-contains).

`--events` takes a comma-separated list and is the primary way to specify event types. `--event <type>` is the same flag’s singular spelling — repeat it instead if you prefer.

## Choosing what a delivery contains

Two settings shape each delivery. You set both when you create the subscription. Change them later with **Edit**, or `ch webhook edit <id> --payload-mode full`.

**Payload.** A new subscription sends **metadata only**. The delivery says which event happened, when, where and who caused it. Its `payload` is `null`, so no event details leave CodeHerder. Choose **Full** if your receiver needs those details, such as the new status in `task.status_changed`. The [Event catalog](https://codeherder.com/docs/webhooks/#event-catalog) lists the keys each event carries under the full payload. An existing subscription keeps the mode it has.

**Format.** The default body is the CodeHerder envelope described in [What you receive](https://codeherder.com/docs/webhooks/#what-you-receive). Choose **OCSF** to send an OCSF 1.1.0 “API Activity” record, which security tools (SIEMs) can ingest directly. Headers and signing are the same for both formats. See [OCSF format](https://codeherder.com/docs/webhooks/#ocsf-format).

## Test your endpoint

Before you rely on a webhook, send it a one-off test delivery to confirm your endpoint is reachable and your signature verification works.

- **In the web app:** click **Test** on the subscription’s row (Settings → Webhooks).
- **From the CLI:** `ch webhook test <id>`. Admin only.

You can’t test a disabled webhook. The **Test** button is greyed out with the hint “Enable it to send a test delivery”, and the CLI refuses the request. Enable the subscription first.

A test delivery is a real sample of what the subscription actually receives: same envelope, same headers, same signature as a live delivery. Its `eventType` is `webhook.test`, and under the full payload the message lands at `payload.message` — for example, `{"message": "test delivery for subscription <id>"}`. On a metadata-only subscription, `payload` is `null`, so there is no message to read. See [What you receive](https://codeherder.com/docs/webhooks/#what-you-receive) for the full body shape.

`webhook.test` is not one of the subscribable event types — it never appears in `ch webhook event-types` and you cannot add it to an event filter. It exists only as the event type the Test action sends.

## The signing secret — copy it now

Every subscription gets a unique signing secret, shown **exactly once** when the webhook is created. CodeHerder uses this secret to sign every delivery so your endpoint can verify the payload came from CodeHerder and was not tampered with.

**It is never shown again.** If you lose it:

1. Delete the subscription (Settings → Webhooks → Delete).
2. Create a new subscription with the same settings.
3. Copy the new secret.

See [Verifying deliveries](https://codeherder.com/docs/webhooks/#verifying-deliveries) below for how to check signatures on incoming requests.

## Event catalog

Run `ch webhook event-types` for the full, always-current list. The most commonly used subscribable event types:

| Event type | When it fires | Payload keys |
| --- | --- | --- |
| `task.created` | A new task is created in the workspace. | `title`, `priority`, `type` |
| `task.status_changed` | A task’s status changes (including terminal completion when `to` is `"done"`). | `from`, `to`, `title` †, `reason` † |
| `task.blocker_filed` | A blocker note is filed against a task. | `taskId`, `reason`, `blockedOnTaskId` † |
| `task.blocker_resolved` | A blocker note on a task is resolved. | `blockerId`, `taskId` |
| `task.advance_requested` | A stage-advance request is filed and is pending approval. | `to`, `requestedBy`, `approvalJson` |
| `task.advance_approved` | A pending stage-advance request is approved and the task moves to the new stage. | `approvedBy`, `newStatus` |
| `task.advance_rejected` | A pending stage-advance request is rejected by someone other than the requester; the task stays put. | `actorMemberId`, `isWithdrawal`, `reason` † |
| `task.advance_withdrawn` | The requester withdraws their own pending stage-advance request; the task stays put. | `actorMemberId`, `isWithdrawal`, `reason` † |
| `session.lost` | A session is lost due to abnormal termination (heartbeat timeout, tunnel disconnect, or its sandbox already retired). | `from`, `to`, `reason` † |
| `session.exited` | A session exits cleanly with an exit code. | `from`, `to`, `reason` † |
| `session.skills_injected` | A device reports the outcome of injecting this workspace’s enabled skills into a session’s worktree at spawn. | `harness`, `skillsDir`, `unsupportedHarness`, `enabled`, `linked`, `alreadyLinked`, `copied`, `skippedOverride`, `gcRemoved`, `skills`, `error` † |
| `device.archived` | A device is archived. | `ownerHumanId` |
| `device.restored` | An archived device is restored. | `ownerHumanId` |
| `device.token_minted` | A device registration token is minted. | `tokenId`, `ownerHumanId`, `byOwner`, `viaRotate` † |
| `device.token_revoked` | A device registration token is revoked. | `tokenId`, `reason` |
| `device.workspace_linked` | A device is linked to an additional workspace. | `linkedWorkspaceId` |
| `device.workspace_unlinked` | A device is unlinked from a workspace. | `unlinkedWorkspaceId` |
| `device.enabled` | A device is re-enabled by an operator. | `disabled` |
| `device.disabled` | A device is disabled by an operator. | `disabled` |
| `device.secrets_acknowledged` | A device owner sets or replaces the device’s acknowledged-secrets consent list (`ch device secrets set`). | `acknowledgedSecrets` |
| `agent.created` | An agent member is created (directly, or via a copy of another agent). | `displayName`, `role`, `capabilities`, `mintedKey` †, `deviceRequirements` †, `copiedFromAgentId` †, `copiedFromWorkspaceId` †, `credentialRefsCopied` †, `credentialRefsDropped` † |
| `agent.archived` | An agent member is archived. | `displayName` |
| `agent.restored` | An archived agent member is restored. | `displayName` |
| `api_key.minted` | An API key is minted for a member — a personal token or a device-registration token. | `memberId`, `reason` †, `name` †, `deviceId` †, `scope` †, `runId` †, `taskSessionId` †, `taskId` † |
| `api_key.revoked` | An API key is revoked — manually, or when a session leaves the active state. | `reason`, `memberId` †, `name` †, `deviceId` †, `devSessionId` †, `runId` †, `taskSessionId` † |
| `cost.recorded` | A cost batch is ingested; one event per distinct workspace in the batch, attributed to a member or task. | `batchTurns`, `usdMicros`, `sessions`, `sessionUuid` † |
| `cost.unpriced_detected` | A cost batch includes turns for models that have no pricing row. | `unpricedModels`, `turnCount` |
| `human.federated_provisioned` | A person signs in for the first time through an external single sign-on provider and CodeHerder provisions their account. | `identityProvider`, `emailDomain` |
| `member.invited` | A member is invited to a workspace. | `email`, `displayName`, `role` |
| `invitation.accepted` | A workspace invitation is accepted. | `email`, `role` |
| `invitation.created` | A workspace invitation is created. | `email`, `role` |
| `secret.created` | A workspace secret is created. | `name`, `version` |
| `secret.deleted` | A workspace secret is deleted. | `name`, `version` |
| `secret.rotated` | A workspace secret’s value is rotated. | `name`, `version` |
| `team.member_added` | A member is added to a team. | `memberId`, `displayName`, `role` |
| `team.member_removed` | A member is removed from a team. | `memberId` |
| `team.member_role_changed` | A team member’s role is changed. | `memberId`, `role` |
| `workspace.alert_fired` | An alert rule’s sustained breach opens a new incident. | `alertRuleId`, `incidentId`, `metric`, `threshold`, `observedValue`, `sampleCount`, `windowSince`, `windowUntil`, `scope` |
| `workspace.alert_resolved` | An alert rule’s open incident resolves. | `alertRuleId`, `incidentId`, `metric`, `observedValue`, `openedAt`, `durationMillis` |
| `workspace.archived` | A workspace is archived. | `name` |
| `workspace.created` | A workspace is created. | `name` |
| `workspace.member_role_changed` | A workspace member’s role is changed. | `memberId`, `role` |
| `workspace.restored` | An archived workspace is restored. | `name` |
| `webhook.disabled` | A webhook subscription is automatically disabled after consecutive delivery failures. | `reason`, `consecutiveFailures`, `lastResponseCode` † |

† Optional — this key may be absent from the payload in some deliveries.

The catalog shows the payload keys under the full payload. A metadata-only subscription receives none of them.

`secret.*` and `api_key.*` payloads never carry the secret’s plaintext, ciphertext, or the API key’s token/hash — only ids, names, and a `reason` enum. The `email`, `displayName` and `emailDomain` keys (in `member.invited`, `team.member_added`, `invitation.*`, `agent.*` and `human.federated_provisioned`) always arrive as `[redacted]`. See [Payload redaction](https://codeherder.com/docs/webhooks/#payload-redaction).

`task.assigned` is retired — it was never emitted (task assignment dropped the underlying column long ago), doesn’t appear in `ch webhook event-types` or the event picker, and can no longer be added to a subscription’s filter.

Subscribe to `webhook.disabled` on a *separate* monitoring webhook to alert on-call when any webhook in your workspace goes dead.

Related reading: [Following a live agent session](https://codeherder.com/docs/following-a-live-session/) and [Monitoring your agents](https://codeherder.com/docs/monitoring-agents/) cover the session activity behind `session.lost` and `session.exited`; [Skills](https://codeherder.com/docs/skills/) covers the delivery report behind `session.skills_injected`; [Understanding costs](https://codeherder.com/docs/costs/) covers the numbers behind `cost.recorded` and `cost.unpriced_detected`. If you’re looking to connect a specific external service like Slack or PagerDuty directly, rather than receiving a generic event payload, see [Integrations](https://codeherder.com/docs/integrations/).

### Approval-gate events

The `task.advance_*` events are the lifecycle of a single approval gate: `task.advance_requested` fires when CodeHerder parks a task waiting on your approval, then exactly one of `task.advance_approved` or `task.advance_rejected` fires once someone acts on it. Subscribing to that pair is how you mirror an approval queue into Slack or an on-call tool instead of checking `ch task list --awaiting-approval` yourself.

`task.advance_withdrawn` and the `isWithdrawal` payload key are reserved for a requester pulling back their own pending request. Every pending advance today is filed by CodeHerder itself, never by a person, so this event doesn’t fire — you’ll see it in the event picker, but you can’t rely on it turning up in a delivery. See [Approvals & staying in control](https://codeherder.com/docs/approvals/) for how gates and pending advances work.

## What you receive

CodeHerder sends an HTTPS POST to your endpoint with the following headers on every delivery:

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `CodeHerder-Webhooks/1` |
| `X-CodeHerder-Timestamp` | Decimal Unix timestamp (seconds) of this delivery attempt. Used to verify the signature and guard against replays. |
| `X-CodeHerder-Signature` | HMAC-SHA256 signature in the form `sha256=<hex>`. |
| `X-CodeHerder-Event-Id` | UUID of the event row — use this as your **idempotency key** to deduplicate at-least-once deliveries. |
| `X-CodeHerder-Event-Type` | The event type string (e.g. `task.created`). |
| `X-CodeHerder-Delivery-Id` | UUID of this specific delivery attempt. |

The request body is a JSON object with these top-level keys:

| Key | Description |
| --- | --- |
| `deliveryId` | UUID of this delivery attempt (same as `X-CodeHerder-Delivery-Id`). |
| `id` | UUID of the event row (same as `X-CodeHerder-Event-Id` — your idempotency key). |
| `eventType` | Event type string. |
| `workspaceId` | UUID of the workspace where the event occurred. |
| `subjectType` | Class of the subject (e.g. `task`, `session`, `member`, `webhook`). |
| `subjectId` | UUID of the subject. |
| `actorMemberId` | UUID of the member who triggered the event; empty string for system-emitted events. |
| `createdAt` | RFC 3339 timestamp of when the event was recorded. |
| `payload` | Per-event-type object under the full payload; `null` under metadata only. Keys vary by type; see the [Event catalog](https://codeherder.com/docs/webhooks/#event-catalog) above. |

### OCSF format

With the **OCSF** format, the body is a single OCSF “API Activity” record (`class_uid` 6003) instead of the envelope above. The main fields:

| Field | Value |
| --- | --- |
| `activity_name`, `api.operation`, `metadata.event_code` | The event type, such as `task.created`. |
| `metadata.uid` | UUID of the event. Use it to deduplicate. |
| `metadata.correlation_uid` | UUID of this delivery. |
| `time` | When the event happened, in Unix **milliseconds**. |
| `actor.user.uid` | UUID of the member who caused the event. |
| `actor.invoked_by` | `codeherder` when the system caused the event and no member did. |
| `resources` | The event’s subject and the workspace. |
| `unmapped` | The event’s payload. Present only under the full payload; absent under metadata only. |

### Payload redaction

A metadata-only subscription sends no payload, so nothing needs redacting. Under the full payload, CodeHerder cleans the payload before signing and sending it. It swaps these values for `[redacted]`:

- Any value that looks like a credential: an API key, an access token, or a private key.
- Any value under the keys `email`, `displayName` or `emailDomain`, at any depth.
- Any text value that contains an email address. The whole value is replaced, not only the address.

The key names always stay intact. If you see `[redacted]` in a delivery, CodeHerder held back a secret or personal detail on purpose. The payload is not corrupted.

## Verifying deliveries

Every delivery is signed with HMAC-SHA256 using the subscription’s signing secret. Verify the signature **before** processing the payload to confirm the request came from CodeHerder and was not tampered with.

**How the signature is computed:**

1. Read `X-CodeHerder-Timestamp` — a decimal Unix timestamp in seconds.
2. Form the signed input: the timestamp string, a literal `.`, then the raw request body bytes.
3. Compute HMAC-SHA256 over that input using your signing secret as the key.
4. The expected signature is `sha256=` followed by the lowercase hex digest.
5. Compare with `X-CodeHerder-Signature` using a **constant-time** comparison to prevent timing attacks.

**Replay protection:** reject any delivery where `|now − timestamp| > 300` seconds. This prevents old captures from being replayed against your endpoint.

A language-neutral recipe:

```
secret = "<your signing secret>"
ts     = request.headers["X-CodeHerder-Timestamp"]   # e.g. "1749905263"
body   = request.rawBody                               # raw bytes, before any JSON parsing

signed_input = ts + "." + body                        # concat as bytes: timestamp string + "." + body
expected     = "sha256=" + hmac_sha256_hex(key=secret, msg=signed_input)

if not constant_time_equal(expected, request.headers["X-CodeHerder-Signature"]):
    return 401  # reject: invalid signature

if abs(now_unix() - int(ts)) > 300:
    return 400  # reject: stale delivery (replay protection)
```

## Retries and delivery guarantees

If your endpoint doesn’t respond successfully, CodeHerder retries the delivery automatically:

- Up to **6 attempts** total — the initial attempt plus 5 retries.
- Each retry waits longer than the last: the first retry is about 30 seconds after the initial attempt, roughly doubling after that, up to a cap of 1 hour.
- A `5xx` response, a timeout, a connection error, or a `429 Too Many Requests` response is retried. If your endpoint sends a `Retry-After` header on a `429`, CodeHerder waits at least that long before trying again.
- Any other `4xx` response is treated as permanent — CodeHerder stops retrying that delivery right away, since the same request would fail the same way again.

Once every attempt is used up, the delivery is marked `failed`. See [Auto-disable behavior](https://codeherder.com/docs/webhooks/#auto-disable-behavior) below for what happens when failures keep piling up across many deliveries.

## Viewing delivery history

Open **Settings → Webhooks** and click **Log** on any row to see recent delivery attempts for that subscription. The page opens on the Outbound view; the switch at the top also offers Inbound. Each row shows:

- **Event type** — what triggered the delivery.
- **Status** — the web app shows **Queued**, **Retrying**, **Delivered**, **Failed**, or **Discarded**. The CLI shows the same states as `pending` (Queued and Retrying), `delivered`, `failed`, and `dead` (Discarded: the subscription was disabled or deleted before CodeHerder could send the delivery).
- **Code** — the HTTP status your endpoint returned (empty if the request never reached it).
- **Attempts** — how many times CodeHerder tried.
- **Created** — when the delivery was first enqueued.

CodeHerder keeps delivery history for **30 days**: `delivered`, `failed`, and `dead` deliveries older than that are removed automatically. A `pending` delivery is never removed by this cleanup — it stays until it resolves to one of those terminal states.

### When a delivery fails for good

Once a delivery exhausts its retry budget (see [Retries and delivery guarantees](https://codeherder.com/docs/webhooks/#retries-and-delivery-guarantees) above), or hits a permanent non- `429` `4xx` response, it’s marked `failed` and CodeHerder does not send it again on its own. You can replay it by hand. See [Redelivering missed events](https://codeherder.com/docs/webhooks/#redelivering-missed-events).

To recover:

1. Fix whatever was wrong with your endpoint.
2. Run `ch webhook redeliver <id>`. It re-enables the subscription if needed and sends the events your endpoint missed.
3. Watch the result with `ch webhook deliveries <id>`.

If you only want to check that your endpoint is reachable first, run `ch webhook test <id>` (or click **Test** in the web app). This sends a `webhook.test` payload. It does not re-send the delivery that failed.

## Redelivering missed events

`ch webhook redeliver <id> [--since <t>]` replays what a subscription missed. It’s available from the CLI only, and only to owners and admins. The web app has no redeliver button.

One run does three things, in order:

1. Re-enables the subscription if it is disabled.
2. Puts its `failed` and `dead` deliveries back in the queue, each with a fresh set of attempts.
3. Queues a delivery for each subscribed event that has none yet, such as events that happened while the webhook was disabled.

By default, CodeHerder queues missed events since the last successful delivery (or since you created the subscription), and requeues every failed or discarded delivery from the last 30 days. To set the window yourself, pass `--since` with a duration such as `7d`, `12h` or `30m`, or with an RFC 3339 time. `--since` bounds both parts:

```
ch webhook redeliver <id> --since 2d
```

Things to know:

- **30-day limit.** Redelivery never reaches back more than 30 days, even if you ask for more. A `--since` time in the future is refused.
- **10,000 events per run.** If more events are waiting, the command stops and prints “Stopped at the per-call cap. Run the same command again to continue.” Run it again to carry on.
- **Safe to rerun.** CodeHerder never re-sends an event your endpoint already received successfully.
- **Order and repeats.** A redelivered event keeps its original event ID. A missed event gets a new delivery ID; a retried failed delivery keeps its old one. Each send has a fresh timestamp and signature. The `createdAt` field keeps the original event time. Events can arrive out of order, and you may see one you already processed. Deduplicate on the event ID, as described in [What you receive](https://codeherder.com/docs/webhooks/#what-you-receive).

## Pausing and resuming a webhook

You don’t need to delete a subscription to stop it temporarily. Disabling a subscription keeps its URL, event filter, and signing secret intact — it just stops deliveries until you turn it back on.

- **In the web app:** open **Settings → Webhooks**, click **Edit** on the subscription, and clear the **Enabled** checkbox. Check it again to resume.
- **From the CLI:** `ch webhook disable <id>` to pause, `ch webhook enable <id>` to resume.

Re-enabling a subscription this way also clears any auto-disabled state — see [Auto-disable behavior](https://codeherder.com/docs/webhooks/#auto-disable-behavior) below.

Events that happen while a webhook is paused are not sent when you resume it. To replay them, run `ch webhook redeliver <id>`. See [Redelivering missed events](https://codeherder.com/docs/webhooks/#redelivering-missed-events).

## Managing webhooks from the CLI

Everything above is also available from the command line:

- `ch webhook list` — list the subscriptions in your workspace.
- `ch webhook show <id>` — show one subscription, including its health and consecutive-failure count (alias: `get`).
- `ch webhook edit <id> [--url ...] [--events ...] [--description ...] [--format ...] [--payload-mode ...]` — change the URL, event filter, description, format, or payload mode on a subscription (alias: `update`).
- `ch webhook enable <id>` / `ch webhook disable <id>` — resume or pause a subscription. See [Pausing and resuming a webhook](https://codeherder.com/docs/webhooks/#pausing-and-resuming-a-webhook) above.
- `ch webhook delete <id>` — delete a subscription.
- `ch webhook deliveries <id> [--limit N]` — recent delivery attempts, the CLI equivalent of the **Log** view above.
- `ch webhook test <id>` — send a test delivery. See [Test your endpoint](https://codeherder.com/docs/webhooks/#test-your-endpoint).
- `ch webhook redeliver <id> [--since <t>]` — re-enable a subscription and replay the events it missed. See [Redelivering missed events](https://codeherder.com/docs/webhooks/#redelivering-missed-events).

## Auto-disable behavior

CodeHerder automatically disables a webhook subscription after **15 consecutive terminal failures**. A failure counts as terminal right away on a non- `429` `4xx` response, or once a delivery runs out of retry attempts — so a persistently `5xx`, timing-out, or unreachable endpoint counts toward the same threshold, not just 4xx responses. The **Health** column in Settings → Webhooks shows **Healthy**, **Failing (N)** with the current failure count, or **Disabled**. Hover over **Disabled** to see whether CodeHerder or an admin disabled it.

If your endpoint ever responds with **`410 Gone`**, CodeHerder disables the subscription immediately, regardless of its failure count — a 410 tells CodeHerder the endpoint is gone for good, so there’s no point counting toward the 15-failure threshold first.

**How you hear about it.** When CodeHerder auto-disables a webhook, every admin and owner of the workspace gets a message in their [inbox](https://codeherder.com/docs/messages/). Admins with a verified email address also get an email, if your CodeHerder installation sends email. The alert names the subscription, the endpoint’s host (never the full URL) and the failure count. The email also includes the last response code. It doesn’t travel through the failed webhook, so you get it even if that webhook was your only alert channel.

Once a subscription is auto-disabled:

1. Fix whatever was causing the failures at your endpoint.
2. Run `ch webhook redeliver <id>`. It re-enables the *same* subscription and replays the events it missed. See [Redelivering missed events](https://codeherder.com/docs/webhooks/#redelivering-missed-events). If you don’t want a replay, use `ch webhook enable <id>` instead, or in the web app, **Settings → Webhooks → Edit** and check **Enabled**.

Re-enabling clears the disabled state and resets the consecutive-failure count to 0. It keeps the subscription’s existing signing secret and event filter, so there’s nothing to update on your receiving end. If the endpoint is still broken, CodeHerder will auto-disable it again after another 15 consecutive failures.

If you’ve lost the signing secret, re-enabling won’t help — that’s the one case where you still need to delete the subscription and create a new one (see [The signing secret — copy it now](https://codeherder.com/docs/webhooks/#the-signing-secret--copy-it-now) above), which issues a new secret.

A `webhook.disabled` event is also emitted when a subscription is auto-disabled. You can subscribe to this type on a *separate* monitoring webhook to alert an on-call tool as well as the inbox message.
