---
title: Webhooks overview
description: Get a signed POST when records change instead of polling. Register a webhook through the API, choose from 138 events, and verify every delivery.
canonical: https://developers.coevera.com/webhooks/overview/
section: webhooks
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Webhooks overview

**Answer:** Create a webhook with POST /entities/Webhooks: url, events and insecure_ssl are required, and a signature UUID makes Coevera sign every delivery. Deliveries are POSTs of a JSON array of up to 100 events, signed in WebHook-Signature with HMAC-SHA256 of the raw body. Answer any 2xx within 10 seconds; failed deliveries are retried for about 24 hours.

A webhook tells Coevera to call your URL when records change, so you do not have to poll the API.
You register it through the API; the CRM has no screen for webhooks.

## How webhooks work

1. A record changes in your space.
2. Coevera POSTs a JSON array of events to your URL, signed with your webhook's secret.
3. Your receiver checks the signature against the raw body.
4. Your receiver answers 2xx within 10 seconds, then processes the events.

## Before you begin

- An API application whose permissions allow **Write** on **Webhooks** (under **Misc**; see
  [permissions and scopes](/get-started/permissions-and-scopes/)), and its credentials in
  `COEVERA_API_KEY` and `COEVERA_API_SECRET` (see [authentication](/get-started/authentication/)).
  **Read** is enough to list webhooks.
- A receiver reachable from the internet over HTTPS. To build one, follow the
  [webhook receiver recipe](/recipes/webhook-receiver/); to try it on your machine first, see
  [testing locally](/webhooks/testing-locally/).
- Your region and space ID in the bar at the top of the page; the samples fill them in.

The samples below share this setup: the base URL, the credentials and your receiver's URL.

**REST · cURL**

```bash
COEVERA_BASE="https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
# Your receiver: Coevera POSTs every delivery to this URL.
RECEIVER_URL="https://hooks.example.com/coevera"
```

**REST · TypeScript**

```ts
const base = `https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");
// Your receiver: Coevera POSTs every delivery to this URL.
const receiverUrl = "https://hooks.example.com/coevera";

async function coevera(method: string, path: string, body?: unknown) {
  const res = await fetch(`${base}${path}`, {
    method,
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`HTTP ${res.status} ${json.name}: ${json.message}`);
  return json;
}
```

**REST · Python**

```python
BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])
# Your receiver: Coevera POSTs every delivery to this URL.
RECEIVER_URL = "https://hooks.example.com/coevera"


def coevera(method, path, **kwargs):
    response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
    body = response.json()
    if not response.ok:
        sys.exit(f"HTTP {response.status_code} {body.get('name')}: {body.get('message')}")
    return body
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"
# Your receiver: Coevera POSTs every delivery to this URL.
RECEIVER_URL="https://hooks.example.com/coevera"

# gql: POST the GraphQL request read from stdin. GraphQL reports errors in the body, often
# with HTTP 200, so fail on those too.
gql() {
  local response
  response=$(curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
    -H "Content-Type: application/json" \
    -d @-)
  if jq -e '.errors' <<<"$response" >/dev/null; then
    echo "$response" >&2
    return 1
  fi
  echo "$response"
}
```

**GraphQL · TypeScript**

```ts
const endpoint = `https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");
// Your receiver: Coevera POSTs every delivery to this URL.
const receiverUrl = "https://hooks.example.com/coevera";

// gql: one GraphQL request. GraphQL reports errors in the body, often with HTTP 200: check both.
async function gql(query: string, variables: object = {}) {
  const res = await fetch(endpoint, {
    method: "POST",
    headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  const body = await res.json();
  if (!res.ok || body.errors) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.errors ?? body)}`);
  return body.data;
}
```

**GraphQL · Python**

```python
ENDPOINT = "https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])
# Your receiver: Coevera POSTs every delivery to this URL.
RECEIVER_URL = "https://hooks.example.com/coevera"


def gql(query, variables=None):
    """One GraphQL request. GraphQL reports errors in the body, often with HTTP 200."""
    response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
    body = response.json()
    if not response.ok or body.get("errors"):
        sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
    return body["data"]
```

## Create a webhook

Send the URL, the events and `insecure_ssl`, which are required, and a `signature`: a UUID you
generate and keep as your receiver's secret. In GraphQL, events are written without the dot
(`OpportunityWon`) and `options` is a JSON string. Every field and operation of the entity is on
the [Webhooks reference page](/reference/rest/webhooks/#create).

**REST · cURL**

```bash
# The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
SECRET=$(od -An -tx1 -N16 /dev/urandom | tr -d ' \n' |
  sed -E 's/^(.{8})(.{4}).(.{3}).(.{3})(.{12})$/\1-\2-4\3-a\4-\5/')
webhook=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Webhooks" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "url": "$RECEIVER_URL",
  "events": ["Opportunity.Won", "Opportunity.Lost"],
  "signature": "$SECRET",
  "insecure_ssl": false,
  "options": {
    "entity": {
      "Opportunity": { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] }
    },
    "chunk_size": 50
  }
}
JSON
)
echo "$webhook"
WEBHOOK_ID=$(jq -er '.data.id' <<<"$webhook")
echo "WEBHOOK_SECRET=$SECRET"
```

**REST · TypeScript**

```ts
// The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
const secret = randomUUID();
const created = await coevera("POST", "/entities/Webhooks", {
  url: receiverUrl,
  events: ["Opportunity.Won", "Opportunity.Lost"],
  signature: secret,
  insecure_ssl: false,
  options: {
    entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
    chunk_size: 50,
  },
});
const webhookId: string = created.data.id;
console.log("Webhook:", webhookId, "WEBHOOK_SECRET:", secret);
```

**REST · Python**

```python
# The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
secret = str(uuid.uuid4())
created = coevera(
    "POST",
    "/entities/Webhooks",
    json={
        "url": RECEIVER_URL,
        "events": ["Opportunity.Won", "Opportunity.Lost"],
        "signature": secret,
        "insecure_ssl": False,
        "options": {
            "entity": {"Opportunity": {"load-only": ["id", "name", "owner_id", "step_id", "value", "revision"]}},
            "chunk_size": 50,
        },
    },
)
webhook_id = created["data"]["id"]
print("Webhook:", webhook_id, "WEBHOOK_SECRET:", secret)
```

**GraphQL · cURL**

```bash
# The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
SECRET=$(od -An -tx1 -N16 /dev/urandom | tr -d ' \n' |
  sed -E 's/^(.{8})(.{4}).(.{3}).(.{3})(.{12})$/\1-\2-4\3-a\4-\5/')
webhook=$(jq -n --arg url "$RECEIVER_URL" --arg secret "$SECRET" '{
  query: "mutation CreateWebhook($input: CreateWebhookInput!) { createWebhook(input: $input) { webhook { id url events insecureSsl options } } }",
  variables: {
    input: {
      url: $url,
      events: ["OpportunityWon", "OpportunityLost"],
      signature: $secret,
      insecureSsl: false,
      options: ({
        entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
        chunk_size: 50
      } | tojson)
    }
  }
}' | gql)
echo "$webhook"
WEBHOOK_ID=$(jq -er '.data.createWebhook.webhook.id' <<<"$webhook")
echo "WEBHOOK_SECRET=$SECRET"
```

**GraphQL · TypeScript**

```ts
// The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
const secret = randomUUID();
const created = await gql(
  `mutation CreateWebhook($input: CreateWebhookInput!) {
    createWebhook(input: $input) { webhook { id url events insecureSsl options } }
  }`,
  {
    input: {
      url: receiverUrl,
      events: ["OpportunityWon", "OpportunityLost"],
      signature: secret,
      insecureSsl: false,
      options: JSON.stringify({
        entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
        chunk_size: 50,
      }),
    },
  },
);
const webhookId: string = created.createWebhook.webhook.id;
console.log("Webhook:", webhookId, "WEBHOOK_SECRET:", secret);
```

**GraphQL · Python**

```python
# The signing secret: a random UUID. Keep it as WEBHOOK_SECRET in your receiver.
secret = str(uuid.uuid4())
created = gql(
    """
    mutation CreateWebhook($input: CreateWebhookInput!) {
      createWebhook(input: $input) { webhook { id url events insecureSsl options } }
    }
    """,
    {
        "input": {
            "url": RECEIVER_URL,
            "events": ["OpportunityWon", "OpportunityLost"],
            "signature": secret,
            "insecureSsl": False,
            "options": json.dumps({
                "entity": {"Opportunity": {"load-only": ["id", "name", "owner_id", "step_id", "value", "revision"]}},
                "chunk_size": 50,
            }),
        }
    },
)
webhook_id = created["createWebhook"]["webhook"]["id"]
print("Webhook:", webhook_id, "WEBHOOK_SECRET:", secret)
```

**Recorded response · REST · 201 Created**

```json
{
  "success": true,
  "data": {
    "is_delete_protected": false,
    "id": "1e000000-0000-4000-a000-000000000001",
    "is_deleted": false,
    "modified": "2026-10-15 09:00:00.000000+00:00",
    "created": "2026-10-15 09:00:00.000035+00:00",
    "application": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Applications/00000000-0000-4000-a000-000000000001",
    "client": null,
    "application_id": "00000000-0000-4000-a000-000000000001",
    "client_id": null,
    "insecure_ssl": false,
    "options": {
      "entity": {
        "Opportunity": {
          "load-only": [
            "id",
            "name",
            "owner_id",
            "step_id",
            "value",
            "revision"
          ]
        }
      },
      "chunk_size": 50
    },
    "signature": "1e000000-0000-4000-a000-000000000002",
    "url": "https://hooks.example.com/coevera",
    "events": [
      "Opportunity.Won",
      "Opportunity.Lost"
    ]
  }
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "createWebhook": {
      "webhook": {
        "id": "1e000000-0000-4000-a000-000000000001",
        "url": "https://hooks.example.com/coevera",
        "events": [
          "OpportunityLost",
          "OpportunityWon"
        ],
        "insecureSsl": false,
        "options": "{\"entity\": {\"Opportunity\": {\"load-only\": [\"id\", \"name\", \"owner_id\", \"step_id\", \"value\", \"revision\"]}}, \"chunk_size\": 50}"
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

What the API checks when you create or update a webhook:

| Field          | Rule                                                                                                                                     |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `url`          | Required. Must start with `https://` or `http://`. An address on a private network, `localhost` included, is refused (400).              |
| `events`       | Required, at least one, from the [events catalogue](/webhooks/events/) (422 otherwise).                                                  |
| `insecure_ssl` | Required. `true` skips the check of your receiver's TLS certificate; keep it `false`.                                                    |
| `signature`    | Optional, but without it deliveries carry no authentication. Must be a UUID (422 otherwise); the API stores and returns it in lowercase. |
| `options`      | Optional, defaults to `{}`; `null` is refused. See [filtering and options](/webhooks/filtering-and-options/).                            |

A refused request answers with the error body that [errors](/concepts/errors/) describes: check
`code` and `message`, not only the status.

> **Warning:** Every read of a webhook returns its `signature`, the secret your receiver verifies deliveries
> with. Keep webhook responses out of logs, and give the **Webhooks** permission only to the
> applications that manage them.

A webhook registered with an API application's credentials has no `client_id`. Its deliveries
include every record the change touched, private records too.

## The payload

Each delivery is a `POST` to your URL with `Content-Type: application/json` and a body that is a
JSON array: one chunk of up to `chunk_size` events, 100 by default. Each event carries (from the
code):

| Field            | What it holds                                                                                                                                   |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook`        | The webhook's registration, such as its `id`, `url`, `events` and `options`, without its `signature`                                            |
| `event`          | The event that fired, such as `Opportunity.Won`                                                                                                 |
| `event_time`     | When the change was saved: UTC, ISO 8601 without an offset                                                                                      |
| `team_space_id`  | Your space ID                                                                                                                                   |
| `entity`         | The record as the REST API returns it, shaped by `load-only` and `expand` in the options ([querying](/concepts/querying/#load-only-and-expand)) |
| `related_entity` | The document for a `DocumentLinked` event, otherwise `null`                                                                                     |

The record is read when the delivery is built, not when it was written, and a retry sends the
same body again. The delivery has no event or delivery ID of its own: deduplicate on `event`,
`entity.id`, `entity.revision` and `event_time`, as the
[security page](/webhooks/security-and-signatures/#replays-and-duplicates) explains.

## Verify the signature

When the webhook has a `signature`, every delivery carries these headers:

| Header                      | Value                                                                            |
| --------------------------- | -------------------------------------------------------------------------------- |
| `WebHook-Signature`         | Lowercase hex HMAC-SHA256 of the exact request body, keyed with your `signature` |
| `WebHook-Signature-Version` | `v1`                                                                             |
| `Webhook-Delivery-Retry`    | The attempt: `0` on the first try, up to `10`                                    |
| `client-request-id`         | The `Client-Request-Id` of the API call that caused the change, if it sent one   |

HTTP header names are case-insensitive, so read them in any case. To check a delivery without
writing code, paste it into the
[signature checker](/webhooks/security-and-signatures/#signature-checker); it runs in your
browser.

## Delivery and retries

| Rule             | Value                                                                        |
| ---------------- | ---------------------------------------------------------------------------- |
| Success          | Any 2xx within 10 seconds                                                    |
| Retries          | Up to 10 retries, 11 attempts, over about 24 hours, with exponential backoff |
| Disabled at once | Your endpoint answers 403 or 410                                             |
| Disabled later   | Retries run out, or a 404 on the last attempt                                |
| Order            | Not guaranteed; each chunk retries on its own                                |
| On delete        | Deleting a webhook sends `DELETE` to its URL; accept it                      |

A disabled webhook disappears without notice. The details, and what to do about it, are on
[delivery and retries](/webhooks/delivery-and-retries/).

## Avoid loops

If your integration writes back to Coevera, a webhook would report your own writes to you. Send
the header `WebHook-Skip-Key: northwind-sync` on those API calls, and list the same key in the
webhook's `options.skip_keys`: the webhook does not fire for those writes. The
update below adds the key.

## Update, list and delete

Update with [`PATCH`](/reference/rest/webhooks/#update) (REST) or `updateWebhook` (GraphQL). The
`events` and `options` you send replace the webhook's, so send the whole list and the whole
object:

**REST · cURL**

```bash
# The events and options you send replace the webhook's: send the whole list and object.
curl -sS --fail-with-body -X PATCH "$COEVERA_BASE/entities/Webhooks/$WEBHOOK_ID" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "events": ["Opportunity.Won", "Opportunity.Lost", "Opportunity.Move"],
  "options": {
    "entity": {
      "Opportunity": { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] }
    },
    "chunk_size": 50,
    "skip_keys": ["northwind-sync"]
  }
}
JSON
echo
```

**REST · TypeScript**

```ts
// The events and options you send replace the webhook's: send the whole list and object.
await coevera("PATCH", `/entities/Webhooks/${webhookId}`, {
  events: ["Opportunity.Won", "Opportunity.Lost", "Opportunity.Move"],
  options: {
    entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
    chunk_size: 50,
    skip_keys: ["northwind-sync"],
  },
});
```

**REST · Python**

```python
# The events and options you send replace the webhook's: send the whole list and object.
coevera(
    "PATCH",
    f"/entities/Webhooks/{webhook_id}",
    json={
        "events": ["Opportunity.Won", "Opportunity.Lost", "Opportunity.Move"],
        "options": {
            "entity": {"Opportunity": {"load-only": ["id", "name", "owner_id", "step_id", "value", "revision"]}},
            "chunk_size": 50,
            "skip_keys": ["northwind-sync"],
        },
    },
)
```

**GraphQL · cURL**

```bash
# The events and options you send replace the webhook's: send the whole list and object.
jq -n --arg id "$WEBHOOK_ID" '{
  query: "mutation UpdateWebhook($input: UpdateWebhookInput!) { updateWebhook(input: $input) { webhook { id events options } } }",
  variables: {
    input: {
      id: $id,
      events: ["OpportunityWon", "OpportunityLost", "OpportunityMove"],
      options: ({
        entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
        chunk_size: 50,
        skip_keys: ["northwind-sync"]
      } | tojson)
    }
  }
}' | gql
```

**GraphQL · TypeScript**

```ts
// The events and options you send replace the webhook's: send the whole list and object.
await gql(
  `mutation UpdateWebhook($input: UpdateWebhookInput!) {
    updateWebhook(input: $input) { webhook { id events options } }
  }`,
  {
    input: {
      id: webhookId,
      events: ["OpportunityWon", "OpportunityLost", "OpportunityMove"],
      options: JSON.stringify({
        entity: { Opportunity: { "load-only": ["id", "name", "owner_id", "step_id", "value", "revision"] } },
        chunk_size: 50,
        skip_keys: ["northwind-sync"],
      }),
    },
  },
);
```

**GraphQL · Python**

```python
# The events and options you send replace the webhook's: send the whole list and object.
gql(
    """
    mutation UpdateWebhook($input: UpdateWebhookInput!) {
      updateWebhook(input: $input) { webhook { id events options } }
    }
    """,
    {
        "input": {
            "id": webhook_id,
            "events": ["OpportunityWon", "OpportunityLost", "OpportunityMove"],
            "options": json.dumps({
                "entity": {"Opportunity": {"load-only": ["id", "name", "owner_id", "step_id", "value", "revision"]}},
                "chunk_size": 50,
                "skip_keys": ["northwind-sync"],
            }),
        }
    },
)
```

Read it back with a [list](/reference/rest/webhooks/#list) call. List webhooks without
`load-only`: with it, `options` comes back as `{}`.

**REST · cURL**

```bash
# Read webhooks without load-only: with it, options comes back empty. Every read also
# returns the signature, so keep these responses out of logs.
curl -sS --fail-with-body "$COEVERA_BASE/entities/Webhooks" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" |
  jq '[.data[] | {id, url, events, options}]'
```

**REST · TypeScript**

```ts
// Read webhooks without load-only: with it, options comes back empty. Every read also
// returns the signature, so keep these responses out of logs.
const list = await coevera("GET", "/entities/Webhooks");
const shown = list.data.map(({ id, url, events, options }: Record<string, unknown>) => ({ id, url, events, options }));
console.log(JSON.stringify(shown, null, 2));
```

**REST · Python**

```python
# Read webhooks without load-only: with it, options comes back empty. Every read also
# returns the signature, so keep these responses out of logs.
webhooks = coevera("GET", "/entities/Webhooks")
shown = [{key: w[key] for key in ("id", "url", "events", "options")} for w in webhooks["data"]]
print(json.dumps(shown, indent=2))
```

**GraphQL · cURL**

```bash
jq -n '{
  query: "{ entities { webhook { getByCriteria(first: 10) { totalCount edges { node { id url events options } } } } } }"
}' | gql
```

**GraphQL · TypeScript**

```ts
const list = await gql(`{
  entities {
    webhook {
      getByCriteria(first: 10) { totalCount edges { node { id url events options } } }
    }
  }
}`);
console.log(JSON.stringify(list.entities.webhook.getByCriteria, null, 2));
```

**GraphQL · Python**

```python
listed = gql("""
{
  entities {
    webhook {
      getByCriteria(first: 10) { totalCount edges { node { id url events options } } }
    }
  }
}
""")
print(json.dumps(listed["entities"]["webhook"]["getByCriteria"], indent=2))
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 1,
  "page_info": {
    "start_cursor": "WyIxZTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIxZTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": [
    {
      "is_delete_protected": false,
      "id": "1e000000-0000-4000-a000-000000000001",
      "is_deleted": false,
      "modified": "2026-10-15 09:00:00.145616+00:00",
      "created": "2026-10-15 09:00:00.000035+00:00",
      "application": "https://eu-central.api.coevera.com/api/v100/rest/spaces/nv2_northwind_docs/entities/Applications/00000000-0000-4000-a000-000000000001",
      "client": null,
      "application_id": "00000000-0000-4000-a000-000000000001",
      "client_id": null,
      "insecure_ssl": false,
      "options": {
        "entity": {
          "Opportunity": {
            "load-only": [
              "id",
              "name",
              "owner_id",
              "step_id",
              "value",
              "revision"
            ]
          }
        },
        "skip_keys": [
          "northwind-sync"
        ],
        "chunk_size": 50
      },
      "signature": "1e000000-0000-4000-a000-000000000002",
      "url": "https://hooks.example.com/coevera",
      "events": [
        "Opportunity.Move",
        "Opportunity.Lost",
        "Opportunity.Won"
      ]
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "webhook": {
        "getByCriteria": {
          "totalCount": 1,
          "edges": [
            {
              "node": {
                "id": "1e000000-0000-4000-a000-000000000001",
                "url": "https://hooks.example.com/coevera",
                "events": [
                  "OpportunityWon",
                  "OpportunityLost",
                  "OpportunityMove"
                ],
                "options": "{\"entity\": {\"Opportunity\": {\"load-only\": [\"id\", \"name\", \"owner_id\", \"step_id\", \"value\", \"revision\"]}}, \"skip_keys\": [\"northwind-sync\"], \"chunk_size\": 50}"
              }
            }
          ]
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

Delete it. Coevera then sends `DELETE` to the webhook's URL, so your receiver should answer it
with a 2xx:

**REST · cURL**

```bash
curl -sS --fail-with-body -X DELETE "$COEVERA_BASE/entities/Webhooks/$WEBHOOK_ID" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echo
```

**REST · TypeScript**

```ts
await coevera("DELETE", `/entities/Webhooks/${webhookId}`);
```

**REST · Python**

```python
coevera("DELETE", f"/entities/Webhooks/{webhook_id}")
```

**GraphQL · cURL**

```bash
jq -n --arg id "$WEBHOOK_ID" '{
  query: "mutation DeleteWebhook($input: DeleteWebhookInput!) { deleteWebhook(input: $input) { webhook { id isDeleted } } }",
  variables: { input: { id: $id } }
}' | gql
```

**GraphQL · TypeScript**

```ts
await gql(
  `mutation DeleteWebhook($input: DeleteWebhookInput!) {
    deleteWebhook(input: $input) { webhook { id isDeleted } }
  }`,
  { input: { id: webhookId } },
);
```

**GraphQL · Python**

```python
gql(
    """
    mutation DeleteWebhook($input: DeleteWebhookInput!) {
      deleteWebhook(input: $input) { webhook { id isDeleted } }
    }
    """,
    {"input": {"id": webhook_id}},
)
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "data": "1e000000-0000-4000-a000-000000000001"
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "deleteWebhook": {
      "webhook": {
        "id": "1e000000-0000-4000-a000-000000000001",
        "isDeleted": true
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

The API soft-deletes: the webhook leaves the list, and `include-deleted=true` still shows it
([querying](/concepts/querying/#load-only-and-expand)).

**REST · cURL**

```bash
# A deleted webhook is gone from the list (total: 0); include-deleted=true would still show it.
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Webhooks" \
  --data-urlencode "filter[id]=$WEBHOOK_ID" \
  -d load-only=id \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echo
```

**REST · TypeScript**

```ts
// A deleted webhook is gone from the list (total: 0); include-deleted=true would still show it.
const left = await coevera("GET", `/entities/Webhooks?filter[id]=${encodeURIComponent(webhookId)}&load-only=id`);
console.log("Webhooks listed with this id:", left.total);
```

**REST · Python**

```python
# A deleted webhook is gone from the list (total: 0); include-deleted=true would still show it.
left = coevera("GET", "/entities/Webhooks", params={"filter[id]": webhook_id, "load-only": "id"})
print("Webhooks listed with this id:", left.get("total"))
```

**GraphQL · cURL**

```bash
# A deleted webhook is gone from the list (totalCount: 0); includeDeleted: true would still show it.
jq -n --arg id "$WEBHOOK_ID" '{
  query: "query Left($id: String!) { entities { webhook { getByCriteria(filter: { id: { eq: [$id] } }) { totalCount } } } }",
  variables: { id: $id }
}' | gql
```

**GraphQL · TypeScript**

```ts
// A deleted webhook is gone from the list (totalCount: 0); includeDeleted: true would still show it.
const left = await gql(
  `query Left($id: String!) {
    entities { webhook { getByCriteria(filter: { id: { eq: [$id] } }) { totalCount } } }
  }`,
  { id: webhookId },
);
console.log("Webhooks listed with this id:", left.entities.webhook.getByCriteria.totalCount);
```

**GraphQL · Python**

```python
# A deleted webhook is gone from the list (totalCount: 0); includeDeleted: true would still show it.
left = gql(
    """
    query Left($id: String!) {
      entities { webhook { getByCriteria(filter: { id: { eq: [$id] } }) { totalCount } } }
    }
    """,
    {"id": webhook_id},
)
print("Webhooks listed with this id:", left["entities"]["webhook"]["getByCriteria"]["totalCount"])
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 0,
  "page_info": {
    "start_cursor": null,
    "end_cursor": null,
    "has_previous_page": false,
    "has_next_page": false
  },
  "data": []
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "webhook": {
        "getByCriteria": {
          "totalCount": 0
        }
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

## Limits

- Webhooks are managed through the API only; the CRM has no screen for them.
- At most 100 events per delivery (`chunk_size`, 1 to 100).
- There is no test event, delivery log or replay in Coevera; test with your own deliveries
  ([testing locally](/webhooks/testing-locally/)).
- There is no notification when a webhook is disabled.
- The Automatizer **Send Webhook** action is a separate system with its own rules; see
  [API webhooks vs Automatizer webhooks](/webhooks/api-vs-automatizer-webhooks/).

## Next steps

- [Events catalogue](/webhooks/events/): every event you can subscribe to.
- [Security and signatures](/webhooks/security-and-signatures/): verify deliveries and handle replays.
- [Build a webhook receiver](/recipes/webhook-receiver/): a tested receiver in TypeScript and Python.
