---
title: "Webhook Endpoints"
description: "12 operations under /v1/webhook-endpoints (GET POST PATCH DELETE): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript."
---

Base URL: `https://api.codespar.dev`

Every operation below requires a Bearer token. See [Authentication](/docs/concepts/authentication).

### GET `/v1/webhook-endpoints`

<Endpoint method="GET" path="/v1/webhook-endpoints" base="https://api.codespar.dev" />

List triggers

Same cursor shape as /v1/sessions. The signing secret is never included. Each endpoint carries its delivery health: `consecutive_failures` (failed or dead attempts since the last delivered one, not the dead-outcome streak the auto-pause counts), `last_response_status` and `last_delivery_at` of the newest finished attempt.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `before` | `string` | no | — |
| `event` | `string` | no | — |
| `limit` | `integer` | no | — |
| `server_id` | `string` | no | — |
| `status` | `"active"` \| `"paused"` \| `"error"` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `next_before` | `string,null` | yes | — |
| `triggers` | array of — | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/webhook-endpoints \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/webhook-endpoints HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/webhook-endpoints",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/webhook-endpoints");
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "triggers": [],
  "next_before": "string"
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/webhook-endpoints" />

### POST `/v1/webhook-endpoints`

<Endpoint method="POST" path="/v1/webhook-endpoints" base="https://api.codespar.dev" />

Subscribe a webhook to an event

The response is the ONLY time the signing secret is returned in plaintext. `webhook_url` is refused when it names a loopback, private or metadata host; DNS is re-checked at dispatch, so passing here is not a promise it will resolve publicly later.

`event` is validated against a regex, NOT against a list of events the platform emits. A name that matches the shape but that nothing publishes is accepted with 201 and then never fires, with no signal to the subscriber.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | object \| object \| object | no | What the trigger does when it fires. Absent means `webhook`, the signed POST to `webhook_url`. `human_review` puts an item in the approvals inbox; `pause_agent` suspends the agent named by `agent_did`, or the agent the event names when `agent_did` is absent. Neither moves money. `webhook_url` is required for `webhook` and refused for the other two. |
| `condition` | `object,null` | no | Optional. Every clause in `all` must hold for the event to fire this trigger. Which fields an event offers is listed per event by GET /v1/triggers/events (`condition_fields`); a field the event does not offer is refused with `trigger_condition_invalid`. `in` takes a list of strings; the other operators take one value. Without a condition every event of the type fires, as before. |
| `event` | `string` | yes | The event type to subscribe to. Matching is EXACT string equality — there is no prefix and no wildcard, so `commerce.payment.succeeded` does not receive `commerce.payment.received`. A name outside the list below is accepted and recorded (`trigger.event_unknown`) rather than refused, so you can subscribe ahead of a release; until something emits it the subscription simply never fires. What this build emits: approval.decided, approval.pending, collect.attempt.expired, collect.attempt.failed, collect.attempt.issued, collect.attempt.ready, collect.attempt.superseded, collect.link.paused, collect.link.published, collect.payment.exception, collect.payment.paid, collect.refund.awaiting_decision, collect.refund.honored, collect.refund.required, commerce.account_launch_in.received, commerce.account_launch_out.received, commerce.account_status.changed, commerce.cashout.confirmed, commerce.charge.cancelled, commerce.charge.created, commerce.charge.expired, commerce.charge.expiry_notified, commerce.charge.paid, commerce.charge.payment_notified, commerce.dict_claim.cancelled, commerce.dict_claim.completed, commerce.dict_claim.confirmed, commerce.dict_claim.opened, commerce.dict_claim.waiting, commerce.internal_transfer_in.received, commerce.internal_transfer_out.received, commerce.mandate.expiring, commerce.mandate.granted, commerce.mandate.paused, commerce.mandate.resumed, commerce.mandate.revoked, commerce.onboarding.backgroundcheck_approved, commerce.onboarding.backgroundcheck_pending, commerce.onboarding.backgroundcheck_rejected, commerce.onboarding.documentscopy_approved, commerce.onboarding.documentscopy_pending, commerce.onboarding.documentscopy_processing, commerce.onboarding.documentscopy_rejected, commerce.onboarding.proposal_approved, commerce.onboarding.proposal_processing_documentscopy, commerce.onboarding.proposal_rejected, commerce.organization.paused, commerce.organization.resumed, commerce.payment.failed, commerce.payment.pending, commerce.payment.received, commerce.payment.refunded, commerce.payment.succeeded, commerce.payment.updated, commerce.pix_out.failed, commerce.pix_out.succeeded, commerce.pix_out.unconfirmed, commerce.pix_reversal_in.received, commerce.pix_reversal_out.received, commerce.recurrence.authorized, commerce.recurrence.cancelled, commerce.recurrence.cycle.accepted, commerce.recurrence.cycle.announced_late, commerce.recurrence.cycle.awaiting_instruction, commerce.recurrence.cycle.cancelled, commerce.recurrence.cycle.expired, commerce.recurrence.cycle.instruction_blocked, commerce.recurrence.cycle.instruction_missing, commerce.recurrence.cycle.instruction_refused, commerce.recurrence.cycle.paid, commerce.recurrence.cycle.queued_instruction_expired, commerce.recurrence.cycle.rejected, commerce.recurrence.cycle.scheduled, commerce.recurrence.cycle.settled_by_held_credit, commerce.recurrence.denied, commerce.recurrence.requested, commerce.recurrence.settlement_held, commerce.recurrence.settlement_hold_overdue, commerce.recurrence.settlement_hold_resolved, commerce.recurrence.settlement_possible_double_credit, commerce.rinne.observed, commerce.spend.failed, commerce.spend.settled, commerce.ted_in.succeeded, dda.boleto.registered, dda.subscription.activated, dda.subscription.failed, gate.paywall.charged, payable.rejected, proxy_call.failed, proxy_call.succeeded, session.closed, system.health.degraded, system.health.recovered, tool_call.failed, tool_call.succeeded, trigger.delivery.failed, trigger.paused_automatically, trigger.test_fire, user.signed_up, wallet.balance.below. |
| `name` | `string` | yes | — |
| `server_id` | `string` | no | — |
| `webhook_url` | `string (uri)` | no | Required for the `webhook` action (the default), refused for the internal ones. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `201` | [TriggerCreated](/docs/api/reference/schemas#triggercreated) | OK |
| `400` | object | Bad Request — the body or query did not match the schema. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/webhook-endpoints \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "name": "Example",
       "event": "string",
       "server_id": "srv_0000000000000000",
       "webhook_url": "https://example.com/hook",
       "condition": {
         "all": [
           {
             "field": "amount_minor",
             "op": "gt",
             "value": 1000
           }
         ]
       },
       "action": {
         "kind": "webhook"
       }
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/webhook-endpoints HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "name": "Example",
  "event": "string",
  "server_id": "srv_0000000000000000",
  "webhook_url": "https://example.com/hook",
  "condition": {
    "all": [
      {
        "field": "amount_minor",
        "op": "gt",
        "value": 1000
      }
    ]
  },
  "action": {
    "kind": "webhook"
  }
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/webhook-endpoints",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "name": "Example",
      "event": "string",
      "server_id": "srv_0000000000000000",
      "webhook_url": "https://example.com/hook",
      "condition": {
        "all": [
          {
            "field": "amount_minor",
            "op": "gt",
            "value": 1000
          }
        ]
      },
      "action": {
        "kind": "webhook"
      }
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Example",
    "event": "string",
    "server_id": "srv_0000000000000000",
    "webhook_url": "https://example.com/hook",
    "condition": {
      "all": [
        {
          "field": "amount_minor",
          "op": "gt",
          "value": 1000
        }
      ]
    },
    "action": {
      "kind": "webhook"
    }
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/webhook-endpoints", {
  body: {
    name: "Example",
    event: "string",
    server_id: "srv_0000000000000000",
    webhook_url: "https://example.com/hook",
    condition: {
      all: [
        {
          field: "amount_minor",
          op: "gt",
          value: 1000
        }
      ]
    },
    action: {
      kind: "webhook"
    }
  }
});
```

</Tab>
</Tabs>

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/webhook-endpoints" body={"{\"name\":\"Example\",\"event\":\"string\",\"server_id\":\"srv_0000000000000000\",\"webhook_url\":\"https://example.com/hook\",\"condition\":{\"all\":[{\"field\":\"amount_minor\",\"op\":\"gt\",\"value\":1000}]},\"action\":{\"kind\":\"webhook\"}}"} />

### POST `/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver`

<Endpoint method="POST" path="/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver" base="https://api.codespar.dev" />

Queue a delivery to be sent again

Queues a fresh attempt at the SAME event, typically for a dead lettered one. Requires `triggers:write`. The handler reads no request body. Note the path: the delivery id is enough, no endpoint id, because the delivery is resolved by joining its endpoint and filtering on the caller's organization and project.

NOTHING IS SENT BY THIS CALL. It inserts a new delivery row with `attempt = 1`, status `pending` and `next_retry_at = now()`, then answers 202. The POST happens when a drain picks the row up, either the in-process worker or the retry-pending call. Since a drain only claims rows whose endpoint is `active`, and this call deliberately does NOT reactivate a paused or auto-paused endpoint, read `trigger_status` in the response: anything other than `active` means the row will sit unclaimed until a patch reactivates the endpoint.

COUNT A REDELIVERY'S ATTEMPTS FROM 2. The row inserted here is a marker, not an attempt: the drain reads its `attempt` and dispatches at `attempt + 1`, so the first POST a redelivery actually makes carries attempt 2, and the retry ladder is entered at that rung rather than at the top. A redelivery therefore gets FOUR real POSTs, attempts 2 through 5, spaced by the ladder's 5 minute, 30 minute and 2 hour rungs, and the fifth is terminal — it dead letters instead of scheduling a sixth. Redelivering does not hand the event a fresh set of five.

It is NOT idempotent. Two calls queue two attempts at the same event and the subscriber receives it twice, with the same `X-CodeSpar-Event-Id`.

Two side effects on top of the insert: the origin row's `next_retry_at` is cleared so it cannot be claimed as well, and the endpoint's consecutive failure streak is reset to 0, which restarts the auto-pause clock.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `delivery_id` | `string` | yes | Delivery id as decimal digits, the string form of the `bigserial` column. Up to 19 digits, which is the width of a signed 64 bit maximum; anything else is refused with 400 `invalid_delivery_id` before the database is touched. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `202` | object | Accepted |
| `400` | object | `delivery_id` is not 1 to 19 decimal digits. |
| `404` | object | No delivery with that id whose webhook endpoint belongs to the caller's organization and project. |

**Response `202`**

| Field | Type | Required | Description |
|---|---|---|---|
| `event_id` | `string` | yes | — |
| `redelivery_id` | `string` | yes | Id of the NEW pending delivery row, bigint as string. |
| `trigger_id` | `string` | yes | — |
| `trigger_status` | `string` | yes | Status of the endpoint the delivery belongs to, read before the insert. Check it: the drain only claims rows whose endpoint is `active`, so a redelivery queued against a `paused` or `error` endpoint sits there until a patch reactivates it. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/webhook-endpoints/deliveries/{delivery_id}/redeliver HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver", {
  path: {
    delivery_id: "delivery_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 202">

```json title="application/json"
{
  "redelivery_id": "redelivery_0000000000000000",
  "trigger_id": "trg_0000000000000000",
  "event_id": "event_0000000000000000",
  "trigger_status": "string"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/webhook-endpoints/deliveries/{delivery_id}/redeliver" />

### POST `/v1/webhook-endpoints/retry-pending`

<Endpoint method="POST" path="/v1/webhook-endpoints/retry-pending" base="https://api.codespar.dev" />

Drain the retry queue for this project

Dispatches the deliveries that are due for another attempt, right now, instead of waiting for the in-process worker tick. Requires `triggers:write`. The handler reads no request body and takes no parameters.

A row is claimed only when ALL of it holds: its status is `failed` or `pending`, its `next_retry_at` has passed, and its endpoint is `active`. An endpoint that auto-paused into `error` therefore drains nothing until a patch reactivates it. Scope is the caller's organization AND project; one project cannot drain a sibling project's queue.

One call claims at most 100 rows, and the limit is not settable from here, so a backlog larger than that needs repeated calls. The claim uses FOR UPDATE SKIP LOCKED, so calling this while the worker is running is safe: the two get disjoint rows rather than dispatching the same event twice.

The counters describe what happened during the call. `scanned` is what was claimed; a claimed row is dispatched synchronously before this returns, so a large drain is a slow request.

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `dead` | `integer` | yes | Attempts that failed on the last allowed attempt and were dead lettered. |
| `delivered` | `integer` | yes | — |
| `failed` | `integer` | yes | Attempts that failed and are scheduled for another retry. |
| `redispatched` | `integer` | yes | — |
| `scanned` | `integer` | yes | Rows the claim actually took, which is also the ceiling on the four counters below. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/webhook-endpoints/retry-pending \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/webhook-endpoints/retry-pending HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/webhook-endpoints/retry-pending",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/retry-pending", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/webhook-endpoints/retry-pending");
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "scanned": 0,
  "redispatched": 0,
  "delivered": 0,
  "failed": 0,
  "dead": 0
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/webhook-endpoints/retry-pending" />

### GET `/v1/webhook-endpoints/{id}`

<Endpoint method="GET" path="/v1/webhook-endpoints/{id}" base="https://api.codespar.dev" />

Read one webhook endpoint

The stored configuration of one webhook endpoint. Requires `triggers:read`.

The signing secret is never returned; `signing_enabled` reports only whether one exists. An id belonging to another organization, or to another project in the caller's own organization, answers 404 and not 403: both filters are in the WHERE clause, so the handler cannot tell absent from someone else's, and must not, because telling them apart confirms the id exists.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | object \| object \| object | yes | What the trigger does when it fires. Absent means `webhook`, the signed POST to `webhook_url`. `human_review` puts an item in the approvals inbox; `pause_agent` suspends the agent named by `agent_did`, or the agent the event names when `agent_did` is absent. Neither moves money. `webhook_url` is required for `webhook` and refused for the other two. |
| `condition` | `object,null` | yes | Null fires on every event of the type. |
| `created_at` | `string (date-time)` | yes | — |
| `event` | `string` | yes | The event name this endpoint subscribes to, dot separated lowercase. |
| `event_known` | `boolean` | yes | Whether `event` is a type this build emits. `false` means the subscription exists but will not fire until a release starts emitting that name: it was accepted so you can subscribe ahead of a release, not because it matches. |
| `id` | `string` | yes | — |
| `last_run_at` | `string,null (date-time)` | yes | Timestamp of the last DELIVERED attempt, on the same rule as `total_runs`. |
| `name` | `string` | yes | — |
| `org_id` | `string` | yes | — |
| `project_id` | `string,null` | yes | Nullable in the row type this response is serialized from; a later migration sets the column NOT NULL, so an endpoint created since then always carries one. |
| `server_id` | `string,null` | yes | — |
| `signing_enabled` | `boolean` | yes | Whether a signing secret exists. The secret itself is never read back: the serializer drops the vault reference and reports only this boolean. |
| `status` | `string` | yes | `active`, `paused` or `error`. Left as an open string because the column is `text` with no CHECK constraint: `active` and `paused` are what a patch may set, and `error` is what the dispatcher writes on its own when an endpoint auto-pauses after enough consecutive dead deliveries. |
| `total_runs` | `integer` | yes | Deliveries that landed. A failed or dead attempt does not count, which is why this can sit at 0 while the deliveries listing is full of rows. |
| `webhook_url` | `string,null` | yes | Null when the action is internal (`human_review`, `pause_agent`). |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/webhook-endpoints/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/webhook-endpoints/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/webhook-endpoints/{id}", {
  path: {
    id: "webhookendpoint_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "org_id": "org_0000000000000000",
  "project_id": "prj_0000000000000000",
  "name": "Example",
  "event": "string",
  "server_id": "srv_0000000000000000",
  "webhook_url": "https://example.com/hook",
  "status": "string",
  "total_runs": 0,
  "last_run_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "signing_enabled": true,
  "event_known": true,
  "condition": {
    "all": [
      {
        "field": "amount_minor",
        "op": "gt",
        "value": 1000
      }
    ]
  },
  "action": {
    "kind": "webhook"
  }
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/webhook-endpoints/{id}" />

### PATCH `/v1/webhook-endpoints/{id}`

<Endpoint method="PATCH" path="/v1/webhook-endpoints/{id}" base="https://api.codespar.dev" />

Update a webhook endpoint

Changes the name, URL and status of a subscription. Only the fields sent change; the others stay as they are.

The `webhook_url` goes through the same syntax check as the create call: HTTPS, and no loopback, private or metadata host. DNS is rechecked at dispatch, so passing here does not promise that it resolves publicly later.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | object \| object \| object | no | Replace the action. Switching to an internal one drops `webhook_url`; switching to `webhook` needs one. |
| `condition` | `object,null` | no | Replace the condition, or `null` to remove it. Absent leaves it as it is. |
| `name` | `string` | no | — |
| `status` | `"active"` \| `"paused"` | no | Only `active` and `paused` are settable. `error` shows up on read but belongs to the dispatcher: it writes that value on its own when an endpoint auto-pauses after enough consecutive dead deliveries, and a patch cannot reach it. |
| `webhook_url` | `string (uri)` | no | — |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | The body did not match the schema (the empty body included), the `webhook_url` was refused by the syntax check, or the condition names a field the endpoint's event does not carry (`trigger_condition_invalid`), or the action could not run on it (`trigger_action_invalid`); both name the cause in `details.reason`. |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `action` | object \| object \| object | yes | What the trigger does when it fires. Absent means `webhook`, the signed POST to `webhook_url`. `human_review` puts an item in the approvals inbox; `pause_agent` suspends the agent named by `agent_did`, or the agent the event names when `agent_did` is absent. Neither moves money. `webhook_url` is required for `webhook` and refused for the other two. |
| `condition` | `object,null` | yes | Null fires on every event of the type. |
| `created_at` | `string (date-time)` | yes | — |
| `event` | `string` | yes | The event name this endpoint subscribes to, dot separated lowercase. |
| `event_known` | `boolean` | yes | Whether `event` is a type this build emits. `false` means the subscription exists but will not fire until a release starts emitting that name: it was accepted so you can subscribe ahead of a release, not because it matches. |
| `id` | `string` | yes | — |
| `last_run_at` | `string,null (date-time)` | yes | Timestamp of the last DELIVERED attempt, on the same rule as `total_runs`. |
| `name` | `string` | yes | — |
| `org_id` | `string` | yes | — |
| `project_id` | `string,null` | yes | Nullable in the row type this response is serialized from; a later migration sets the column NOT NULL, so an endpoint created since then always carries one. |
| `server_id` | `string,null` | yes | — |
| `signing_enabled` | `boolean` | yes | Whether a signing secret exists. The secret itself is never read back: the serializer drops the vault reference and reports only this boolean. |
| `status` | `string` | yes | `active`, `paused` or `error`. Left as an open string because the column is `text` with no CHECK constraint: `active` and `paused` are what a patch may set, and `error` is what the dispatcher writes on its own when an endpoint auto-pauses after enough consecutive dead deliveries. |
| `total_runs` | `integer` | yes | Deliveries that landed. A failed or dead attempt does not count, which is why this can sit at 0 while the deliveries listing is full of rows. |
| `webhook_url` | `string,null` | yes | Null when the action is internal (`human_review`, `pause_agent`). |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X PATCH https://api.codespar.dev/v1/webhook-endpoints/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "name": "Example",
       "webhook_url": "https://example.com/hook",
       "status": "active",
       "condition": {
         "all": [
           {
             "field": "amount_minor",
             "op": "gt",
             "value": 1000
           }
         ]
       },
       "action": {
         "kind": "webhook"
       }
     }'
```

</Tab>
<Tab value="HTTP">

```http
PATCH /v1/webhook-endpoints/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "name": "Example",
  "webhook_url": "https://example.com/hook",
  "status": "active",
  "condition": {
    "all": [
      {
        "field": "amount_minor",
        "op": "gt",
        "value": 1000
      }
    ]
  },
  "action": {
    "kind": "webhook"
  }
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.patch(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "name": "Example",
      "webhook_url": "https://example.com/hook",
      "status": "active",
      "condition": {
        "all": [
          {
            "field": "amount_minor",
            "op": "gt",
            "value": 1000
          }
        ]
      },
      "action": {
        "kind": "webhook"
      }
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Example",
    "webhook_url": "https://example.com/hook",
    "status": "active",
    "condition": {
      "all": [
        {
          "field": "amount_minor",
          "op": "gt",
          "value": 1000
        }
      ]
    },
    "action": {
      "kind": "webhook"
    }
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.patch("/v1/webhook-endpoints/{id}", {
  path: {
    id: "webhookendpoint_0000000000000000"
  },
  body: {
    name: "Example",
    webhook_url: "https://example.com/hook",
    status: "active",
    condition: {
      all: [
        {
          field: "amount_minor",
          op: "gt",
          value: 1000
        }
      ]
    },
    action: {
      kind: "webhook"
    }
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "org_id": "org_0000000000000000",
  "project_id": "prj_0000000000000000",
  "name": "Example",
  "event": "string",
  "server_id": "srv_0000000000000000",
  "webhook_url": "https://example.com/hook",
  "status": "string",
  "total_runs": 0,
  "last_run_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "signing_enabled": true,
  "event_known": true,
  "condition": {
    "all": [
      {
        "field": "amount_minor",
        "op": "gt",
        "value": 1000
      }
    ]
  },
  "action": {
    "kind": "webhook"
  }
}
```

</SplitPane>
</Split>

<TryIt method="PATCH" path="/v1/webhook-endpoints/{id}" body={"{\"name\":\"Example\",\"webhook_url\":\"https://example.com/hook\",\"status\":\"active\",\"condition\":{\"all\":[{\"field\":\"amount_minor\",\"op\":\"gt\",\"value\":1000}]},\"action\":{\"kind\":\"webhook\"}}"} />

### DELETE `/v1/webhook-endpoints/{id}`

<Endpoint method="DELETE" path="/v1/webhook-endpoints/{id}" base="https://api.codespar.dev" />

Delete a webhook endpoint

Removes the endpoint and purges its HMAC signing key in the SAME transaction. Requires `triggers:write`. A hard delete, because an endpoint is configuration and not an audit record.

The delivery history goes with it. The delivery table's foreign key onto this one is declared ON DELETE CASCADE, so this call also removes every attempt and every dead letter recorded for the endpoint; read the deliveries you still need first.

It fails CLOSED. If the vault cannot purge the key, the whole transaction rolls back, the endpoint is still there, and the answer is 503 `vault_unavailable`. The alternative would leave live signing material in the vault for an object the customer believes is gone, with no route left that could read, rotate or remove it. Retry once the vault is reachable.

That 503 does not distinguish causes. Its catch is the widest on this surface: any failure inside the transaction, not only an unreachable vault, comes back under this one code. What it always means is that nothing was deleted.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `204` | — | No Content |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |
| `503` | object | The signing secret could not be purged, so nothing was deleted. Retriable. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X DELETE https://api.codespar.dev/v1/webhook-endpoints/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
DELETE /v1/webhook-endpoints/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.delete(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.delete("/v1/webhook-endpoints/{id}", {
  path: {
    id: "webhookendpoint_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
</Split>

<TryIt method="DELETE" path="/v1/webhook-endpoints/{id}" />

### GET `/v1/webhook-endpoints/{id}/deliveries`

<Endpoint method="GET" path="/v1/webhook-endpoints/{id}/deliveries" base="https://api.codespar.dev" />

List recent delivery attempts

Attempts for one endpoint, newest first by `created_at`. Requires `triggers:read`. The endpoint is loaded under the caller's organization and project first, so a delivery cannot be read across tenants; an unknown or foreign endpoint id is 404.

`limit` is the ONLY control: there is no cursor and no total, so an attempt older than the 200 most recent is not reachable through this route. Each row is one attempt, not one event; a retried event appears once per attempt.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | `integer` | no | Rows to return. Clamped into the range 1 to 200: a larger or smaller value is reduced or raised rather than refused, and a value that does not parse as a number falls back to 50. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `deliveries` | array of object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/webhook-endpoints/{id}/deliveries HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/webhook-endpoints/{id}/deliveries", {
  path: {
    id: "webhookendpoint_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "deliveries": [
    {
      "id": "obj_0000000000000000",
      "trigger_id": "trg_0000000000000000",
      "event_id": "event_0000000000000000",
      "attempt": 0,
      "status": "string",
      "response_status": 0,
      "error": "string",
      "delivered_at": "2026-01-15T12:00:00.000Z",
      "next_retry_at": "2026-01-15T12:00:00.000Z",
      "created_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/webhook-endpoints/{id}/deliveries" />

### GET `/v1/webhook-endpoints/{id}/deliveries/{delivery_id}`

<Endpoint method="GET" path="/v1/webhook-endpoints/{id}/deliveries/{delivery_id}" base="https://api.codespar.dev" />

Inspect one delivery attempt

One attempt with the request that was sent, the response that came back, and the originating event. Requires `triggers:read`. This is the endpoint for answering `did the subscriber ever see this event, and in what form`.

`request.body` is rebuilt with the dispatcher's own serializer, so it is the byte sequence the signature covered: verify by taking the HMAC of `<request.sent_at_unix>.<request.body>` with the endpoint's signing secret. `request.headers` is re-derived for display rather than stored, the `v1` value in the signature header is redacted, and the legacy signature header the dispatcher also sends is not listed there.

Order of checks: the endpoint is resolved first, so a foreign or unknown `{id}` is 404 even when `{delivery_id}` is malformed. A malformed `{delivery_id}` under an endpoint the caller owns is 400 `invalid_delivery_id`; a well formed id that names no attempt of THIS endpoint is 404.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `delivery_id` | `string` | yes | Delivery id as decimal digits, the string form of the `bigserial` column. Up to 19 digits, which is the width of a signed 64 bit maximum; anything else is refused with 400 `invalid_delivery_id` before the database is touched. |
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | `delivery_id` is not 1 to 19 decimal digits. |
| `404` | object | Either no such webhook endpoint in the caller's organization and project, or no delivery with that id belonging to it. Both answer the same code, on purpose. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `delivery` | object | yes | — |
| `event` | object | yes | — |
| `request` | object | yes | — |
| `response` | object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries/{delivery_id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/webhook-endpoints/{id}/deliveries/{delivery_id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries/{delivery_id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}/deliveries/{delivery_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/webhook-endpoints/{id}/deliveries/{delivery_id}", {
  path: {
    id: "webhookendpoint_0000000000000000",
    delivery_id: "delivery_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "delivery": {
    "id": "obj_0000000000000000",
    "trigger_id": "trg_0000000000000000",
    "event_id": "event_0000000000000000",
    "attempt": 0,
    "status": "string",
    "response_status": 0,
    "error": "string",
    "delivered_at": "2026-01-15T12:00:00.000Z",
    "next_retry_at": "2026-01-15T12:00:00.000Z",
    "dead_at": "2026-01-15T12:00:00.000Z",
    "receipt_at": "2026-01-15T12:00:00.000Z",
    "created_at": "2026-01-15T12:00:00.000Z"
  },
  "request": {
    "url": "https://example.com/hook",
    "sent_at_unix": 0,
    "headers": {
      "Content-Type": "string",
      "X-CodeSpar-Event": "string",
      "X-CodeSpar-Event-Id": "xcodesparevent_0000000000000000",
      "X-CodeSpar-Trigger-Id": "xcodespartrigger_0000000000000000",
      "X-CodeSpar-Attempt": "string",
      "X-CodeSpar-Signature": "string"
    },
    "body": "string"
  },
  "response": {
    "status": 0,
    "body": "string",
    "error": "string"
  },
  "event": {
    "id": "obj_0000000000000000",
    "source": "string",
    "event_type": "string",
    "payload": {},
    "received_at": "2026-01-15T12:00:00.000Z",
    "provider_event_id": "providerevent_0000000000000000"
  }
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/webhook-endpoints/{id}/deliveries/{delivery_id}" />

### GET `/v1/webhook-endpoints/{id}/dlq`

<Endpoint method="GET" path="/v1/webhook-endpoints/{id}/dlq" base="https://api.codespar.dev" />

List dead lettered deliveries

The attempts that exhausted the retry ladder, `status = 'dead'`, newest first by `dead_at` with nulls last. Requires `triggers:read`. Separate from the deliveries listing so an operator can page the failures without wading through successful attempts.

A FIRST delivery reaches here after five attempts: the initial one plus four retries, whose earliest times are 1 minute, 5 minutes, 30 minutes and 2 hours after the attempt they follow, so about two and a half hours at best. A retry only fires when a drain claims the row, so the real elapsed time can be longer. A REDELIVERY gets fewer, and the redeliver operation explains why. Nothing retries a dead row on its own: it is replayed only by the redeliver call.

Same single `limit` control as the deliveries listing, with no cursor.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | `integer` | no | Rows to return. Clamped into the range 1 to 200: a larger or smaller value is reduced or raised rather than refused, and a value that does not parse as a number falls back to 50. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `dead_letters` | array of object | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/webhook-endpoints/{id}/dlq \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/webhook-endpoints/{id}/dlq HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}/dlq",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}/dlq", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/webhook-endpoints/{id}/dlq", {
  path: {
    id: "webhookendpoint_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "dead_letters": [
    {
      "id": "obj_0000000000000000",
      "event_id": "event_0000000000000000",
      "attempt": 0,
      "response_status": 0,
      "error": "string",
      "dead_at": "2026-01-15T12:00:00.000Z",
      "created_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/webhook-endpoints/{id}/dlq" />

### POST `/v1/webhook-endpoints/{id}/rotate-secret`

<Endpoint method="POST" path="/v1/webhook-endpoints/{id}/rotate-secret" base="https://api.codespar.dev" />

Mint a new signing secret for a webhook endpoint

Generates a new HMAC secret and overwrites the vault entry in place. Requires `triggers:write`. The handler reads no request body.

THERE IS NO OVERLAP WINDOW. The vault reference does not change, so the next dispatch that dereferences it signs with the new secret and the old one stops verifying the moment this transaction commits. Install the returned value in the subscriber before the next event fires, or those deliveries fail signature checks on the far side while still counting as delivered here.

The plaintext is revealed exactly once, in this response. The vault write and the row update share one transaction, so a failure leaves neither half standing and the answer is 503 `vault_unavailable` with no rotation performed: the previous secret keeps working.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |
| `503` | object | The new secret could not be persisted, so no rotation occurred and the previous secret is still in force. Retriable. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `secret` | `string` | yes | The new signing secret in plaintext, 32 random bytes as hex, shown EXACTLY ONCE. Later reads expose only `signing_enabled`, and a lost secret is replaced by rotating again. |
| `signing_enabled` | `true` | yes | — |
| `trigger_id` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/webhook-endpoints/{id}/rotate-secret \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/webhook-endpoints/{id}/rotate-secret HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}/rotate-secret",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}/rotate-secret", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/webhook-endpoints/{id}/rotate-secret", {
  path: {
    id: "webhookendpoint_0000000000000000"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "trigger_id": "trg_0000000000000000",
  "signing_enabled": true,
  "secret": "string"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/webhook-endpoints/{id}/rotate-secret" />

### POST `/v1/webhook-endpoints/{id}/test-fire`

<Endpoint method="POST" path="/v1/webhook-endpoints/{id}/test-fire" base="https://api.codespar.dev" />

Deliver a synthetic event to a webhook endpoint

Publishes a synthetic event on this subscription and delivers it at once, so you can watch the request arrive on the other side without waiting for a real event.

The endpoint has to be `active`: a paused one is refused with 409 rather than queued in silence. The delivered body carries `test: true` and the `trigger_id`, written by the server AFTER your `payload`, so those two markers cannot be forged by the caller.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Endpoint id, `trg_` followed by a 16 character nanoid, as minted by the create call. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `event_type` | `string` | no | Dot separated lowercase, the same shape as a subscription's `event`. When absent, the synthetic event goes out as `trigger.test_fire`. |
| `payload` | object | no | EXTRA fields in the event body. They do not overwrite the markers: the server writes `test: true`, `trigger_id` and `requested_at` LAST, so a subscriber that decides on `test` never receives a test fire disguised as a real one. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | The body did not match the schema. |
| `404` | object | No webhook endpoint with that id in the caller's organization and project. |
| `409` | object | The subscription is not `active` (its current status comes back in `details.status`), or its action is internal (`human_review`, `pause_agent`) and would run for real: simulate instead. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `delivery_id` | `string,null` | yes | Null when the dispatch never got as far as recording an attempt. |
| `error` | `string,null` | yes | Why the attempt failed, in the same form as the delivery history. |
| `event_id` | `string` | yes | The synthetic event, persisted so the delivery has a real target. |
| `response_status` | `integer,null` | yes | The HTTP status the subscriber answered, or null when there was no response. |
| `status` | `string` | yes | The outcome of the attempt, as the dispatcher wrote it. |
| `trigger_id` | `string` | yes | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/webhook-endpoints/{id}/test-fire \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "event_type": "string",
       "payload": {}
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/webhook-endpoints/{id}/test-fire HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "event_type": "string",
  "payload": {}
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/webhook-endpoints/{id}/test-fire",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "event_type": "string",
      "payload": {}
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/webhook-endpoints/{id}/test-fire", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "event_type": "string",
    "payload": {}
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.post("/v1/webhook-endpoints/{id}/test-fire", {
  path: {
    id: "webhookendpoint_0000000000000000"
  },
  body: {
    event_type: "string",
    payload: {}
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "trigger_id": "trg_0000000000000000",
  "event_id": "event_0000000000000000",
  "delivery_id": "delivery_0000000000000000",
  "status": "string",
  "response_status": 0,
  "error": "string"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/webhook-endpoints/{id}/test-fire" body={"{\"event_type\":\"string\",\"payload\":{}}"} />

