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

    How a request is built

    The address starts with https://api.clomni.ai/v1, the token goes in the Authorization: Bearer <token> header, and the body is JSON (Content-Type: application/json). The address has no workspace number.

    • A conversation's id in the API is the same as the number shown in the dashboard: “conversation 1042” means GET /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.
  2. 2

    Read conversations and messages

    GET /conversations (scope conversations:read) returns conversations in pages of 25. Parameters: status (open, resolved, pending, snoozed, all, default open), 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_reply field shows whether you can send the customer a free-form message right now. Messages: GET /conversations/{conversation_id}/messages (scope messages:read) gives the last 20 messages, and for older ones use ?before=<smallest id>. message_type: 0 incoming, 1 outgoing, 2 system note, 3 template. status: sent, delivered, read, failed.

  3. 3

    Write a reply and manage the conversation

    POST /conversations/{conversation_id}/messages, scope messages: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 status is sent at first, and the result comes later with the message.updated webhook. When it fails, the reason is in content_attributes.external_error. For a private note the customer does not see, send "private": true (scope notes:write).

    • Resolve or open: POST /conversations/{conversation_id}/toggle_status with { "status": "resolved" } (scope conversations:write).
    • Assign: POST /conversations/{conversation_id}/assignments with { "assignee_id": 12 } or { "team_id": 3 } (scope conversations:assign).
    • Lists of agents and teams: GET /agents, GET /teams.
  4. 4

    Create and search contacts

    POST /contacts (scope contacts: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"}'

    identifier is 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-10045 searches by name, email, phone and identifier. A second contact with the same email or phone returns 422: search first, then update. custom_attributes keys must be created in the workspace beforehand.

  5. 5

    Pagination: regular lists

    • GET /conversations: page (from 1), pages of 25, response data.meta and data.payload.
    • GET /contacts: page, pages of 15, response meta.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. 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 (the next_cursor from the previous response), limit (1-100, default 50). Response: data, next_cursor, has_more.

    1. First load: start without updated_since and continue with after=next_cursor until has_more: false.
    2. Save the largest updated_at you received.
    3. Next time, updated_since = the saved time minus 2 minutes.
    4. Write each object by id (upsert) and drop duplicates.
    5. Get new events right away through webhooks, and run sync every 5-15 minutes.

    In sync objects a conversation's id is the internal ID, and the dashboard number is in number (conversation_number on 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.

  7. 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-Key header, and send the same one on a retry.

    • It works on all /v1 POSTs 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: 409 idempotency_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.

  8. 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. Scope events:read plus the topic's read scope (messages:read, conversations:read, contacts:read), otherwise the list comes back empty. Parameters: types (for example message.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.

Document