Back to documentation

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.

bash·Subscribe
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
  }'
json·The response - the only time the secret is shown
{
  "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"
}
FieldMeaning
target_urlWhere to POST. Must be HTTPS and reachable from the internet.
event_typesWhich events to send. ["*"] means all of them.
extra_headersStatic headers added to every delivery - a shared token for your own gateway, for example.
masked_fieldsFields this endpoint must never receive. See below.
max_attemptsAttempts before a delivery is abandoned. Default 5.
is_activeSet 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.

http·The request you receive
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
json·The body
{
  "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"
  }
}
FieldUse it for
delivery_idDeduplication. The same number on a retry; a new one for a genuinely new delivery.
event_typeRouting. A stable machine code.
timestampWhen the delivery was built (UTC).
payload.entity_type / entity_idWhich record changed - fetch it over the API if you need the full row.
payload.metadataEvent-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.

http·A representative selection
ContactCreated        ContactUpdated        ContactDeleted
LeadCreated           LeadStatusChanged     LeadConverted
DealCreated           DealStageChanged      DealWon            DealLost
TaskCreated           TaskUpdated
PipelineCreated       PipelineStageUpdated
TagCreated            SegmentUpdated        CampaignCreated

The 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.

bash·Mask on the way out
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.

Need technical help?

If your question is not answered here, get in touch with our team.

Contact us
Webhooks · Cliento Pro