Webhooks
Have Cliento Pro call your endpoint when something happens, instead of polling for it.
Last revised: 2026-08-23
What a webhook is here
Every state change in a workspace writes an event - a contact created, a deal won, an invoice paid. A webhook subscription is a standing instruction to POST those events to a URL you control, as they happen.
Use one instead of polling GET /events on a timer. Polling costs you latency you cannot tune away and costs the workspace a request per interval per integration; a webhook arrives within seconds of the change and costs nothing while nothing is happening.
Creating a subscription
Create one in the app under Settings → Webhooks, or over the API with webhooks.write.
curl -sS -X POST "$CLIENTO_HOST/api/v1/webhooks" \
-H "Authorization: Bearer $CLIENTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse sync",
"description": "Mirrors contacts and deals into the data warehouse",
"target_url": "https://hooks.example.com/cliento",
"event_types": ["ContactCreated", "ContactUpdated", "DealWon"],
"max_attempts": 5,
"is_active": true
}'{
"id": 7,
"name": "Warehouse sync",
"target_url": "https://hooks.example.com/cliento",
"event_types": ["ContactCreated", "ContactUpdated", "DealWon"],
"secret": "whsec_M2Yx…",
"max_attempts": 5,
"is_active": true,
"last_delivery_at": null,
"created_at": "2026-08-23T10:20:00"
}| Field | Meaning |
|---|---|
target_url | Where to POST. Must be HTTPS and reachable from the internet. |
event_types | Which events to send. ["*"] means all of them. |
extra_headers | Static headers added to every delivery - a shared token for your own gateway, for example. |
masked_fields | Fields this endpoint must never receive. See below. |
max_attempts | Attempts before a delivery is abandoned. Default 5. |
is_active | Set false to pause without deleting - queued deliveries are then abandoned. |
What your endpoint must do
- Be HTTPS and publicly reachable. A private address is never called.
- Answer 2xx to mean 'received'. Anything else - including a redirect - counts as a failure and is retried.
- Answer within 10 seconds. That is the socket timeout; a slower reply is a failed attempt even if you eventually process it.
- Do the work afterwards. Verify, enqueue, return 200. Processing inline is what turns a slow database into a retry storm.
- Accept duplicates. Delivery is at-least-once - see the delivery guide.
The payload
Every delivery is a JSON POST with the same envelope. payload is the event; the three fields around it are the delivery's own metadata.
POST /cliento HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
X-Cliento-Signature: sha256=6b2f…
X-Cliento-Delivery-Id: 88213
X-Cliento-Event-Type: ContactCreated{
"delivery_id": 88213,
"event_type": "ContactCreated",
"timestamp": "2026-08-23T10:21:07Z",
"payload": {
"id": 91442,
"tenant_id": 17,
"user_id": 5,
"entity_type": "contact",
"entity_type_label": { "en": "Contact", "fa": "مخاطب", "ar": "جهة اتصال" },
"entity_id": 4182,
"event_type": "ContactCreated",
"event_type_label": { "en": "Contact created", "fa": "مخاطب ایجاد شد", "ar": "تم إنشاء جهة الاتصال" },
"source": "api",
"metadata": { "email": "sara@example.com" },
"occurred_at": "2026-08-23T10:21:06"
}
}| Field | Use it for |
|---|---|
delivery_id | Deduplication. The same number on a retry; a new one for a genuinely new delivery. |
event_type | Routing. A stable machine code. |
timestamp | When the delivery was built (UTC). |
payload.entity_type / entity_id | Which record changed - fetch it over the API if you need the full row. |
payload.metadata | Event-specific extras. Varies by event type; treat unknown keys as optional. |
Available events
Event types are PascalCase names of the form <Entity><PastTenseVerb>. Subscribe to the exact names you handle rather than to *, so a new event type added to the product does not start arriving at an endpoint that has no code for it.
ContactCreated ContactUpdated ContactDeleted
LeadCreated LeadStatusChanged LeadConverted
DealCreated DealStageChanged DealWon DealLost
TaskCreated TaskUpdated
PipelineCreated PipelineStageUpdated
TagCreated SegmentUpdated CampaignCreatedThe complete, current list is whatever appears in your own workspace's event log: GET /events returns real events with their event_type, and the Settings → Webhooks screen offers the same catalogue as a picker. Both are generated from the product, so neither can go stale the way a hand-written list here would.
Keeping fields out of a payload
masked_fields names fields this destination must never receive - a phone number that may leave your CRM but must not leave your country, for instance. GET /webhooks/maskable-fields lists the catalogue of keys.
curl -sS -X PATCH "$CLIENTO_HOST/api/v1/webhooks/7" \
-H "Authorization: Bearer $CLIENTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "masked_fields": ["contact.phone", "contact.email"] }'Masking is applied before the delivery is stored, so the queued row is already masked and a retry cannot re-send the original. It also means GET /webhooks/{subscription_id}/deliveries - the delivery log - never holds an unmasked copy.