C cenaly.com
Start free

Public API and webhooks

Scoped keys, REST API for leads, properties, events, conversations and segments, webhooks for new leads and events — with curl examples

7 min read Admin panel demo
On this page 9

Public API and webhooks

The REST API lets your CRM, website backend or own bot work with cenaly.com leads and conversations the same way the admin panel does: read cards, change properties, send events, reply in conversations. Webhooks work the other way round — they notify your server about new leads, events and messages.

What you need#

What Where
Role account owner — the key acts on their behalf; staff do not see keys
Section chat enabled (leads and conversations live there)
Key Integrations → API & webhooks → Keys → Create key
API address https://api.cenaly.com/public-api/v1
Method reference api.cenaly.com/public-api/v1/docs
OpenAPI 3.1 api.cenaly.com/public-api/v1/openapi.json

Step 1. Key and permissions#

  1. Open Integrations and click API & webhooks in the page header.
  2. On the Keys tab click Create key and enter a name (“CRM”, “Website backend”, “Telegram bot”).
  3. Pick a permission set or tick permissions one by one; with several locations — which locations the key may use; and the expiry.
  4. Copy the cnk_… secret. It is shown only once: we keep only its fingerprint (SHA-256).
Permission What it allows
contacts:read list, search and card of a lead, properties
contacts:write change properties and tags, merge, erase on a data subject request
events:read the event feed of a lead
events:write send events (purchase, sign-up, anything of your own)
conversations:read conversations and messages
conversations:write operator reply, note, status, assignee, tags
messages:send a message to the lead’s latest conversation
segments:read saved segments and their members

Sets: CRM / backend (leads, properties, events, reading conversations and segments), Own bot (reads leads, replies in conversations, writes to leads), Read only, Everything.

  • Permissions change the key’s permissions and locations on the fly — the integration keeps working.
  • Reissue gives a new secret; the old one stops working immediately, permissions and the log stay.
  • Revoke — immediately and for good, the next request gets 401.

Pass the key in the Authorization: Bearer cnk_… header (or X-Api-Key: cnk_…). Keep it on your server only — never in a browser or a mobile app.

Step 2. First requests#

KEY=cnk_your_key
API=https://api.cenaly.com/public-api/v1

# what the key can do: permissions, locations, limit
curl -H "Authorization: Bearer $KEY" "$API/me"

# leads, newest first (20 per page)
curl -H "Authorization: Bearer $KEY" "$API/contacts?limit=20"

# search by name, email or phone
curl -H "Authorization: Bearer $KEY" "$API/contacts?q=anna"

The response is always an envelope:

{
  "meta": { "status": 200, "requestId": "…", "nextCursor": "WyIyMDI2LTEwLTA0…" },
  "data": { "total": 128, "contacts": [ { "id": "…", "status": "lead", "name": "Anna", "updatedAt": "2026-10-04T09:12:00Z" } ] }
}

The next page is the same request with cursor=<meta.nextCursor>; no nextCursor — no more pages. An error is {"meta": {...}, "error": {"code": "…"}}.

Location. If the key has one location, you do not need to pass it. With several — add location=<location id> (the list is GET /v1/locations), otherwise you get 400 location_required.

Properties and events by your user_id. You do not have to store our lead id: add ?by=user_id, and your user identifier becomes the address. No lead with that user_id yet — we create one.

# properties: operations update_or_create, set_once, add, delete, append, union, exclude
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"ops":[{"op":"update_or_create","key":"$email","value":"anna@example.com"},
              {"op":"add","key":"orders","value":1},
              {"op":"union","key":"interests","value":"yoga"}]}' \
  "$API/contacts/u-1001/props?by=user_id"

# events in a batch (up to 100); a repeat with the same id counts once
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"events":[{"id":"ord-5531","userId":"u-1001","name":"order_paid","props":{"sum":45,"currency":"GEL"}}]}' \
  "$API/events"

# reply in a conversation and change its status
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"text":"Hello! Your order is on its way."}' "$API/conversations/<id>/reply"
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"status":"resolved"}' "$API/conversations/<id>/status"

Writes go through the same queue as edits from the panel: changes apply within seconds, and a write by user_id answers 202 with pending. In the lead card the author of the change is shown as “API: ”.

Methods#

Method What it does Permission
GET /v1/me, GET /v1/locations key, permissions, locations —
GET /v1/contacts leads: status (anon, lead, user), q, limit, cursor contacts:read
GET /v1/contacts/{id} card: properties, tags, event summary contacts:read
POST /v1/contacts/{id}/props properties via operations (up to 250) or {"props":{…}} contacts:write
POST / DELETE /v1/contacts/{id}/tags lead tags contacts:write
POST /v1/contacts/merge merge two cards: preview, then "apply": true contacts:write
POST /v1/contacts/{id}/erase erase on a data subject request ("confirm": true) contacts:write
GET /v1/contacts/{id}/events the lead’s event feed events:read
POST /v1/contacts/{id}/events, POST /v1/events events (up to 100 at once) events:write
GET /v1/contacts/{id}/conversations the lead’s conversations conversations:read
POST /v1/contacts/{id}/messages write to the lead’s latest conversation messages:send
GET /v1/conversations, /{id}, /{id}/messages conversations and messages conversations:read
POST /v1/conversations/{id}/reply operator reply or a note ("type": "note") conversations:write
POST /v1/conversations/{id}/status, /assign, /tags status, assignee, tags conversations:write
GET /v1/segments, GET /v1/segments/{id}/contacts segments and their members segments:read

Fields, examples and error codes of every method are in the reference.

Step 3. Webhooks#

Webhooks are set up per location: Chat → Settings → Webhooks — an https address, events and a signing secret. Lead events were added next to the conversation events (new conversation, new message, status, rating, tags):

Event When it arrives
lead.created a new lead appeared (contact left in the chat, a form, or created via the API)
lead.props_changed lead properties changed — the keys are in changedProps
lead.event_tracked the lead has an event; by default all your events without system ones ($…), the list of names is set in “Which events to send”
message.goal_reached the lead reached a message goal (pop-up, campaign)
{
  "event": "lead.event_tracked",
  "deliveryId": "WD…",
  "at": "2026-10-04T09:12:03Z",
  "webhookId": "…",
  "locationId": "…",
  "contact": { "id": "…", "status": "user", "userId": "u-1001", "props": { "orders": 3 }, "tags": ["vip"] },
  "leadEvent": { "name": "order_paid", "at": "2026-10-04T09:12:00Z", "props": { "sum": 45 } }
}

The lead’s name, email and phone go into the body only if the webhook has “Send guest contacts” ticked. Verify the signature with HMAC-SHA256 of the body using the webhook secret:

# header X-Meni-Signature: sha256=<hex>
echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET"

Answer 2xx within 5 seconds — otherwise the delivery is retried; after a long run of failures the webhook is switched off, and you see it in the settings. Drop duplicates by deliveryId.

Limitations#

  • 60 requests per minute per key; above that — 429 with a Retry-After header. What is left — in X-RateLimit-*.
  • Up to 200 records per page (50 by default), up to 100 events per batch, up to 250 property operations per request, up to 30 tags at once.
  • Up to 20 live keys per account.
  • A key’s call log is kept for 30 days: time, method, path template (no ids or body), response code, duration.
  • Events sent via the API land in the lead card (feed, summary, segments), but not in the raw visit analytics log.
  • You can write to a lead only in an existing conversation: no conversation — 409 no_conversation.

Troubleshooting#

Response What it means What to do
401 api_key_missing / api_key_invalid no key in the request, or it is wrong, revoked or expired check the header; reissue the key if needed
403 scope_required the key lacks the permission Permissions of the key — add it
400 location_required the key has several locations add location=<id>
404 location_not_found the location is not among the key’s locations check GET /v1/locations
404 not_found no such lead or conversation for your own ids use ?by=user_id
429 rate_limited more than 60 requests per minute wait Retry-After seconds, batch events
502 upstream_error temporary failure retry later

Every request is visible on the Call log tab — with the response code and the key name.

FAQ#

Can I call the API from a browser? No: the key gives access to all leads of a location. Call the API from your server.

How is user_id different from a lead id? A lead id is our card identifier. user_id is your user identifier (a CRM number, a website account id); with ?by=user_id you can address the card by it.

What happens if I send an event twice? An event with the same id counts once — feel free to retry the request after a network error.

Do I need a separate key per location? No: one key can work with several locations, the location is chosen with the location parameter.

Was this article helpful?