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

9 min read Admin panel demo
On this page 11

Public API and webhooks

The REST API lets your CRM, website backend or own bot work with cenaly.ge 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.ge/public-api/v1
Method reference api.cenaly.ge/public-api/v1/docs
OpenAPI 3.1 api.cenaly.ge/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.ge/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":1450,"currency":"RUB"}}]}' \
  "$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": 1450 } }
}

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.

Check the call log#

If an integration stops receiving data, open Integrations → API & webhooks → Call log and select the key and period. The last 14 days are available; choose 1, 3 or 7 days for a shorter check.

Entries show the time and key, HTTP method, path without identifiers, response code, an error if present, and duration in milliseconds. Hover over the request result to see its IP address. Request and response bodies are not logged, so record contents and personal data are not duplicated in the technical history. The log lets you check a call's result, but cannot reconstruct the full exchange.

If a record is missing, check the selected key and expand the period to 14 days. Older calls are not displayed in this window.

Send sales to an external BI system#

A manager with locations in Moscow and Kazan wants sales in their own analytics system. A public API key does not expose sales: its permissions cover only leads, lead events, conversations, messages and segments. Even the Everything preset adds no order or receipt methods.

To send new order events, configure a scenario:

  1. Open Settings → Status automation → Scenarios (Flow) and click Add scenario.
  2. Under When, choose Order is paid. Other choices are Order is placed, Order status changes and Order is cancelled.
  3. Under Then, add Call webhook and enter your integration handler's address. Add conditions if needed.
  4. Wait for Saved, place a matching test order and check that your handler receives the event.

The result is a call to your handler when a matching event occurs. Your integrator sets up loading into BI and field mapping; a scenario does not replace an export of all past history. If the event does not arrive, check that the scenario is enabled, the event and all conditions match, and the handler's address is correct. The public API log covers incoming calls made with keys; check delivery of this outgoing webhook on the receiving side.

For a one-off analysis, use Export CSV in the order list or prepare a data archive. These are manual options, not a promise of scheduled BI refreshes.

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.
  • Call log displays the last 14 days. Request and response bodies are absent; method, path without identifiers, result, error, duration and IP help investigate failures without copying record contents.
  • 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.