Public API and webhooks
The REST API lets your CRM, website backend or own bot work with meni.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.meni.ge/public-api/v1 |
| Method reference | api.meni.ge/public-api/v1/docs |
| OpenAPI 3.1 | api.meni.ge/public-api/v1/openapi.json |
Step 1. Key and permissions#
- Open Integrations and click API & webhooks in the page header.
- On the Keys tab click Create key and enter a name (“CRM”, “Website backend”, “Telegram bot”).
- Pick a permission set or tick permissions one by one; with several locations — which locations the key may use; and the expiry.
- 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.meni.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":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 —
429with aRetry-Afterheader. What is left — inX-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.
Related sections#
- Guest chat — conversations, leads and webhook settings
- Leads — the unified inbox
- API and MCP of your PBX — the separate telephony API
- Integrations