Using Developer API endpoints: examples, pagination, idempotency
Last updated: 11 November 2026 · 8 min read
An endpoint is one address and one operation of the API: a method (GET to read, POST to create, PATCH to change, DELETE to delete) and a path. All endpoints, their parameters and response examples are on the “API reference” page of developer.clomni.ai. Each endpoint lists the scopes it needs, and any one of them is enough. An endpoint that is not in the list is closed to developer tokens and returns 403 endpoint_not_available. Which endpoint and scope each job needs is in the “Which scopes you need: scopes and endpoints by what you want to do” article and in the “Scopes and endpoints: …” articles.
Steps
-
1
How a request is built
The address starts with
https://api.clomni.ai/v1, the token goes in theAuthorization: Bearer <token>header, and the body is JSON (Content-Type: application/json). The address has no workspace number.- A conversation's
idin the API is the same as the number shown in the dashboard: “conversation 1042” meansGET /conversations/1042. - Regular endpoints return times as Unix seconds, sync endpoints as ISO 8601 (
2026-11-08T16:00:00Z). The time zone is UTC. - Phone numbers are written in international format:
+994501234567.
- A conversation's
-
2
Read conversations and messages
GET /conversations(scopeconversations:read) returns conversations in pages of 25. Parameters:status(open,resolved,pending,snoozed,all, defaultopen),assignee_type(me,unassigned,assigned,all),inbox_id,page.curl "https://api.clomni.ai/v1/conversations?status=open&page=1" \ -H "Authorization: Bearer $CLOMNI_TOKEN"
const res = await fetch("https://api.clomni.ai/v1/conversations?status=open&page=1", { headers: { Authorization: `Bearer ${process.env.CLOMNI_TOKEN}` }, }); const { data } = await res.json(); for (const c of data.payload) console.log(c.id, c.status, c.meta.sender.name);import os, requests res = requests.get("https://api.clomni.ai/v1/conversations", params={"status": "open", "page": 1}, headers={"Authorization": f"Bearer {os.environ['CLOMNI_TOKEN']}"}, timeout=10) for c in res.json()["data"]["payload"]: print(c["id"], c["status"], c["meta"]["sender"]["name"])A conversation's
can_replyfield shows whether you can send the customer a free-form message right now. Messages:GET /conversations/{conversation_id}/messages(scopemessages:read) gives the last 20 messages, and for older ones use?before=<smallest id>.message_type:0incoming,1outgoing,2system note,3template.status:sent,delivered,read,failed. -
3
Write a reply and manage the conversation
POST /conversations/{conversation_id}/messages, scopemessages:send:curl -X POST "https://api.clomni.ai/v1/conversations/1042/messages" \ -H "Authorization: Bearer $CLOMNI_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2a9e-3b7d-4f0a-9c55-0e8b1d2a7f41" \ -d '{"content": "Hello, your order will be delivered today."}'The channel sends the message in the background, so
statusissentat first, and the result comes later with themessage.updatedwebhook. When it fails, the reason is incontent_attributes.external_error. For a private note the customer does not see, send"private": true(scopenotes:write).- Resolve or open:
POST /conversations/{conversation_id}/toggle_statuswith{ "status": "resolved" }(scopeconversations:write). - Assign:
POST /conversations/{conversation_id}/assignmentswith{ "assignee_id": 12 }or{ "team_id": 3 }(scopeconversations:assign). - Lists of agents and teams:
GET /agents,GET /teams.
- Resolve or open:
-
4
Create and search contacts
POST /contacts(scopecontacts:write):curl -X POST "https://api.clomni.ai/v1/contacts" \ -H "Authorization: Bearer $CLOMNI_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Leyla Hasanova", "phone_number": "+994501234567", "identifier": "crm-10045"}'identifieris the ID in your own system and is unique within the workspace.PATCH /contacts/{contact_id}changes only the fields you send.GET /contacts/search?q=crm-10045searches by name, email, phone andidentifier. A second contact with the same email or phone returns 422: search first, then update.custom_attributeskeys must be created in the workspace beforehand. -
5
Pagination: regular lists
GET /conversations:page(from 1), pages of 25, responsedata.metaanddata.payload.GET /contacts:page, pages of 15, responsemeta.count,meta.current_page,payload.GET /conversations/{conversation_id}/messages:before=<message_id>, 20 messages each time.
These lists are meant for showing on screen. Do not walk them to copy all the data: when a new conversation arrives between pages, the order shifts and objects can be lost.
-
6
Sync: copy data into your own database
GET /sync/conversations(conversations:read),GET /sync/messages(messages:read),GET /sync/contacts(contacts:read). Parameters:updated_since(ISO 8601),after(thenext_cursorfrom the previous response),limit(1-100, default 50). Response:data,next_cursor,has_more.- First load: start without
updated_sinceand continue withafter=next_cursoruntilhas_more: false. - Save the largest
updated_atyou received. - Next time,
updated_since= the saved time minus 2 minutes. - Write each object by
id(upsert) and drop duplicates. - Get new events right away through webhooks, and run sync every 5-15 minutes.
In sync objects a conversation's
idis the internal ID, and the dashboard number is innumber(conversation_numberon a message). File links are short-lived. Sync is limited to 60 requests a minute. To move the whole history once,POST /exports(exports:run, 1 job at a time, 6 a day) fits better. - First load: start without
-
7
Against duplicate sends: Idempotency-Key
When the network drops, you cannot tell whether a POST went through. Make a new UUID for each new operation and send it in the
Idempotency-Keyheader, and send the same one on a retry.- It works on all
/v1POSTs except/oauth/*. The key is 1-255 characters and is kept for 24 hours per app and workspace. - Same key, same body: the operation does not run again, and the stored response comes back with
Idempotency-Replayed: true. - Same key, different body: 409
idempotency_key_reused. If the first request is still running: 409idempotency_key_in_progress. - 5xx responses are not stored, so you can retry with the same key.
Messages also have
content_attributes.client_echo_id: a request with the same value in the same conversation within 24 hours does not create a new message. - It works on all
-
8
Event feed: GET /v1/events
It returns the events of the last 30 days (the same as in the webhooks, with the same
id), oldest first. Scopeevents:readplus the topic's read scope (messages:read,conversations:read,contacts:read), otherwise the list comes back empty. Parameters:types(for examplemessage.created,contact.updated),after,limit(1-100). If your server has no public address, read the feed every minute. If it has one, pick up here what the webhook missed.