Browse Developers
Build with OnloView as Markdown

Conversations API

Create a conversation for one of your users from your own backend, receive its durable Onlo id, set conversation attributes, and search that user’s conversations over REST.

For
Backend engineers integrating Onlo from a server or migration job
Needs
Everything here runs server-side. The API key is a long-lived secret and must never reach browser or mobile code.
Time
15 minutes

Before you start

  • An Onlo organization on a plan that includes API access
  • A full-access API key from Dashboard → Integrations → API
  • A stable identifier for each of your users, and one for each source conversation you will create

What this API does

The Conversations API gives your backend one path to record a customer conversation in Onlo and find it again later. You create a conversation, Onlo returns a durable UUID, and you store that UUID against your own record.

It is deliberately narrow. Creating a conversation writes an empty conversation — it sends no message, and it never triggers the AI, a playbook, an action, a ticket, or any outbound delivery. If you are looking for the AI to answer a customer, that happens through a connected channel or the Messenger SDK, not here.

OperationEndpointKey scope
Test your credentialsGET /pingread or full
Create a conversation for a userPOST /conversationsfull
Fetch one conversationGET /conversations/{id}read or full
Set or remove custom attributesPUT /conversations/{id}full
Search one user’s conversationsPOST /conversations/searchread or full

If you are coming from Intercom

This API deliberately matches Intercom’s wire shape, so most Intercom client code carries over: bearer auth, snake_case fields, a `type` discriminator on every resource, `conversation.list` with `pages.next.starting_after`, the `error.list` error envelope, integer Unix-epoch-second timestamps, `PUT` to update, and the `query`/`operator`/`field`/`value` search grammar.

Three differences will affect your code on day one, and there is a dedicated page for the full list.

  • Creating a conversation does NOT take a `body` and does NOT create a message. Intercom requires one.
  • Every search must filter on exactly one contact. Intercom lets you search a whole workspace.
  • Creating a conversation is idempotent on your `external_id`. Intercom has no idempotency, so a retry there duplicates.

Base URL

All requests go to `https://onlo.ai/api/v1`. There is no API subdomain — if you have seen `api.onlo.ai` or `api.onlo.app` referenced anywhere, those hosts do not exist and will not resolve.

The three identifiers you need

Your identifiers are opaque and case-sensitive. Onlo stores them exactly as sent and rejects leading or trailing whitespace rather than trimming it, so one string in your system can never become two different keys.

ValueWho creates itExampleWhat you do with it
`from.external_id`You`customer_123`Your stable id for one user. Reuse it on every create and every search for that user.
`external_id`You`legacy_conv_987`Your stable id for one source conversation. It is also the idempotency key — reuse the exact same value on retries.
`id`Onlo`9a4f18f5-93ac-4476-b87f-6af63c700c64`The durable Onlo conversation UUID. Persist it; you need it to update or fetch the conversation.

Make your first three calls

  1. Step 1

    Confirm your key works

    Call `/ping` before anything else. It verifies the key, your plan, any IP allowlist, and reports which scope the key carries.

    Connection testshell
    export ONLO_API_BASE_URL="https://onlo.ai/api/v1"
    export ONLO_API_TOKEN="olk_live_replace_with_your_key"
    
    curl --request GET "$ONLO_API_BASE_URL/ping" \
      --header "Authorization: Bearer $ONLO_API_TOKEN" \
      --header "Accept: application/json"

    Expected resultA `200` response containing your `organization_id` and `scope`.

  2. Step 2

    Create a conversation and store the id

    Send your source conversation id as `external_id` and your user id as `from.external_id`. If that user does not exist in Onlo yet, this creates them.

    Createshell
    curl --request POST "$ONLO_API_BASE_URL/conversations" \
      --header "Authorization: Bearer $ONLO_API_TOKEN" \
      --header "Content-Type: application/json" \
      --data '{
        "external_id": "legacy_conv_987",
        "from": {
          "type": "contact",
          "external_id": "customer_123",
          "name": "Sarah Chen",
          "email": "sarah@example.com"
        },
        "custom_attributes": { "order_id": "ord_456", "plan": "pro" }
      }'

    Expected resultA `201` response whose `id` is the Onlo conversation UUID. Persist the mapping from your `external_id` to that `id` before you treat the record as migrated.

  3. Step 3

    Set attributes, then search for them

    Use `PUT` with the returned id to set business fields you want to look up by later, then search for that user filtered on those fields.

    Set attributesshell
    curl --request PUT "$ONLO_API_BASE_URL/conversations/9a4f18f5-93ac-4476-b87f-6af63c700c64" \
      --header "Authorization: Bearer $ONLO_API_TOKEN" \
      --header "Content-Type: application/json" \
      --data '{
        "custom_attributes": {
          "order_id": "ord_789",
          "is_vip": true,
          "plan": null
        }
      }'
    Searchshell
    curl --request POST "$ONLO_API_BASE_URL/conversations/search" \
      --header "Authorization: Bearer $ONLO_API_TOKEN" \
      --header "Content-Type: application/json" \
      --data '{
        "query": {
          "operator": "AND",
          "value": [
            { "field": "contact.external_id",        "operator": "=", "value": "customer_123" },
            { "field": "custom_attributes.order_id", "operator": "=", "value": "ord_789" }
          ]
        },
        "pagination": { "per_page": 50, "starting_after": null }
      }'

    Expected resultThe `PUT` returns the full conversation with the updated `custom_attributes`; the search returns that conversation inside a `conversation.list`.

Run the three calls above in order with a real full-access key.

Expected result

The create returns `201` with an `id`; the `PUT` returns that same `id` with your attributes applied; the search returns exactly that conversation with `total_count: 1`.

If you don't see this
  • A `401` means the key is missing, malformed, revoked, or pasted with surrounding whitespace — re-copy it from the dashboard.
  • A `403 insufficient_scope` means you used a read-only key for create or `PUT`. Create a full-access key.
  • A `403 api_plan_restricted` means your plan does not include API access. Contact your organization owner.
  • A search returning an empty list means the contact was not found. Confirm you sent the same `from.external_id` you created the conversation with — matching is exact and case-sensitive.

Complete backend example

The full loop, including the retry-safety and pagination behavior you should copy rather than reimplement. Run it only from a trusted backend.

Node.js 18+typescript

Requires ONLO_API_TOKEN in the server environment.

const baseUrl = process.env.ONLO_API_BASE_URL ?? 'https://onlo.ai/api/v1';
const token = process.env.ONLO_API_TOKEN;
if (!token) throw new Error('ONLO_API_TOKEN is required');

async function callOnlo(path: string, options: RequestInit = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    ...options,
    signal: AbortSignal.timeout(30_000),
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
      ...(options.body ? { 'Content-Type': 'application/json' } : {}),
      ...options.headers,
    },
  });

  const body = await response.json().catch(() => ({}));
  if (!response.ok) {
    // Errors arrive as Intercom's error.list: errors is an array.
    const first = body.errors?.[0];
    const requestId = response.headers.get('X-Onlo-Request-Id');
    throw new Error(`${response.status} ${first?.code ?? 'request_failed'} ${first?.message ?? ''} request_id=${requestId ?? 'missing'}`);
  }
  return { body, response };
}

// 1. Create an empty conversation and keep the durable Onlo id.
const { body: created, response: createResponse } = await callOnlo('/conversations', {
  method: 'POST',
  body: JSON.stringify({
    external_id: 'legacy_conv_987',
    from: { type: 'contact', external_id: 'customer_123', name: 'Sarah Chen' },
    custom_attributes: { order_id: 'ord_456' },
  }),
});

const conversationId = created.id;
const wasReplay = createResponse.headers.get('X-Onlo-Idempotent-Replay') === 'true';
// Persist legacy_conv_987 -> conversationId now. If THAT write fails, retry this
// exact request: the same id comes back with wasReplay === true.

// 2. Set attributes. A null value removes that key.
await callOnlo(`/conversations/${encodeURIComponent(conversationId)}`, {
  method: 'PUT',
  body: JSON.stringify({ custom_attributes: { order_id: 'ord_789', plan: null } }),
});

// 3. Page through every match for this contact.
const conversations = [];
let startingAfter: string | null = null;
for (;;) {
  const { body: page } = await callOnlo('/conversations/search', {
    method: 'POST',
    body: JSON.stringify({
      query: {
        operator: 'AND',
        value: [{ field: 'contact.external_id', operator: '=', value: 'customer_123' }],
      },
      pagination: { per_page: 50, starting_after: startingAfter },
    }),
  });
  conversations.push(...page.conversations);
  // `next` is ABSENT on the last page, so this loop terminates on its own.
  if (!page.pages?.next) break;
  startingAfter = page.pages.next.starting_after;
}

Where to go next