WebhooksWebhooks overview
Webhooks overview
Get a signed POST when records change instead of polling. Register a webhook through the API, choose from 138 events, and verify every delivery.
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
- A record changes in your space.
- Coevera POSTs a JSON array of events to your URL, signed with your webhook's secret.
- Your receiver checks the signature against the raw body.
- 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), and its credentials in
COEVERA_API_KEYandCOEVERA_API_SECRET(see authentication). Read is enough to list webhooks. - A receiver reachable from the internet over HTTPS. To build one, follow the webhook receiver recipe; to try it on your machine first, see 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.
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"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;
}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 bodyCOEVERA_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"
}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;
}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.
# 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"// 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);# 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)# 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"// 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);# 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){
"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"
]
}
}Show all 36 linesShow less
{
"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}"
}
}
}
}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 (422 otherwise). |
insecure_ |
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. |
A refused request answers with the error body that 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. |
event_ |
When the change was saved: UTC, ISO 8601 without an offset |
team_ |
Your space ID |
entity |
The record as the REST API returns it, shaped by load-only and expand in the options (querying) |
related_ |
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 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; 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.
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 (REST) or updateWebhook (GraphQL). The
events and options you send replace the webhook's, so send the whole list and the whole
object:
# 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// 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"],
},
});# 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"],
},
},
)# 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// 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"],
}),
},
},
);# 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 call. List webhooks without
load-only: with it, options comes back as {}.
# 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}]'// 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));# 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))jq -n '{
query: "{ entities { webhook { getByCriteria(first: 10) { totalCount edges { node { id url events options } } } } } }"
}' | gqlconst 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));listed = gql("""
{
entities {
webhook {
getByCriteria(first: 10) { totalCount edges { node { id url events options } } }
}
}
}
""")
print(json.dumps(listed["entities"]["webhook"]["getByCriteria"], indent=2)){
"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"
]
}
]
}Show all 49 linesShow less
{
"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}"
}
}
]
}
}
}
}
}Show all 25 linesShow less
Delete it. Coevera then sends DELETE to the webhook's URL, so your receiver should answer it
with a 2xx:
curl -sS --fail-with-body -X DELETE "$COEVERA_BASE/entities/Webhooks/$WEBHOOK_ID" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echoawait coevera("DELETE", `/entities/Webhooks/${webhookId}`);coevera("DELETE", f"/entities/Webhooks/{webhook_id}")jq -n --arg id "$WEBHOOK_ID" '{
query: "mutation DeleteWebhook($input: DeleteWebhookInput!) { deleteWebhook(input: $input) { webhook { id isDeleted } } }",
variables: { input: { id: $id } }
}' | gqlawait gql(
`mutation DeleteWebhook($input: DeleteWebhookInput!) {
deleteWebhook(input: $input) { webhook { id isDeleted } }
}`,
{ input: { id: webhookId } },
);gql(
"""
mutation DeleteWebhook($input: DeleteWebhookInput!) {
deleteWebhook(input: $input) { webhook { id isDeleted } }
}
""",
{"input": {"id": webhook_id}},
){
"success": true,
"data": "1e000000-0000-4000-a000-000000000001"
}{
"data": {
"deleteWebhook": {
"webhook": {
"id": "1e000000-0000-4000-a000-000000000001",
"isDeleted": true
}
}
}
}The API soft-deletes: the webhook leaves the list, and include-deleted=true still shows it
(querying).
# 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// 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);# 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"))# 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// 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);# 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"]){
"success": true,
"total": 0,
"page_info": {
"start_cursor": null,
"end_cursor": null,
"has_previous_page": false,
"has_next_page": false
},
"data": []
}{
"data": {
"entities": {
"webhook": {
"getByCriteria": {
"totalCount": 0
}
}
}
}
}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).
- 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.
Next steps
- Events catalogue: every event you can subscribe to.
- Security and signatures: verify deliveries and handle replays.
- Build a webhook receiver: a tested receiver in TypeScript and Python.