# AUTHORING Source: https://docs.open.cx/AUTHORING # Docs authoring spec Everything here is enforced by `pnpm lint-frontmatter`. A PR that touches any `.mdx` under `docs/` fails CI if the changed files don't conform. Mintlify's built-in search ranks pages on four signals: `title`, `description`, `keywords`, and body text. Getting the first three right is the difference between "users find what they need in one search" and "users give up and open a support ticket". Treat frontmatter as a search surface, not metadata. ## Required frontmatter Every page — content pages and API reference alike — must ship this frontmatter: ```yaml theme={"dark"} --- title: 'Zendesk overview' sidebarTitle: 'Overview' description: 'Run Zendesk as the surface your agents work in while OpenCX resolves, routes, and grounds every conversation in your Help Center.' icon: 'ticket' keywords: - zendesk - zendesk integration - zendesk handoff - connect zendesk - zd tickets - help center sync - sunshine conversations - zendesk 401 --- ``` ### `title` — unique, descriptive * **Never** a bare word from this denylist: `Overview`, `Troubleshooting`, `Connect`, `Index`, `Introduction`, `Get started`, `Configuration`, `Setup`. * Format: ` ` — e.g. `Zendesk overview`, `Notion troubleshooting`, `Shopify connect`, `Voice agent introduction`. * The title is what Mintlify's search results show. Two pages titled `Overview` are indistinguishable to a searching user. * Use `sidebarTitle` if you want the sidebar to stay short (`Overview`); it doesn't affect search ranking. ### `description` — 120–160 chars, "what + when" * Answer two questions: *what does this page do* and *when should I read it*. * Example: `Connect Zendesk as a knowledge source to sync help center articles and tickets into OpenCX agents. Use when onboarding an existing Zendesk account.` * Shows up in search results underneath the title and as the `` for SEO. * Hard lint range: 80–200 chars. Target range: 120–160. ### `keywords` — 8–15 entries, four categories Pick from these buckets — at least one entry from each of the first three: 1. **Product / aliases (2–3)** — product name + common abbreviations or misspellings. `zendesk`, `zd`, `zen desk`. 2. **Task phrases (2–3)** — verbs a user would type. `connect zendesk`, `sync zendesk tickets`, `zendesk oauth`. 3. **Synonyms (2–3)** — alternate terminology. `help desk integration`, `ticket sync`, `zendesk crm`. 4. **Error / failure terms (1–2, when relevant)** — symptoms users search when something breaks. `zendesk 401`, `subdomain invalid`, `token expired`. Rules of thumb: * **Don't duplicate keywords from a parent page.** If `integrations/zendesk/overview.mdx` already owns `zendesk integration`, don't repeat it on `integrations/zendesk/channels/email.mdx` — that page should own `zendesk email`, `zendesk email channel`, etc. * **Don't over-keyword.** More than 20 entries dilutes ranking. * **Use phrases, not single words, for specificity.** `zendesk email` beats `email` in a large corpus. * **Mirror real user language.** Steal phrasing from support tickets, Slack questions, competitor docs. ## Author PR checklist Paste into your PR description: * [ ] `title` unique and descriptive (not bare `"Overview"` / `"Troubleshooting"`) * [ ] `description` 120–160 chars, answers *what + when* * [ ] `keywords` 8–15 entries with ≥1 alias, ≥1 task phrase, ≥1 synonym * [ ] Searched the page title + 2 alternate phrases in `pnpm dev` and confirmed top-3 ranking * [ ] No keyword overlap with parent / sibling pages that would dilute ranking ## Tools * `pnpm dev` — run Mintlify locally on port 3009 and test search interactively. * `pnpm lint-frontmatter` — runs the frontmatter lint. Fails on missing title/description/keywords, denylisted titles, and out-of-range description lengths. * `pnpm lint-frontmatter:fix` — auto-patches trivially-fixable cases (e.g. adds `sidebarTitle` when rewriting `title`). * `pnpm suggest-frontmatter -- ` — prints a suggested keyword set and rewritten title for one page. * `pnpm suggest-frontmatter -- --missing-only` — lists every file currently missing `keywords`. * `pnpm enrich-api-stubs -- ` — fills in a stub API page from the OpenAPI spec. * `pnpm benchmark-search` — runs the 25-query benchmark against `http://localhost:3009` and prints a scorecard. ## Opt-out For the rare page where the rules genuinely don't apply (landing page, coming-soon stub), add this comment as the very first line of the MDX body (after the closing `---`): ```mdx theme={"dark"} {/* lint-frontmatter-disable: */} ``` Use sparingly. The lint output lists every opt-out so they stay visible. # Authentication Source: https://docs.open.cx/actions/authentication Secure action calls — store org-wide secrets as global variables or named secrets, pass per-user tokens from the widget, and rely on context headers OpenCX attaches automatically. Your action endpoint decides who's allowed to call it. OpenCX gives you four ways to reach that decision — pick one or combine them. ## The Four Paths | Path | Use it for | Where it lives | | - | - | - | | **Named secrets** | A credential one action (or a few) needs — referenced per header, sent only where you put it | [Settings → Secrets](https://platform.open.cx/settings) | | **Global variables** | Credentials every customer shares — a server-to-server API key, a signing secret | [Settings → Variables](https://platform.open.cx/settings) | | **Widget headers** | Credentials that differ per customer — a session token, a per-user JWT | Your frontend, passed to the widget at init | | **Context headers** | Identifying the OpenCX-side session / org / contact to your API | Attached automatically by OpenCX | ## Named Secrets Define a credential once, then reference it from any action header as `{{secrets.}}` — or pick it from the header editor's secret picker. Unlike global variables, a secret is sent **only** on the requests whose headers reference it, so one workspace can talk to many upstream systems without every call carrying every key. Values are **write-only**: encrypted at rest, never returned by any API or screen after creation, and decrypted only at the moment a request actually fires. Forgot a value? Type a replacement — there is no way to read the old one back. MCP server auth headers accept the same `{{secrets.}}` references. Go to **[Settings → Secrets](https://platform.open.cx/settings)**, choose a name, paste the value. The name is permanent — actions reference secrets by name. In the action's header editor, set the value to `{{secrets.}}` or use the picker. A header whose secret is missing is dropped, never sent raw. ## Global Variables Store once, OpenCX injects the value into every action call for your organization. Values are **encrypted at rest** and **decrypted per request**. Go to **[Settings → Variables](https://platform.open.cx/settings)**. Set a **key** and **value**. The key becomes the HTTP header name on every outbound action call. Nothing further to wire up — any action whose endpoint sits behind that header will authenticate. Use a full `Authorization` header value (e.g. `Bearer `) when your API expects Bearer auth — OpenCX sends the value verbatim, it does not prepend `Bearer `. ## Widget Headers Pass per-user credentials from your frontend at widget-init time. OpenCX uses them only for that contact's action calls and does not persist them. ```javascript theme={"dark"} const options = { token: 'YOUR_COPILOT_TOKEN', headers: { Authorization: `Bearer ${userJwt}`, }, }; ``` Widget headers **override** same-named global variables for that contact's session. Use this when each signed-in customer needs their own scoped token. ## Context Headers When your org has context headers enabled, OpenCX adds four identifying headers to every action call: | Header | Value | | - | - | | `x-opencx-org-id` | Your organization UUID | | `x-opencx-session-id` | The conversation session UUID — the trace key | | `x-opencx-ticket-number` | Numeric ticket ID for the session | | `x-opencx-external-user-id` | The contact's external ID if they're authenticated | Your API can use these to: * **Trace** an action call back to a specific conversation in the [OpenCX inbox](https://platform.open.cx/inbox) — the session ID is a clickable link. * **Rate-limit** per org without customers passing their own org ID as a parameter. * **Scope** responses — e.g. return a contact's orders based on `x-opencx-external-user-id` instead of trusting a query parameter the AI extracted. Context headers are opt-in at the org level. If you need them on, ask support to enable automatic action headers for your org. Every action call also carries `User-Agent: OpenCX` so your firewall and logs can filter. ## Choosing A Path | Situation | Path | | - | - | | Every customer hits the same backend with the same server-to-server key | **Global variable** | | Each signed-in customer has their own scoped token | **Widget header** (with a global variable for any shared service-account header) | | Your API needs to know which OpenCX session / org / contact made the call | **Context headers** | ## Security Best Practices Treat `x-opencx-org-id` as a signal, not a credential — pair it with a global-variable shared secret your API also checks. That way a leaked URL alone can't forge OpenCX traffic. Redact anything matching `Authorization`, `*-Token`, `*-Key` in your access logs. OpenCX does not replay headers, but your own log pipeline and dashboards can. OpenCX refuses to call `http://` URLs. If your endpoint isn't behind TLS today, put it behind a gateway that is. Update the global variable value — no action edit needed. Existing in-flight tool calls finish against the old value; the next call uses the new one. *** ## Related Documentation Form fields, headers, parameters, test. How headers reach your API at call time. 401s, 403s, and signal-header gotchas. Full widget auth model including identity verification. # Build An Action Source: https://docs.open.cx/actions/build-an-action Create an HTTP action in the OpenCX dashboard — name, endpoint, parameters, headers, test, and enable on a channel. Build an action once, reuse it everywhere the AI runs. You author each action from **[Actions](https://platform.open.cx/actions)** in your [OpenCX dashboard](https://platform.open.cx). ## Prerequisites * An **endpoint** the AI can reach over HTTPS. OpenCX does not call `http://` URLs. * A **description** of what the endpoint does — the AI reads it to decide when to call. * Any **credentials** the endpoint expects. See [Authentication](/actions/authentication) for where secrets live. ## Create The Action Go to **[Actions → New action](https://platform.open.cx/actions/new)**. **Name** (minimum 5 characters) and **Description** (minimum 20 characters) are both required. The AI reads them together to decide whether the customer's request matches this action — treat the description as prompt, not internal notes. Good: *"Look up an order by its numeric order ID and return shipping status, carrier, and tracking number."* Bad: *"Order stuff."* Pick the HTTP **Method** (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) and paste the full **API Endpoint** URL. For path parameters, use curly braces — for example `https://api.acme.com/orders/{order_id}`. The AI fills `order_id` from the conversation. For `POST` / `PUT` / `PATCH`, paste a JSON schema describing the body. If you don't have a schema, paste a sample JSON object and OpenCX infers the schema. Each field becomes a parameter the AI extracts from the conversation. Add static **Headers** (e.g. `Content-Type: application/json`) and per-call **Parameters** (query or path). Each row has a **Key** and **Value**. Keep identifiers that stay the same on every call, such as an account ID, as fixed query or path parameter values. Leave customer-specific inputs, such as an order reference, dynamic. When managing actions through the API, fixed values are stored in `payload.parameters[].value`; preserve them when [updating an action](/api-reference/actions/update). Click the wand icon on a value to turn it into a magic field. Magic fields are filled from conversation context automatically, so the AI doesn't need to guess them. By default every action is visible to every contact. To restrict — for example a refund action only VIPs should trigger — pick one or more [segments](https://platform.open.cx/contacts) under **Restricted to segments**. OpenCX filters the tool list on the server before the AI sees it, so restricted actions never reach unauthorized contacts. Toggle the channels the AI can use this action on — widget, email, SMS, WhatsApp, Instagram, Messenger, X (Twitter), phone, Slack, and more. A brand-new action defaults to widget only. Click **Test** to send a sample request with placeholder context headers and your saved global variables. A 2xx response means the HTTP contract works. **Test proves the HTTP call works — not that the AI will pick it.** Production firewalls, CORS, rate limits, and IP allowlists still apply. Always verify in a real conversation after enabling on a channel. ## Limits | | Value | | - | - | | **Request timeout** | 60 seconds — no retries | | **Supported methods** | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` | | **Transport** | HTTPS only | | **Body formats** | JSON, `multipart/form-data` | | **Path parameters** | `{curly_braces}` in the URL | | **Magic fields** | Available on both header values and parameter values | | **Per-org action count** | No hard limit today | ## Good To Know Only the URL and method are referenced at call time — you can change either without re-authoring parameters. Description changes take effect on the next conversation turn. Some actions need explicit customer confirmation — address changes, refunds, cancellations. The **Require form submission** toggle pauses the AI on a filled-in form the customer must submit before the call goes out. Once enabled, the requirement stays enabled when you edit other action settings; turn the toggle off explicitly to remove it. If two actions have overlapping names or descriptions, the AI may pick the wrong one. Sharpen the description to include the **noun** (order, account, subscription) and the **verb** (look up, cancel, update). Avoid generic names like `get_data`. *** ## Related Documentation Where secrets live, how headers reach your API. Bulk-author from a spec, keep in sync from CI. Timeout, retries, response handling, failure modes. Common failure patterns and fixes. # Execution Source: https://docs.open.cx/actions/execution How an action call runs end to end — when the AI picks a tool, what reaches your API, the 60-second timeout, and how failures get back to the AI. You built the action. Here's what happens on the wire when the AI decides to call it. ## Path Of A Call ```mermaid theme={"dark"} flowchart LR A([Customer message]) --> B[Filter by segment] B --> C[AI picks a tool] C --> D[OpenCX calls your API] D --> E[Response back to AI] E --> F([Reply to customer]) style A fill:#1a5c20,color:#fff,stroke:#267a2e style F fill:#1a5c20,color:#fff,stroke:#267a2e ``` Before the AI sees any tools, OpenCX filters the action list by the contact's segment memberships. An action with an empty **Restricted to segments** list is visible to every contact; a restricted action reaches only contacts in a matching segment. The AI reads each action's **name** and **description**, matches them against the conversation, and picks zero or one tool per turn. Fuzzy or overlapping descriptions lead to wrong picks — tighten the description if the AI picks an adjacent action. OpenCX sends an HTTP request from its infrastructure to your endpoint. Headers include `User-Agent: OpenCX`, any action-level headers, any global variables, any widget-passed headers, and (if enabled) the `x-opencx-*` context headers. Path and query parameters are filled with values the AI extracted from the conversation. OpenCX reads the full response body as text and hands it to the AI along with the status code. Keep responses compact — large bodies dilute context and hurt reply quality. The AI incorporates the response into its reply. For channels like email or SMS, that reply is formatted appropriately before delivery. ## Timeout And Retries | | Value | | - | - | | **Timeout per call** | 60 seconds (hard, enforced with `AbortSignal.timeout`) | | **Retries on failure** | None — OpenCX does not retry | | **Concurrency** | One tool call per conversation turn — the AI waits for the response before replying | | **Circuit breaker** | None — every call hits your endpoint | If your endpoint needs longer than 60 seconds, expose a webhook pattern: the action kicks off work and returns a reference ID; a follow-up action (or a workflow) polls by that ID. ## How The AI Sees Failures OpenCX returns whatever happened, and the AI decides what to do next — retry, fall back to another action, or hand off to a human. | Failure | What the AI receives | | - | - | | **Timeout** | `responseStatus: 408`, body `Request timed out after 60000ms` | | **Network error** | The string `Something went wrong` | | **4xx or 5xx** | The upstream status + response body verbatim | | **Success** | The 2xx status + response body verbatim | Because the response body is read as plain text, your API should return short, human-readable error messages on 4xx (e.g. `"order 12345 not found"`). The AI can then explain the failure to the customer in its own words rather than echoing a stack trace. ## Good To Know The full body counts against the AI's context window. Paginate or summarize large lists server-side — return the three most recent orders, not the customer's entire order history. OpenCX does not reshape your response. JSON passes through as the raw text the AI reads. If you want pretty-formatted fields in the reply, mirror them in your response payload exactly. Every action call, including request parameters and response status, is recorded on the conversation session. Open any session in the [inbox](https://platform.open.cx/inbox) and expand the tool-call row to audit. *** ## Related Documentation The fields that decide what the AI sees. Where each header comes from at call time. When the call doesn't land as expected. What happens when the AI gives up after a failed action. # AI Actions Source: https://docs.open.cx/actions/introduction Let the AI do real work in your systems mid-conversation — look up orders, file tickets, update accounts, trigger workflows — instead of only replying with text. A customer asks "where's my order?". The AI calls your order-lookup endpoint, reads the status from the response, and replies with the real tracking number. That's an **action** — an HTTP call the AI decides to make, authored by you in the [OpenCX dashboard](https://platform.open.cx/actions). Ready to build? Skip to **[Build an action](/actions/build-an-action)**. ## Core Capabilities Actions are not keyword-triggered. The AI reads the action's **name** and **description** to decide whether the customer's request matches, then fills in parameters from the conversation. Write the description like you'd write a Jira ticket for a teammate — "look up the order status and return tracking details for a given order ID." Each action can be restricted to one or more contact segments. A refund action visible only to VIP customers, an internal-account-tools action visible only to staff contacts — all filtered on the server before the AI sees the tool list. Once an action is enabled on a channel, it's available there — widget, email, SMS, WhatsApp, Instagram, Messenger, X (Twitter), phone, Slack, and more. No per-channel re-authoring. Build an action from the dashboard form, or upload an OpenAPI spec and generate one action per operation. CI pipelines can push spec changes automatically. See [OpenAPI import](/actions/openapi-import). ## Where Actions Fit OpenCX exposes four kinds of tools to the AI. This doc set covers the first two. | Kind | Who authors it | Lives where | | - | - | - | | **HTTP actions** | You, in the [OpenCX dashboard](https://platform.open.cx/actions) | This doc set | | **MCP server tools** | An external MCP server you connect | [MCP servers](/actions/mcp-servers) | | **Integration actions** | Built in per integration (e.g. [Shopify AI Actions](/integrations/shopify/ai-actions)) | Under the relevant integration page | | **System actions** | Built-in capabilities (human handoff, send email, collect input, auto-resolve) | No configuration — always available | ## Next Step Name, endpoint, parameters, test, enable on a channel. *** ## Related Documentation Dashboard form, fields, test button, channel enablement. Connect an external MCP server and use its tools as actions. Global variables, per-user headers, automatic context headers. How a call runs — timeout, retries, response handling. Import failures, 401s, test-passes-prod-fails, AI not calling. # MCP Servers Source: https://docs.open.cx/actions/mcp-servers Connect an external MCP server so its tools become AI actions — OAuth or header auth, per-tool opt-out, and a preset gallery for Slack, HubSpot, Notion, and more. **Beta.** MCP servers are available now, but the surface is still settling — expect changes. Instead of authoring one HTTP action per endpoint, point OpenCX at an MCP server and every tool it publishes becomes available to your AI agents. Useful for vendors that already ship a remote MCP server — Slack, HubSpot, Notion, Stripe — where hand-authoring each operation as a separate action would just be re-describing their own tool definitions. ## Connect a Server Go to **[Actions](https://platform.open.cx/actions)** and add a server from the **MCP servers** section — either pick one from the preset gallery (OpenCX, Slack, HubSpot, Intercom, Linear, Notion, Stripe) or add a custom URL. A Streamable HTTP or SSE endpoint, e.g. `https://mcp.example.com/mcp`. OpenCX probes it to detect how it authenticates. Select **Our team — one shared account** to connect from Actions, or **Each user — their own account** to let signed-in widget customers connect individually. Ownership is fixed when the server is created. For a shared account, authenticate here. For personal accounts, follow [Widget connections](/widget/connections) to authenticate your customers and let them connect inside the widget. * **OAuth** — if the server advertises OAuth (discovery + dynamic client registration), OpenCX opens its authorization page. Approve, and you're redirected back connected. Tokens are refreshed automatically and kept out of the widget. Personal connections can optionally be [reused by your backend](/widget/authentication#reuse-a-customer-connection-in-your-backend). * **Request headers** — if the server expects a static credential instead (an API key, a bearer token), enter it as a header. Values are encrypted at rest and never displayed again; reference a [named secret](/actions/authentication#named-secrets) instead of pasting a raw value so the credential lives in one place. Slack's MCP server supports neither automatic path, so it uses a pre-registered OAuth client instead of discovery. Once connected, OpenCX lists every tool the server publishes. The server is enabled by default — flip it off to keep the connection and its tool list without letting agents call anything. ## Personal accounts in the widget Enable a personal server in Actions, then authenticate each customer through your backend. The widget asks the customer to connect only when a request needs access, and continues after authorization. Customers manage their own connections and saved approvals from **Connections** in the widget session list. Shared team connections are managed in Actions. For personal servers, use **Capabilities** in the server's add or edit dialog to describe the actions it supports. This helps the agent request a connection for relevant tasks before it can see the customer's authorized tools. Include useful limits, keep the description aligned with enabled actions, and leave out credentials and customer data. This description is guidance, not a guarantee of access; tools are checked after the customer connects. See [Widget connections](/widget/connections) for setup and [widget authentication](/widget/authentication#connect-each-customers-account) for account binding and optional restrictions. ## Per-Tool Control Open a connected server's detail page to see its live tool catalog, grouped by what each tool does — write, fetch, or other. Switch individual tools off, or a whole group at once. * The catalog is re-read from the server (`tools/list`) every time you open the page, so a tool the vendor adds later shows up already enabled. * Switching a tool off stores it in a disabled set on your connection — it stays off even after the vendor's catalog changes, until you turn it back on. * Write tools default to visible/open in the list since they're the ones worth auditing before an agent gets to call them; fetch tools default collapsed. ## How Agents Call MCP Tools Once enabled, an MCP server's tools are available to the AI agent exactly like any other action — the agent decides whether a tool matches the customer's request, same as for [HTTP actions](/actions/introduction). Tool names are namespaced as `__` so two servers can each expose a tool with the same raw name (e.g. two different `search` tools) without colliding. If a server is unreachable when the agent tries to list its tools, that server's tools are omitted without breaking the whole tool list; its detail page reports the failure reason inline. ## Troubleshooting | Symptom | Cause | | - | - | | "Reconnect required" badge | The OAuth grant expired or was revoked upstream. Reconnect from the server's detail page. | | Server shows connected but tools list is empty or errors | The server itself is unreachable or erroring on `tools/list` — check the vendor's status, then revisit the page to re-probe. | | A credential stopped working after you changed the server URL | Stored header values and OAuth tokens are pinned to the URL they were saved against. Repointing a server to a new host drops them — re-authenticate after changing the URL. | | A tool you expected to see is missing | It may be switched off in [per-tool control](#per-tool-control) above, or the vendor removed it from their catalog. | *** ## Related Documentation How actions fit alongside integration and system tools. Named secrets, global variables, and header auth for actions. The other way to bulk-add actions, from your own OpenAPI spec. General action failures — import errors, 401s, AI not calling. # OpenAPI Import Source: https://docs.open.cx/actions/openapi-import Generate OpenCX actions from an OpenAPI spec — upload from the dashboard or sync automatically from CI when the spec changes. If your product already exposes an OpenAPI spec, you can generate one OpenCX action per operation instead of authoring each by hand. **Import replaces every action previously generated from your spec.** Hand-authored actions in the dashboard are left alone. If you mix both, keep hand-authored actions in a clearly separate naming convention. ## What OpenCX Accepts | | Value | | - | - | | **OpenAPI versions** | 2.0, 3.0, 3.1 | | **File format** | JSON or YAML | | **Base URL** | Read from `servers[0].url`, or you can override at import time | | **Auth** | Operations call out auth in the spec, but **secrets** live in global variables — see [Authentication](/actions/authentication) | | **Default channel** | Imported actions are enabled on the widget channel only; toggle others from the action's page | ## Custom Extensions OpenCX reads three vendor extensions to pre-configure each action: | Extension | Effect | | - | - | | `x-open-require-form-submission` | When `true`, the AI pauses on a form the customer must submit before the call | | `x-open-pinned` | When `true`, the action is pinned at the top of the dashboard list | | `x-open-tags` | Array of tag strings applied to the action for dashboard filtering | Drop these at the operation level: ```yaml theme={"dark"} paths: /orders/{order_id}: get: operationId: getOrder summary: Look up an order by ID x-open-tags: ['orders', 'support'] x-open-pinned: true ``` ## Upload Go to **[Actions](https://platform.open.cx/actions)**, click **Import from OpenAPI**, and pick your `spec.json` or `spec.yaml`. Watch the actions list for new rows. If validation fails, the dashboard surfaces the first invalid operation — fix and re-upload. Use the public API endpoint `PUT https://api.open.cx/actions/import` with a multipart file upload. Auth is a bearer API key with the `actions:write` scope — generate one under **[Settings → Access](https://platform.open.cx/settings/access)**. See [API authentication](/api-reference/authentication). ### GitHub Actions ```yaml theme={"dark"} name: Sync OpenCX actions on: push: branches: [main] paths: - 'openapi.yaml' jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Push spec to OpenCX env: OPENCX_TOKEN: ${{ secrets.OPENCX_API_TOKEN }} run: | curl --fail-with-body \ -X PUT https://api.open.cx/actions/import \ -H "Authorization: Bearer $OPENCX_TOKEN" \ -F "file=@openapi.yaml" ``` ### GitLab CI ```yaml theme={"dark"} sync-opencx-actions: image: curlimages/curl:latest rules: - if: $CI_COMMIT_BRANCH == "main" changes: - openapi.yaml script: - | curl --fail-with-body \ -X PUT https://api.open.cx/actions/import \ -H "Authorization: Bearer $OPENCX_API_TOKEN" \ -F "file=@openapi.yaml" ``` The import runs in a single transaction — old actions generated from your previous spec are soft-deleted and new ones inserted atomically. No separate cleanup step is needed. ## What Fails And Why | Error | What it means | Fix | | - | - | - | | `failed_to_parse_spec` | The file isn't valid OpenAPI or the format doesn't match the extension | Run your spec through a validator; confirm JSON vs YAML | | `invalid_tool_schema` | One operation uses schema features the AI tool-call validator rejects (deeply nested `oneOf`, circular refs, non-JSON-Schema types) | Simplify the offending operation, remove custom types, re-upload | | `cannot_determine_api_endpoint` | No `servers[0].url` in the spec and no override passed | Add a `servers` block, or send `serverBaseUrl` with the API call | *** ## Related Documentation Hand-author a single action from the dashboard form. Where secrets live for imported actions. Timeout, retries, response handling. Import errors and post-import failures. # Actions troubleshooting Source: https://docs.open.cx/actions/troubleshooting Fix common AI Actions failures — OpenAPI import errors, 401s that pass in Test, production calls that never land, and AI skipping an action. Before debugging, have these ready: the **action name**, a **session ID** where it should have been called, and the **endpoint** you expect to receive traffic. Looking for setup? See [Build an action](/actions/build-an-action). For the runtime model, see [Execution](/actions/execution). ## Common Troubleshooting Scenarios | Symptom | Likely cause | What to do | | - | - | - | | **OpenAPI import fails with `invalid_tool_schema`** | One operation uses schema features the AI tool validator rejects — nested `oneOf`, circular `$ref`, non-JSON-Schema types | Simplify the offending operation, flatten `oneOf`, resolve circular refs, re-upload. See [OpenAPI import](/actions/openapi-import). | | **Endpoint returns 401 / 403 but Test works** | The **Test** call injects placeholder `x-opencx-*` context headers; your production middleware blocks them, or your bearer-token casing differs between the Test payload and the saved [global variable](/actions/authentication) | Allow `x-opencx-*` through your WAF. Confirm the global variable value matches the **full** `Authorization` string, including any `Bearer ` prefix. | | **Test passes but the production call never lands** | CORS (widget path only), firewall / IP allowlist, or provider-side rate limit on your production endpoint | Add OpenCX egress IPs to your allowlist — ask support for the current list. Check CORS for widget-initiated calls. Inspect your API gateway for rate-limit denials. | | **AI doesn't call the action at all** | Channel toggle off, segment restriction excludes this contact, or the description is too vague for the AI to match intent | Enable the channel on the action's page. Verify the contact's [segment memberships](https://platform.open.cx/contacts). Sharpen the description to name the noun + verb (e.g. *"look up an order by ID and return tracking details"*). | | **None of the above** | — | Capture the session ID and action name, contact support. | OpenCX enforces a **60-second timeout** and **does not retry**. Failures return to the AI as status codes or a short error string — see [Execution](/actions/execution) for the exact failure shapes. ## Limits Recap | | Value | | - | - | | **Request timeout** | 60 seconds (no retries) | | **Transport** | HTTPS only | | **Supported methods** | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` | | **Body formats** | JSON, `multipart/form-data` | ## Impact Of Misconfiguration Misconfigured actions fail silently from the customer's point of view — the AI either calls the wrong endpoint, gets an error and covers with a generic fallback, or skips the action entirely and hands off. None of these show up as a red banner in the dashboard. **Always verify in a real conversation after changing an action's endpoint, description, or segment restriction.** *** ## Related Documentation Re-check fields, magic values, and the Test button. Re-check timeout, headers, and response handling. Global variables, widget headers, context headers. What happens when repeated action failures escalate. # AI Instructions Source: https://docs.open.cx/ai-instructions/overview Write the rules your AI follows and the answers it gives — a system prompt plus versioned training scenarios, all edited in one dashboard. **AI Instructions** is the dashboard surface where you teach the AI directly — no connectors, no source systems. You write two things here: the **AI Profile** (how the agent behaves) and **training scenarios** (how it answers specific questions). Everything drafts first, publishes when you're ready. Open [**AI Instructions**](https://platform.open.cx/ai-instructions) in your OpenCX dashboard. ## What lives here A single system prompt that defines who the agent is, the tone it uses, and the rules it never breaks. It's injected into every conversation before retrieval runs. Edit it as rich text; the editor keeps a **draft** alongside the published version so you can revise without affecting live conversations until you hit **Publish**. Discrete instructions the AI applies on top of the profile. Each scenario has a type: **Behavioral** (always active — tone, formatting, policy reminders) or **Scenario-specific** (triggered when the customer's question matches). Scenarios carry their own draft/published lifecycle and version history. See [Training scenarios](/ai-instructions/training-scenarios). Group scenarios by team, product area, or channel. Directories are flat org-wide — no nesting. Moving a scenario between directories is a drag; visibility settings (channels, segments) can be applied to a whole directory at once. Every publish of a training scenario writes an immutable version. Open the history panel on any scenario to see who published what and when, and roll back — a rollback populates the draft with the old content so you can review before republishing. The AI Profile is a single mutable document and does not keep version history. ## When to use AI Instructions vs. the other pillars Pick the surface closest to where your content already lives. Don't retype things you can sync. | Pick this when… | Use | | - | - | | The rule is **universal** (tone, never reveal pricing, always confirm order ID) | **AI Instructions** — AI Profile or a Behavioral scenario | | You have a **specific question** and a **specific answer** that's too important to rely on retrieval | **AI Instructions** — Scenario-specific | | The content is a **customer-facing article** you'd want on a public Help Center anyway | [**Help Center**](/help-center/overview) | | The content already lives in **Zendesk, Notion, Confluence, GitBook, Shopify, Intercom, Front, Freshdesk, or a website** | [**Knowledge Sources**](/knowledge/sources/overview) | ## Draft, publish, and what the AI sees Every document in AI Instructions — the profile and every scenario — has a **published** state and a **draft** state. * The draft is what you see while editing. It is **never** served to a customer. * Publishing copies the draft over the live version. For scenarios, a new immutable version is written so you can roll back later. * Discarding a draft wipes your unsaved changes and reverts to whatever is currently live. Because everything is org-wide, a publish takes effect on the next conversation across every channel. *** ## Related Documentation Behavioral vs. scenario-specific, channel and segment targeting, version history. Author customer-facing articles that also train the AI. Sync content from Zendesk, Notion, Confluence, and more. The three ways OpenCX learns what to say. # Training scenarios Source: https://docs.open.cx/ai-instructions/training-scenarios Write behavioral rules and question-specific answers the AI honors, with draft, publish, versioning, and channel or segment targeting. A **training scenario** is a single instruction you write for the AI. Every scenario is either **behavioral** (the AI applies it on every message) or **scenario-specific** (the AI applies it when the customer asks a matching question). Scenarios are the fastest way to teach the AI something without connecting a source or writing an article. Add a scenario from [**AI Instructions**](https://platform.open.cx/ai-instructions) — click the **+** next to a directory in the sidebar. ## Behavioral vs. scenario-specific Pick the type that matches the job. You can change a scenario's type later, but the draft/published state resets when you do. | | Behavioral | Scenario-specific | | - | - | - | | **When it applies** | Every conversation, on every message | When the customer's question matches the scenario's trigger question | | **How it's matched** | Always on — injected before retrieval | Semantic search against the trigger question; top matches are injected | | **Good fit for** | Tone, formatting rules, policy reminders, things that must hold on every reply | "When customer asks X, say Y" — refund policy, business hours, known product limitation | | **Example trigger** | *(none)* | "How do I cancel my subscription?" | | **Example content** | "Always confirm the order ID before discussing shipping. Never share another customer's details." | "Cancel from Account → Billing. Your access continues through the end of the current billing period." | Scenario-specific instructions match on **meaning**, not exact text. "Can I cancel?" and "How do I end my subscription?" hit the same scenario as long as the trigger question covers the intent. ## Write a scenario Go to [**AI Instructions**](https://platform.open.cx/ai-instructions). Pick the directory where the scenario should live — or click **New directory** first if you want to organize it separately. Click **+ New scenario** in the sidebar. Give it a clear title (this is only shown inside the dashboard — the AI doesn't see it). Set the type to **Behavioral** or **Scenario-specific**. For scenario-specific, fill the trigger question with the way a customer would actually phrase it. Write the answer or rule in the content editor — plain language is fine; the AI will rephrase it in its own voice unless you tell it not to. Under **Visibility**, pick channels (Chat, Email, SMS, WhatsApp, Phone, etc.) and contact segments the scenario should apply to. Leave both empty to apply everywhere. Phone agents also need the scenario, or its folder, selected in their [knowledge sources](/phone/agent-config#knowledge-sources). Click **Publish**. The scenario takes effect on the next message across every channel you selected. Your draft is preserved — if you keep editing, you'll see a pending draft badge until you publish again. ## Format instruction content Select text to open the formatting toolbar: text styles, bold and italics, links, **More formatting** for colors, highlights and alignment, and **Insert** for tables, checklists, quotes, code blocks and dividers. Type `/` to insert the same blocks without selecting text first. Inside a table, the handles above a column or beside a row add, remove and merge cells; drag a column boundary to resize it. Click a link to edit it; **⌘-click** (**Ctrl-click** on Windows and Linux) opens it in a new tab. The same tools are available when editing the AI profile. ## Common patterns Three canonical scenario-specific examples you can adapt. Each is written as the trigger question + the answer you want the AI to give. ``` Trigger: Can I get a refund? / Cancel my order / Money back Answer: We don't issue refunds directly, but I can help resolve the issue. Could you tell me what's not working so I can look into it? ``` ``` Trigger: What are your hours? / When are you open? / Business hours Answer: Support is available Monday–Friday, 9am–5pm EST. Outside those hours, leave a message and we'll reply on the next business day. ``` ``` Trigger: How much does it cost? / Pricing / Subscription fee Answer: Plans start at $49/month for Basic. Full pricing breakdown is at example.com/pricing — want me to walk through which plan fits your use case? ``` Keep answers in the voice you want the customer to hear — the AI will follow your phrasing closely on scenario-specific matches. ## Channel and segment targeting Leave the targeting fields empty unless you need the scope. When you set them: * **Channels** — comma-separated list. Valid values: Chat Widget, Email, SMS, WhatsApp, Phone, Slack, Instagram, Messenger, API. A scenario with channels set only fires on those channels. * **Segments** — OpenCX contact segments. A scenario restricted to segments only fires if the contact is a member of at least one listed segment. Anonymous conversations (no contact) never match segment-restricted scenarios. Setting **both** requires **both** to match. Use directory-level visibility to apply the same rule to a whole folder at once. ## Version history Every publish writes an immutable version you can review or roll back to. * **See versions** — open a scenario and click **History**. Each row shows the version number, who published it, and when. * **Roll back** — pick a version and click **Restore**. The content loads into your draft; nothing changes for customers until you publish. * **Drafts are not versioned.** Only published versions are kept. Save aggressively and publish when you're ready. ## Good to know Only the published version of a scenario is served at answer time. Drafts, including new unpublished scenarios, are invisible to the AI until you publish them. Drag a scenario in the sidebar to move it. Visibility settings on a directory (channels, segments) can be applied to every scenario inside with one action. A directory of 500 refund scenarios is harder to maintain than one Help Center article or a synced Zendesk category. Use scenarios for the things that would be lost in a long article — edge cases, policy carve-outs, overrides. When you mark an agent reply as "teach the AI" from a resolved session, OpenCX drafts a scenario-specific instruction pre-filled from the conversation. It lands as a draft — review and publish it the same way you would any other scenario. *** ## Related Documentation AI Profile, directories, and how direct training fits alongside the rest. Topic gating for when the AI should hand off instead of answer. Long-form content the AI cites in customer replies. Sync existing docs instead of rewriting them as scenarios. # Create an action Source: https://docs.open.cx/api-reference/actions/create post /actions Create actions your AI agent can call during sessions. Configure endpoints, dynamic inputs, and fixed parameter values through the API. Set `payload.parameters[].value` to a non-empty string to supply a fixed query or path parameter on every call. Omit `value` or set it to `null` when the parameter should be filled at call time. A `schema.example` only illustrates an input; it does not supply a fixed value. For example, an account identifier can stay fixed while the order reference varies: ```json theme={"dark"} { "parameters": [ { "name": "accountId", "in": "query", "required": true, "value": "account-123", "schema": { "type": "string" } }, { "name": "orderId", "in": "path", "required": true, "value": null, "schema": { "type": "string" } } ] } ``` Place these parameters inside `payload` alongside the action's path and method. See [Build an action](/actions/build-an-action) for dashboard setup. # Delete an action Source: https://docs.open.cx/api-reference/actions/delete delete /actions/{action_id} Permanently delete a single action by ID. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment sync. # Delete all actions Source: https://docs.open.cx/api-reference/actions/delete-all-actions delete /actions Delete all actions. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment sync. # Delete all actions Source: https://docs.open.cx/api-reference/actions/delete_all delete /actions/all Delete all actions. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment sync. # Get an action Source: https://docs.open.cx/api-reference/actions/get get /actions/{action_id} Get a single action by ID. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment sync. # Import actions from OpenAPI spec Source: https://docs.open.cx/api-reference/actions/import put /actions/import Import actions from OpenAPI spec — Upload an OpenAPI specification file (v2, v3, or v3. # Import from openapi file Source: https://docs.open.cx/api-reference/actions/import-from-openapi-file put /actions Import AI actions in bulk by uploading an OpenAPI spec. Use when syncing a large set of tools from an external API catalogue or a deploy pipeline. # AI Actions Source: https://docs.open.cx/api-reference/actions/index The AI Actions API lets you manage the HTTP API calls your AI agent can make during conversations. The AI Actions API lets you manage the HTTP API calls your AI agent can make during conversations. Actions connect your AI to external systems — looking up orders, processing refunds, booking appointments, and more. ## How Actions Work Each action defines an HTTP API call the AI can make: * **Endpoint** — The URL to call * **Method** — GET, POST, PUT, PATCH, or DELETE * **Payload** — OpenAPI-style specification defining parameters, request body, and response format * **Channels** — Which communication channels (web, email, WhatsApp, etc.) can trigger this action The AI decides when to call an action based on the conversation context and the action's name/description. ## Available Endpoints List all configured actions Get a single action by ID Create a new action Update an existing action Delete a single action Delete all actions Import actions from an OpenAPI spec file List all unique action tags # List all actions Source: https://docs.open.cx/api-reference/actions/list get /actions List all actions configured for your organization. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment… # List distinct action tags Source: https://docs.open.cx/api-reference/actions/list_tags get /actions/tags List distinct action tags — Get all unique tags used across actions in your organization. # Update an action Source: https://docs.open.cx/api-reference/actions/update patch /actions/{action_id} Update an existing action. Use when programmatically managing the AI actions (tools) your agents can call, typically as part of a deploy or environment sync. When you supply `payload`, it replaces the action's existing payload. Keep the full operation specification, including any fixed `parameters[].value` fields. To change only the action's name or description, omit `payload`. Fixed query and path parameters use a non-empty string in `value`. Omit that field or set it to `null` for a dynamic input. A `schema.example` does not replace a fixed value. See [Create an action](/api-reference/actions/create) for an example. # Audit Logs API Source: https://docs.open.cx/api-reference/audit-logs/index Read your workspace audit trail over the API: sign-ins, sign-outs and every change, with filters and stable cursor paging. Use it to export or monitor activity. The Audit Logs API returns your workspace's audit trail, newest first. It covers teammate sign-ins and sign-outs, and every change made in the dashboard, over the API, or by automations. See [Audit logs overview](/security/audit-logs) for what the log records. ## Key Concepts * **Scope required** — `audit-logs:read` * **Event** — `event_type` says what happened (`login`, `logout`, `create`, `update`, `api_key_create`, …). `entity_type` and `entity_id` say what it happened to * **Actor** — `actor_type` is `user`, `system` or `api`. For an API key, `api_key` names the key and who created it * **Changes** — `changes.before` and `changes.after` hold the fields that changed, when the event has them * **Read-only** — entries can't be edited or deleted ## Sign-ins and sign-outs A sign-in is an event with `entity_type: "user"` and `event_type: "login"`. `metadata.method` says how the teammate signed in: `google`, `microsoft`, `sso`, `partner`, `password` or `other`. For `sso`, `metadata.sso_provider_id` names the connection. A sign-out has `event_type: "logout"`. `metadata.reason` says why it ended: | `reason` | Meaning | | - | - | | `signed_out` | The teammate signed out. `actor_type` is `user` | | `expired` | The sign-in expired. Recorded the next time that browser loaded the dashboard | | `revoked` | The sign-in was ended before it expired, including by the teammate's removal from another workspace | | `removed_from_org` | An admin of this workspace removed the teammate. `metadata.revoked_sessions` counts the sign-ins ended | | `deprovisioned` | This workspace's identity provider deprovisioned the teammate | | `deactivated` | The identity provider deactivated the teammate | `metadata.user_email` always names the teammate, including when the system ended the sign-in. `ip_address` and `user_agent` are the request that signed in or out, when there was one. ## Filtering Every filter is optional, and filters combine. `eventType`, `entityType` and `eventCategory` accept one value, or the same parameter repeated to match any of several: ```bash theme={"dark"} curl "https://api.open.cx/audit-logs?eventType=login&eventType=logout&startDate=2026-09-01T00:00:00Z" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Use `entityId` to follow one thing, for example a workflow with `entityType=workflow&entityId=`. Use `userId` to follow one teammate: everything they did, plus the sign-outs the system ended for them. ## Paging Responses carry `pagination` (`total`, `page`, `limit`, `totalPages`) and `next_cursor`. * **Cursor paging (recommended)** — pass `next_cursor` back as `cursor` until it is `null`. Events written while you page never shift the pages, so nothing is skipped or read twice. When `cursor` is set, `page` is ignored * **Page numbers** — `page` and `limit` keep working. Pages can shift if new events arrive between requests `limit` is 20 by default and at most 100. `page` must be at least 1, and `cursor` must come from this workspace. Anything else is a `400`. # List audit logs Source: https://docs.open.cx/api-reference/audit-logs/list get /audit-logs List audit log events newest first, filtered by event, entity, actor or date, and paged by cursor or page number. Requires the audit-logs:read scope. # Authentication Source: https://docs.open.cx/api-reference/authentication Authenticate every API request with a Bearer token. Generate keys per scope, rotate them freely, and handle the standard 401/403 error responses. All API requests require a **Bearer token** in the `Authorization` header. ```bash theme={"dark"} curl https://api.open.cx/contacts \ -H "Authorization: Bearer YOUR_API_KEY" ``` *** ## Generate an API key 1. Go to [Settings → Access](https://platform.open.cx/settings/access) in the OpenCX dashboard. 2. Click **Create API Key**. 3. Give it a name (e.g. "Production", "Staging"). 4. Choose access level: * **Full access** — the key can call every endpoint with no restrictions. * **Custom scopes** — select only the resources and actions (read / write) this key needs. 5. Copy the key. Existing keys created before scopes were introduced remain full-access. The full key is only shown once. Store it securely — if you lose it, generate a new one. *** ## Using the key Pass the key as a Bearer token in the `Authorization` header on every request: ```bash theme={"dark"} Authorization: Bearer ocx_live_abc123... ``` ```bash cURL theme={"dark"} curl -X GET https://api.open.cx/contacts \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript Node.js theme={"dark"} const response = await fetch("https://api.open.cx/contacts", { headers: { Authorization: "Bearer YOUR_API_KEY", }, }); ``` ```python Python theme={"dark"} import requests response = requests.get( 'https://api.open.cx/contacts', headers={'Authorization': 'Bearer YOUR_API_KEY'}, ) ``` *** ## Scopes Scopes restrict an API key to specific resources and actions. A **full-access** key bypasses all scope checks, while a **scoped** key can only call the endpoints it was granted. Each scope follows the pattern `resource:action`, where action is `read` (GET / list) or `write` (create / update / delete). ### Available scopes | Resource | Read | Write | | - | - | - | | Actions | `actions:read` | `actions:write` | | Agent availability | `agent-availability:read` | `agent-availability:write` | | Audit logs | `audit-logs:read` | — | | Autopilot | `autopilot:read` | `autopilot:write` | | Blocklist | `blocklist:read` | `blocklist:write` | | Chat sessions | `chat-sessions:read` | `chat-sessions:write` | | Contact segments | `segments:read` | `segments:write` | | Contacts | `contacts:read` | `contacts:write` | | Crawl | `crawl:read` | `crawl:write` | | CSAT | `csat:read` | `csat:write` | | Email | `email:read` | `email:write` | | Handoff analytics | `handoff-analytics:read` | — | | Help center | `help-center:read` | `help-center:write` | | Impact report | `impact-report:read` | — | | Inbox views | `inbox-views:read` | `inbox-views:write` | | Insights | `insights:read` | `insights:write` | | Media | — | `media:write` | | Office hours | `office-hours:read` | `office-hours:write` | | Organization | `org:read` | `org:write` | | Phone | `phone:read` | `phone:write` | | Redaction | `redaction:read` | `redaction:write` | | Salesforce CRM | `salesforce-crm:read` | — | | Salesforce MIAW | — | `salesforce-miaw:write` | | Secrets | — | `secrets:write` | | Sequences | `sequences:read` | `sequences:write` | | SIP | `sip:read` | `sip:write` | | Slack | `slack:read` | `slack:write` | | SLA analytics | `sla-analytics:read` | — | | SLA policies | `sla:read` | `sla:write` | | Tags | `tags:read` | `tags:write` | | Teams | `teams:read` | `teams:write` | | Training | `training:read` | `training:write` | | Users | `users:read` | `users:write` | | WhatsApp | `whatsapp:read` | `whatsapp:write` | | Widget | `widget:read` | `widget:write` | | Workflows | `workflows:read` | `workflows:write` | Read-only resources (Audit logs, Handoff analytics, Impact report, Salesforce CRM, SLA analytics) have no write scope. Media and Secrets are write-only — there is no `media:read` or `secrets:read`. Secret values are never returned by any endpoint, so no scope can read one back. Salesforce MIAW only exposes a write scope for sending webhook events. Agent availability covers reading and setting a teammate's availability, availability status, and chat capacity, without inviting or managing members. `users:read` and `users:write` also grant it. Workflows also expose `workflows:trigger`, granted independently of read / write. It only authorizes calls to a workflow webhook trigger URL whose trigger configuration has **Access Control** set to *Private (Authentication Required)*. Public webhook triggers do not require any scope. *** ## Error responses | Status | Meaning | | - | - | | `401 Unauthorized` | Missing or invalid API key | | `403 Forbidden` | Key is valid but lacks the required scope | **401 — invalid or missing key** ```json theme={"dark"} { "statusCode": 401, "message": "Unauthorized", "error": "Unauthorized" } ``` **403 — missing scope** ```json theme={"dark"} { "statusCode": 403, "message": "This API key does not have the required scope: \"tags:read\"." } ``` *** ## Best practices * **Never commit keys to source control.** Use environment variables or a secrets manager. * **Rotate keys periodically.** You can create multiple keys and revoke old ones from the dashboard. * **Use separate keys for each environment** (production, staging, development) so revoking one doesn't break the others. * **Use scoped keys in production.** Grant only the permissions each integration needs (principle of least privilege). * **Prefer separate keys per integration** so revoking one doesn't break others, and each gets only the scopes it needs. # Get autopilot status Source: https://docs.open.cx/api-reference/autopilot/get get /autopilot Get the autopilot enabled/disabled status for each channel. Use when toggling or configuring autopilot mode across agents. # Enable or disable autopilot for a channel Source: https://docs.open.cx/api-reference/autopilot/update put /autopilot Enable or disable autopilot for a channel — Turn autopilot on or off for a specific channel. # Block a contact Source: https://docs.open.cx/api-reference/blocklist/block_contact post /blocklist/contacts Block a contact by email. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Block email domains Source: https://docs.open.cx/api-reference/blocklist/block_domains post /blocklist/domains Block one or more email domains. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Block IP addresses Source: https://docs.open.cx/api-reference/blocklist/block_ips post /blocklist/ips Block IP addresses or CIDR ranges, IPv4 or IPv6, optionally with expires_in_hours. Applies to web widget traffic — email and WhatsApp carry no visitor IP. # Check if a contact is blocked Source: https://docs.open.cx/api-reference/blocklist/check_contact get /blocklist/contacts/check/{contact_id} Check whether a specific contact is currently blocked. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Check if a domain or email is blocked Source: https://docs.open.cx/api-reference/blocklist/check_domain get /blocklist/domains/check Check if a domain or email is blocked. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Check if an IP is blocked Source: https://docs.open.cx/api-reference/blocklist/check_ip get /blocklist/ips/check Check whether an IP address is blocked. Matches exact entries as well as any CIDR range that contains the address. # Blocklist Source: https://docs.open.cx/api-reference/blocklist/index Manage blocked contacts, email domains, and IP addresses. Use when stopping abusive or unwanted visitors from reaching your AI assistant. The Blocklist API lets you manage blocked contacts, blocked email domains, and blocked IP addresses. Blocked visitors are ignored by the AI — instead of an automated response they receive a short message explaining they cannot contact the company right now. You can customise that message per organization in **Settings → Autopilot**. ## Blocked Contacts Block individual contacts by email. Useful for stopping the AI from responding to abusive users, spam accounts, or internal test contacts. ## Blocked Domains Block entire email domains (e.g. "spam.com"). All contacts with emails on blocked domains will be ignored by the AI. Supports blocking multiple domains at once. ## Blocked IPs Block a single address (e.g. "203.0.113.7") or a whole CIDR range (e.g. "203.0.113.0/24"), IPv4 or IPv6. Useful against widget abuse where the visitor keeps creating throwaway contacts, so contact- and domain-blocking never bite. The address is resolved from Cloudflare's `CF-Connecting-IP` header, which cannot be forged by the client. A client IP is only observable on the web/widget channels; once a session has been seen from a blocked address the block continues to apply to that session on every channel. ## Available Endpoints List all blocked contacts Block a contact by email Check if a contact is blocked Remove a contact from the blocklist List all blocked domains Block one or more email domains Check if a domain or email is blocked Remove a domain from the blocklist List all blocked IPs and CIDR ranges Block one or more IP addresses or ranges Check if an IP is blocked Remove an IP from the blocklist # List blocked contacts Source: https://docs.open.cx/api-reference/blocklist/list_contacts get /blocklist/contacts List all blocked contacts. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # List blocked domains Source: https://docs.open.cx/api-reference/blocklist/list_domains get /blocklist/domains List all blocked email domains. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # List blocked IPs Source: https://docs.open.cx/api-reference/blocklist/list_ips get /blocklist/ips List all blocked IP addresses and CIDR ranges. Use when auditing which visitors are barred from the AI assistant, or reconciling against an external abuse list. # List IP addresses seen in recent sessions Source: https://docs.open.cx/api-reference/blocklist/session_ip_activity get /blocklist/ips/session-activity List the IP addresses opening sessions, ranked by volume, with distinct contacts per address. Use when deciding which addresses to block instead of guessing. # Unblock a contact Source: https://docs.open.cx/api-reference/blocklist/unblock_contact delete /blocklist/contacts/{id} Unblock a contact — Remove a contact from the blocklist. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Unblock a domain Source: https://docs.open.cx/api-reference/blocklist/unblock_domain delete /blocklist/domains/{id} Unblock a domain — Remove a domain from the blocklist. Use when stopping a visitor from reaching the AI assistant, such as abuse, spam or internal test accounts. # Unblock an IP Source: https://docs.open.cx/api-reference/blocklist/unblock_ip delete /blocklist/ips/{id} Remove an IP address or CIDR range from the blocklist, restoring the visitor’s access to the AI assistant. # List WAV recordings for call flows Source: https://docs.open.cx/api-reference/call-flows/list-assets get /call-flows/assets List uploaded WAV recordings in your organization, including the IDs used by native IVR workflow nodes. Requires `workflows:read`, with telephony and Classical IVR enabled. Returns the organization’s recordings, newest first, including their IDs, names, sample rates, channel counts, durations in seconds and creation times. This endpoint returns the full list without pagination parameters. Use a recording ID in a [Classical IVR workflow](/phone/call-flows). # Assign an active call flow to a phone number Source: https://docs.open.cx/api-reference/call-flows/set-binding put /call-flows/binding Assign an active IVR Incoming Call workflow to an organization phone number, or remove its call-flow assignment. A phone number can have only one inbound destination: an IVR or a phone agent. Assigning an IVR to a number already assigned to an agent returns a validation error; unassign the number from the agent first. Assigning a phone agent to an IVR number is also rejected until the IVR assignment is removed. Use `workflowId: null` to remove the IVR assignment. Assigning a different active IVR replaces the previous IVR for that number. Changes apply to new calls. Requires both `phone:write` and `workflows:write` scopes. Telephony and Classical IVR must be enabled. The workflow must be active, use the `ivr-incoming-call` trigger and belong to the same organization as the number. See [Classical IVR setup](/phone/call-flows) for activation and real-call testing. # Upload an immutable WAV recording Source: https://docs.open.cx/api-reference/call-flows/upload-asset post /call-flows/assets Upload an immutable WAV recording for a native call flow using base64 audio and workflow write permission. Requires `workflows:write`, with telephony and Classical IVR enabled. Send the WAV bytes as base64 in `wavBase64` and a recording name in `name`. Supports mono or stereo, 16-bit PCM, A-law or mu-law WAV files at 8, 16, 24, 32, 44.1 or 48 kHz. The decoded file must be at most 20 MB and five minutes long. Invalid or unsupported audio is rejected. The response contains the recording ID, name, sample rate, channels, duration in seconds and creation time. Use the ID in a Classical IVR node. Every upload creates a new recording; it does not replace an existing recording, activate a workflow or change number routing. See [WAV recordings](/phone/call-flows#wav-recordings). # Add a tag to a chat session Source: https://docs.open.cx/api-reference/chat-sessions/add_tag post /chat/sessions/{session_id}/tags Add a tag to a session. The tag is created if it does not exist yet, and synced to the underlying ticketing system when one is connected. # Assign or unassign a chat session Source: https://docs.open.cx/api-reference/chat-sessions/assignee post /chat/sessions/{session_id}/assignee Assign a session to a teammate, named either by user ID or by email. Send either field as null to unassign. Use `GET /users` to look up user IDs. # Create a chat session Source: https://docs.open.cx/api-reference/chat-sessions/create post /chat/sessions Create a chat session — Creates a new chat session for the organization. Use when orchestrating conversations from code — creating sessions, sending messages, or listing history… ## Custom data Pass top-level `custom_data` to store session-level attributes on the new chat session, such as an order ID, cart value, or routing flag. Values can be strings, numbers, or booleans, and are returned by [Get Session](/api-reference/chat-sessions/get). ```json theme={"dark"} { "channel": { "type": "api" }, "custom_data": { "order_id": "ord_123", "cart_value": 149.99, "priority": "high" } } ``` Nested objects are rejected with a `400`. To attach contact-level attributes instead, use `contact.custom_data` on [Send Message](/api-reference/chat-sessions/send). Session `custom_data` set through this endpoint is stored on the session but is **not** synced to a connected external ticketing system (e.g. Intercom or HubSpot). # Create a session comment Source: https://docs.open.cx/api-reference/chat-sessions/create_comment post /chat/sessions/{session_id}/comments Add an internal note (agent comment) to a session via the REST API. Notes are visible to agents in the inbox and never shown to the customer. ## Internal notes Session comments are internal notes: agents see them in the inbox, the customer never does. A comment can be anchored two ways: * **Attached to a message** — pass `thread_id`, the UUID of the message being commented on. Message UUIDs come from [Session History](/api-reference/chat-sessions/list_history), where each event's `id` is its message UUID. * **Session-level** — omit `thread_id`. The note stands on its own in the session timeline, not attached to any message. `agent_id` is the numeric ID of the team member authoring the note. ## Retrieving comments Comments are part of the session timeline, so there is no separate list endpoint. Use [Session History](/api-reference/chat-sessions/list_history) and filter for events where `event.type` is `human_agent_comment`. Each one carries the author (`human_agent_id`), the text (`content.text`), and the thread it belongs to (`comment_thread_id`). ## Deep linking You can link agents straight to a message or note in the dashboard inbox: * **To a message:** `https://platform.open.cx/inbox/?s=&h=` * **To a comment on a message:** `https://platform.open.cx/inbox/?s=&thread=&comment=` — `thread_id` is the message the comment is attached to, and `comment_uuid` is the `uuid` returned when the comment is created. * **To a session-level comment:** `https://platform.open.cx/inbox/?s=&comment=&h=` — the note is its own timeline row, so `h` targets the note itself. The inbox opens the session and scrolls to the target. # Delete a session comment Source: https://docs.open.cx/api-reference/chat-sessions/delete_comment delete /chat/sessions/{session_id}/comments/{comment_uuid} Delete a session comment — Removes an internal note from a chat session. Only the agent who authored the note may delete it. Deletes an internal note. The note stops appearing in the inbox and in [List history](/api-reference/chat-sessions/list_history), and the endpoint answers `204` with no body. The acting agent is passed as the `agent_id` **query parameter**, since a `DELETE` carries no body: ```bash theme={"dark"} curl -X DELETE "https://api.open.cx/chat/sessions/{session_id}/comments/{comment_uuid}?agent_id=42" \ -H "Authorization: Bearer $OPENCX_API_KEY" ``` Only the note's author may delete it — anyone else receives a `403`. A `comment_uuid` that belongs to a different session returns a `404`. # Filter chat sessions Source: https://docs.open.cx/api-reference/chat-sessions/filter post /chat/sessions/filter Search and filter chat sessions with advanced criteria. Use when orchestrating conversations from code — creating sessions, sending messages, or listing history for an external… Each returned session includes all of its assigned `tags`, even when filtering by a subset of tag names. Each tag contains `tag_name` and `created_at`. Tags are ordered by name; sessions without tags return an empty array. The `support_team` field contains the assigned support team's `id` and `name`. The ID remains stable when the team is renamed. This field is `null` when no support team can be resolved for the session. The `first_human_message_at` field contains the first recorded human reply time as an ISO 8601 timestamp, or `null` when no human reply has been recorded. Internal notes and automated messages do not set this timestamp. Later human replies do not change it. The `title` field is the session title: the email subject, a subject set by an agent, or an AI-generated title. It is `null` until one is set. Use `assignee` to identify who is currently assigned to a session: | Type | Example | Meaning | | - | - | - | | Human | `{"type":"human","id":77,"name":"Alex Smith"}` | The numeric ID matches `GET /users`. | | AI | `{"type":"ai"}` | AI is assigned; no ID or name is returned. | | None | `{"type":"none"}` | The session is unassigned. | A human assignee's `name` matches the organization display name in `GET /users`. An organization name override takes precedence; a blank or missing override falls back to the account name. The name is `null` if the user is no longer a member of the organization or cannot be found; the recorded ID remains available for historical attribution. Reading a session does not require the `users:read` scope. Reading the user directory, including emails, does. The response field `assignee_id` is deprecated. Human IDs remain strings there; AI and unassigned sessions return `null`. Clients that previously used the AI placeholder must switch to `assignee.type`. Assignment requests continue to use `assignee_id` with a numeric human user ID or `null` to unassign. # Get a chat session Source: https://docs.open.cx/api-reference/chat-sessions/get get /chat/sessions/{session_id} Get a chat session — Retrieves details and call recording of a specific chat session by its ID. Use when orchestrating conversations from code — creating sessions, sending messages, or listing… The response includes `tags`, with each tag's `tag_name` and `created_at` timestamp. Tags are ordered by name. Sessions without tags return an empty array. The `support_team` field contains the assigned support team's `id` and `name`. The ID remains stable when the team is renamed. This field is `null` when no support team can be resolved for the session. The `first_human_message_at` field contains the first recorded human reply time as an ISO 8601 timestamp, or `null` when no human reply has been recorded. Internal notes and automated messages do not set this timestamp. Later human replies do not change it. The `title` field is the session title: the email subject, a subject set by an agent, or an AI-generated title. It is `null` until one is set. The `recording_url` field contains a download link for the latest completed phone or web call recording, or `null` when unavailable. Links expire after seven days; request the session again to get a fresh link. This field is only included when getting a session by ID. Use `assignee` to identify who is currently assigned to a session: | Type | Example | Meaning | | - | - | - | | Human | `{"type":"human","id":77,"name":"Alex Smith"}` | The numeric ID matches `GET /users`. | | AI | `{"type":"ai"}` | AI is assigned; no ID or name is returned. | | None | `{"type":"none"}` | The session is unassigned. | A human assignee's `name` matches the organization display name in `GET /users`. An organization name override takes precedence; a blank or missing override falls back to the account name. The name is `null` if the user is no longer a member of the organization or cannot be found; the recorded ID remains available for historical attribution. Reading a session does not require the `users:read` scope. Reading the user directory, including emails, does. The response field `assignee_id` is deprecated. Human IDs remain strings there; AI and unassigned sessions return `null`. Clients that previously used the AI placeholder must switch to `assignee.type`. Assignment requests continue to use `assignee_id` with a numeric human user ID or `null` to unassign. # Hand the session off to a human agent Source: https://docs.open.cx/api-reference/chat-sessions/handoff post /chat/sessions/{session_id}/handoff Hand off a chat session — Hands the session off to a human agent, re-running the pipeline even if already handed off. Use to escalate a conversation from code. # Sessions Source: https://docs.open.cx/api-reference/chat-sessions/index A session in Open represents any interaction between a customer and your support team. A session in Open represents any interaction between a customer and your support team. This could be a conversation with an AI agent, a human agent, or both working together to help the customer. ## What is a Session? Think of a session as a complete conversation thread. It starts when a customer reaches out and ends when their inquiry is resolved. Each session can include: * Messages between the customer and AI * Messages between the customer and human agents * Files and attachments * System actions and responses * Session metadata (duration, channel, status) ## Communication Channels Sessions can happen through multiple channels: Real-time chat through your website or app Email conversations with threaded replies Text message conversations Voice calls with transcription WhatsApp Business conversations Slack conversations API integrations ## How Sessions Work 1. **Session Start** * Customer initiates contact through any channel * System creates a new session * AI agent begins the conversation 2. **During the Session** * AI handles initial responses * Human agents can join when needed * Files and information can be shared * System actions can be triggered 3. **Session End** * Customer's inquiry is resolved * Session is marked as complete * Summary and insights are generated ## Session Custom Data Use top-level `custom_data` on [Create Session](/api-reference/chat-sessions/create) or [Send Message](/api-reference/chat-sessions/send) for attributes that belong to the session, such as an order ID, cart value, or routing flag. Use `contact.custom_data` for attributes that belong to the contact, such as plan, region, or account tier. Both can be sent in one request: ```json theme={"dark"} { "sender": "contact", "custom_data": { "order_id": "ord_123", "priority": "high" }, "contact": { "custom_data": { "plan": "pro" } }, "message": { "type": "text", "text": "I need help with this order" } } ``` ## Available Endpoints Start a new chat session Retrieve session details View all sessions Modify session information Assign a session to a teammate, or unassign it Move a session to another team Tag a session, synced to your ticketing system Send a message in a session Add an internal note, visible to agents only Export activity across your organization with date and event-type filters View session message history # List chat sessions Source: https://docs.open.cx/api-reference/chat-sessions/list get /chat/sessions Returns a paginated list of chat sessions for the organization. Use when orchestrating conversations from code — creating sessions, sending messages, or listing history for an… Each session includes `tags`, with each tag's `tag_name` and `created_at` timestamp. Tags are ordered by name. Sessions without tags return an empty array. The `support_team` field contains the assigned support team's `id` and `name`. The ID remains stable when the team is renamed. This field is `null` when no support team can be resolved for the session. The `first_human_message_at` field contains the first recorded human reply time as an ISO 8601 timestamp, or `null` when no human reply has been recorded. Internal notes and automated messages do not set this timestamp. Later human replies do not change it. The `title` field is the session title: the email subject, a subject set by an agent, or an AI-generated title. It is `null` until one is set. Use `assignee` to identify who is currently assigned to a session: | Type | Example | Meaning | | - | - | - | | Human | `{"type":"human","id":77,"name":"Alex Smith"}` | The numeric ID matches `GET /users`. | | AI | `{"type":"ai"}` | AI is assigned; no ID or name is returned. | | None | `{"type":"none"}` | The session is unassigned. | A human assignee's `name` matches the organization display name in `GET /users`. An organization name override takes precedence; a blank or missing override falls back to the account name. The name is `null` if the user is no longer a member of the organization or cannot be found; the recorded ID remains available for historical attribution. Reading a session does not require the `users:read` scope. Reading the user directory, including emails, does. The response field `assignee_id` is deprecated. Human IDs remain strings there; AI and unassigned sessions return `null`. Clients that previously used the AI placeholder must switch to `assignee.type`. Assignment requests continue to use `assignee_id` with a numeric human user ID or `null` to unassign. # Export session events Source: https://docs.open.cx/api-reference/chat-sessions/list_events get /chat/sessions/events Pull recorded events across all sessions for reporting and periodic syncs, with optional date ranges, message type filters, and cursor pagination. Export recorded activity across all sessions in your organization, including messages, internal notes, and system events. Requires an API key with `chat-sessions:read`. Use `created_after` and `created_before` to select a date window. Both accept ISO 8601 timestamps with a timezone and are optional. The lower bound is inclusive; the upper bound is exclusive and must be later than the lower bound. Use `include_message_types` to select recorded types, such as `agent_reopened_session,agent_comment`, or `exclude_message_types` to omit types, such as `message,agent_message`. Both accept comma-separated values. `include_message_types` takes precedence when both are provided. With neither filter, all recorded types are returned. Results are ordered oldest first by `created_at`, then by event `id` when timestamps match. The default `limit` is 100 events; it accepts whole numbers from 1 to 250. Pass the response's `next` value as `cursor` to continue, keeping the same organization, date window, and type filters until `next` is `null`. Deduplicate overlapping exports by event `id`. Each item uses the same `id`, `created_at`, and typed `event` format as [session history](/api-reference/chat-sessions/list_history), with `session_id`, `contact_id`, `message_type`, `actor`, and `details` added for exports. The session and contact IDs can be `null` when unavailable. Filter on `message_type`, which is the recorded type. It can differ from the normalized `event.type`. For example, `agent_reopened_session` can return `{"type":"human_agent_reopened_session","human_agent_id":77}` in `event`. Use `actor` to identify who performed the action: | Type | Identifier | | - | - | | `human` | Human user ID in `actor.id`, as a string. Typed event payloads can use numeric human IDs. | | `contact` | Contact ID when recorded. | | `workflow` | Workflow ID and `workflow_run_id` when recorded. | | `ai` | AI actor; no human user ID. | | `api`, `system`, `integration` | Source of the recorded action. | | `unknown` | The historical actor could not be identified. | The assignment recipient is separate from the actor: `details.target` describes a `human`, `ai`, `team`, or `none` target with an optional string `id`. `details` also contains recorded message text, `sub_status_id`, `workflow_id`, `workflow_run_id`, `edited_at`, and `deleted_at`. Unavailable values are `null`. Events without a history mapping are retained as `event.type: "unmapped_event"`, with their recorded `message_type` and text `content` when available. State checkpoint events return `payload: null`. Date filters use the original event time, including microsecond precision. Historical imports can arrive later, and edits do not change the original creation time. Re-export earlier windows to reconcile late imports and edits. This endpoint exports recorded history; it does not capture every session or contact attribute change or reconstruct missing historical identities. # List chat session history Source: https://docs.open.cx/api-reference/chat-sessions/list_history get /chat/sessions/{session_id}/history Returns a paginated list of events (messages, handoffs, status changes, etc.) for a chat session. # Remove a tag from a chat session Source: https://docs.open.cx/api-reference/chat-sessions/remove_tag delete /chat/sessions/{session_id}/tags/{tag_name} Remove a tag from a session, and from the underlying ticketing system when one is connected. # Search contacts and messages Source: https://docs.open.cx/api-reference/chat-sessions/search get /chat/sessions/search Full-text search across contacts (by name, email, phone) and chat messages. Use when orchestrating conversations from code — creating sessions, sending messages, or listing… # Send a message Source: https://docs.open.cx/api-reference/chat-sessions/send post /chat/sessions/{session_id}/send Send a message — Sends a message in a chat session. Use when orchestrating conversations from code — creating sessions, sending messages, or listing history for an external… ## Custom data This endpoint accepts two independent `custom_data` objects, each merged into a different record: * **Top-level `custom_data`** — session-level attributes that belong to this conversation, such as an order ID, cart value, or routing flag. Merged into the chat session and returned by [Get Session](/api-reference/chat-sessions/get). Values can be strings, numbers, or booleans. * **`contact.custom_data`** — contact-level attributes that belong to the person, such as plan, region, or account tier. Merged into the contact. Values are strings. Both merge into any existing data, so you only need to send the keys you want to add or update. Nested objects are rejected with a `400`. ```json theme={"dark"} { "sender": "contact", "custom_data": { "order_id": "ord_123", "cart_value": 149.99, "priority": "high" }, "contact": { "custom_data": { "plan": "pro", "region": "emea" } }, "message": { "type": "text", "text": "I need help with this order" } } ``` Session `custom_data` set through this endpoint is stored on the session but is **not** synced to a connected external ticketing system (e.g. Intercom or HubSpot). ## Option taps An agent reply sent from a macro with options carries them on the [`conversation.agent_response`](/api-reference/webhooks/types) webhook as `options`, next to the reply's `message_id`, and on the message's `event.options` in [session history](/api-reference/chat-sessions/list_history). On API sessions your client renders them. When the customer taps one, send a contact message that names the tap in `message.option_reply`. The message is recorded with the option's label as its text, and an option that isn't on a message of this session is rejected with a `400`: ```json theme={"dark"} { "sender": "contact", "message": { "type": "text", "text": "Yes", "option_reply": { "message_id": "7c3f1a7e-2a51-4b8e-9d0f-6f1b2c3d4e5f", "option_id": "b1e2c3d4-0000-4a5b-8c9d-112233445566" } } } ``` Only the first tap on the session's newest message with options counts, and only while the session is open. A tap on a `workflow` option runs the macro's **Macro Option Clicked** workflows and the AI doesn't reply; a `text` option is handled like any contact message. A tap that doesn't count, and the same text sent without `option_reply`, is handled as a normal message. `option_reply` is rejected with a `400` when `sender` is `agent`. # Clear the custom status of a chat session Source: https://docs.open.cx/api-reference/chat-sessions/sub_status_clear delete /chat/sessions/{session_id}/sub-status Remove the custom status from a session while leaving its built-in status untouched. Clearing on a closed session neither reopens nor re-closes it. # Set the custom status of a chat session Source: https://docs.open.cx/api-reference/chat-sessions/sub_status_set put /chat/sessions/{session_id}/sub-status Apply a custom status to a session. The session moves to that status's built-in parent, so an open-parented status reopens a closed session and a closed-parented one closes it. Applying a custom status whose parent is `closed_resolved` closes the session the same way as [updating its status](/api-reference/chat-sessions/update#ai-outcome-on-close), including the AI outcome it records. # Transfer a chat session to a team Source: https://docs.open.cx/api-reference/chat-sessions/team post /chat/sessions/{session_id}/team Transfer a session to a team. Open routes to an available member of that team when one is free, so this both transfers and assigns. Use `GET /teams` to look up team IDs. # Update a chat session Source: https://docs.open.cx/api-reference/chat-sessions/update patch /chat/sessions/{session_id} Update a chat session — Updates a chat session. Use when orchestrating conversations from code — creating sessions, sending messages, or listing history for an external surface. Send `summary` to replace the session's summary, for example with notes from a system that handled part of the conversation. Send `null` to clear it. A summary-only update leaves the status unchanged. When the same request closes the session with `generate_summary_and_sentiment_on_resolve`, the `summary` you send is kept instead of the generated one. ## AI outcome on close Closing a session as `closed_resolved` while the AI agent owns it outside assist mode records the AI outcome `assumed_resolved`, provided the AI agent replied and no teammate replied or took the session over. The outcome counts toward the AI resolutions in your impact report and is passed to `ticket-resolved` workflows. Voice and internal sessions are left unchanged. A `closed_unresolved` close never records an AI outcome, reclosing or retrying a close keeps the outcome already on the session, and a session closed as unresolved is not marked as an AI resolution when it is later changed to resolved. Each close is written to the audit log with the API key that made it. # Update a session comment Source: https://docs.open.cx/api-reference/chat-sessions/update_comment patch /chat/sessions/{session_id}/comments/{comment_uuid} Update a session comment — Replaces the content of an internal note and stamps it as edited. Only the agent who authored the note may edit it. Replaces the content of an existing internal note and stamps it as edited. Only the note's **author** may edit it: `agent_id` must match the agent the note is attributed to, otherwise the request is rejected with a `403`. A note that has been deleted can no longer be edited. `agent_id` identifies who the edit is attributed to — it is not a credential. An API key is scoped to the organization, not to one agent, so treat this rule as a safeguard against editing the wrong agent's note rather than as a permission boundary between your own agents. ```json theme={"dark"} { "content": "Correction: the refund was issued on the 14th, not the 12th.", "agent_id": 42 } ``` The note is resolved against the session in the path, so a `comment_uuid` belonging to a different session returns a `404` rather than editing the wrong conversation. ## Attachments `attachments` is a full replacement of the note's file list, not a patch: | You send | Result | | - | - | | No `attachments` field | Existing files are kept as they are | | `"attachments": []` | All files are removed from the note | | `"attachments": [...]` | The list you send replaces the existing files | So editing only the wording is safe — omit the field and the files stay put. To drop a single file, send the list of the ones you want to keep. # Link many contacts to companies Source: https://docs.open.cx/api-reference/companies/bulk-link post /companies/links Link up to 100 contacts to companies in one call, naming each contact by id, email, or phone. A row that fails returns its own error and does not affect the others. # Retrieve a company Source: https://docs.open.cx/api-reference/companies/get get /companies/{company_id} Retrieve one company by its OpenCX id, with its name, your own external ID, and its attributes. Use when you already hold a company ID. # Introduction to companies Source: https://docs.open.cx/api-reference/companies/introduction Group contacts under the company they belong to, and attach the account data your agents need during a conversation. ## What is a company? A **company** is an account record that contacts belong to. If three people from Acme Retail open sessions, they can all point at one Acme Retail company instead of each carrying their own copy of the plan tier, the CSM's name, and the account id. A contact can belong to several companies, and a company can have any number of contacts. ## Identity is your key, not ours Every company is identified by `external_id` — whatever you already call it in your own system. A Zoho account id, a merchant id, a tenant slug. Writes resolve on that key, so the same `POST /companies` call creates the company the first time and updates it every time after. `name` is for display only. It is not unique, so two merchants can both be called "Acme" and renaming one is never a collision. ## What you can do * **Create or update a company** → [Upsert a company](/api-reference/companies/upsert) * **Find companies by owner, attribute, or name** → [Search companies](/api-reference/companies/search) * **Assign the teammate who owns it** → [Set the owner](/api-reference/companies/set-owner) * **Add a company to a contact** → [Link a contact](/api-reference/contacts/companies/link) * **Read a contact's companies** → [List a contact's companies](/api-reference/contacts/companies/list) * **Remove one company from a contact** → [Unlink a contact](/api-reference/contacts/companies/unlink) Linking is strict: the `external_id` must already exist. An unknown key returns a 400 rather than creating a company, so a typo in a sync script cannot quietly fill your account list with junk. ## Attributes `custom_data` holds flat key/value attributes — strings, numbers, and booleans. Sending an attribute merges it into what is already there, and sending it as `null` clears that one key. Keys you do not mention are left alone. ```json theme={"dark"} { "external_id": "acct_88213", "name": "Acme Retail", "custom_data": { "plan": "growth", "seats": 40, "csm": "sara@acme.test" } } ``` Company attributes are not visible to the AI agent. They are available to your workflows through the **Get Contact Companies** action, and to your agents in the inbox, but they are never added to the model's prompt. ## Who owns the account A company can name one teammate as its owner, the CSM or account manager who looks after it. The owner shows on the company's page and on the companies card in the inbox, so an agent picking up a session can see who to ask. Name the owner by OpenCX user id or by email, whichever you already hold: ```json theme={"dark"} { "owner": { "email": "sara@yourcompany.test" } } ``` Email matching ignores capitalization, so an address copied straight out of a CRM works. Send `"owner": null` to leave the company unowned, which is the state every company starts in. You can set the owner while creating the company, in the same [upsert](/api-reference/companies/upsert) call: ```json theme={"dark"} { "external_id": "acct_88213", "name": "Acme Retail", "owner": { "email": "sara@yourcompany.test" } } ``` Omitting `owner` leaves ownership alone, so a nightly attribute sync never reassigns an account by accident. The owner must be a member of your organization. An email that belongs to nobody on your team returns a 400 rather than quietly clearing the field. The error reads the same whether the address is unknown to OpenCX or belongs to another organization, so it tells you nothing about who exists outside your own team. When a teammate leaves your organization, every company they owned becomes unowned. ## Syncing from a CRM A CRM sync usually pushes many links at once. [Link many contacts](/api-reference/companies/bulk-link) takes up to 100 in a single call and isolates failures: a row naming a company that was never created returns its own error, and the other 99 links still land. Results come back in request order with a summary. Name each contact whichever way you already hold it — `contact_id`, `contact_email`, or `contact_phone`, exactly one per row — so you don't have to resolve every contact to an OpenCX id first. Each result carries the resolved `contact_id`, which is worth storing against your own record to skip the lookup next time. ```json theme={"dark"} { "links": [ { "contact_email": "sara@acme.test", "external_id": "acct_88213" }, { "contact_phone": "+15551234567", "external_id": "acct_88213" }, { "contact_id": "8f3c...", "external_id": "acct_44100" } ] } ``` Phone matching is forgiving about spelling: `+15551234567` and `15551234567` find the same contact. If a number matches more than one contact, that row reports an error instead of guessing — name that contact by `contact_id`. ## Scopes Reads need `companies:read`; writes need `companies:write`. These are separate from `contacts:*` on purpose. Company records usually carry data pulled from your CRM, so a key that manages contacts does not automatically get to read or rewrite the account graph. ## Companies in workflows Five workflow actions cover the same ground without any API calls: * **Create or Update Company** * **Link Contact to Company** * **Get Contact Companies** — returns `found`, `count`, and the list, so you can branch on a contact having no company * **Set Company Owner** — takes an email or an agent id, so a workflow can assign the owner from a CRM lookup * **Unlink Contact from Company** — removes one company and leaves the contact's others alone Get Contact Companies returns each company's owner as `agentOwnerId`, `agentOwnerName`, and `agentOwnerEmail`. The id is the same numeric agent id **Change Ticket Assignee** takes, so a workflow can route a session to whoever owns the account. Check `agentOwnerId` is set before assigning, since most companies have no owner. Linking a contact does not fire the **Contact Updated** trigger. Nothing on the contact record changes, and a workflow that links on Contact Updated would otherwise re-trigger itself. # List companies Source: https://docs.open.cx/api-reference/companies/list get /companies Page through every company in your organization, newest first. Use to browse the account list or reconcile it against your own CRM. # Search companies Source: https://docs.open.cx/api-reference/companies/search post /companies/search Find companies by the teammate who owns them, by attribute conditions such as a plan or a subscription date, or by name, sorted and paged. Every filter you send must hold, so each one narrows the result. Leave `filters` out to page through every company. ```json theme={"dark"} { "filters": { "owner_ids": [12], "custom_data": [ { "key": "plan", "op": "eq", "value": "enterprise" }, { "key": "subscriptionStart", "op": "after", "value": "2025-03-01" } ] }, "search": "acme", "ordering": { "field": "contacts_count", "direction": "desc" }, "page": 1, "limit": 50 } ``` ## Owner `owner_ids` matches companies owned by any of the listed teammates. `unowned: true` matches companies nobody owns. Send both to get "these teammates' companies, plus the unowned ones". ## Attribute conditions | `op` | Matches when the attribute… | `value` | | - | - | - | | `exists` / `not_exists` | is set / is not set | — | | `eq` / `neq` | equals / does not equal the value | text | | `contains` | contains the value, ignoring case | text | | `before` / `after` | is a date earlier / later than the day | `YYYY-MM-DD` | `eq` also matches numbers and booleans, so `"value": "40"` finds a `seats` attribute stored as `40`. `before` and `after` read the attribute as a date when it holds a whole ISO 8601 date or timestamp (`"2025-03-14"`, `"2025-03-14T09:30:00Z"`, `"2025-03-14T09:30:00+03:00"`) or a Unix timestamp. Dates are compared by UTC day: a timestamp with an offset counts on its UTC day, and one without an offset is read as UTC. The day you send matches neither side, so `after 2025-03-01` starts on March 2. A number above 100,000,000,000 is read as milliseconds, anything smaller as seconds. An attribute in any other format, such as `"14/03/2025"` or `"2025-03-14 approx"`, never matches a date condition. ## Scopes Needs `companies:read`. # Set a company's owning teammate Source: https://docs.open.cx/api-reference/companies/set-owner put /companies/{external_id}/owner Assign the teammate who owns a company account, naming them by OpenCX user id or by email, or clear the owner to leave the account unassigned. # Create or update a company Source: https://docs.open.cx/api-reference/companies/upsert post /companies Create a company, or update it if the external_id already exists. Attributes merge into the existing record, and you can set the owner in the same call. # Add a Connect participant Source: https://docs.open.cx/api-reference/connect/add-participant POST /connect/threads/{thread_id}/participants Add a participant to a thread — typically the courier at assignment time. Waiting lanes for their role activate automatically on join. Add a participant to an open thread. The standard use is adding the `courier` at assignment — the `customer_courier` lane flips from `waiting` to `active` and the Supervisor briefs the courier with any context that matters ("leave at the gate, code 4411"). A thread holds one active participant per role; adding a second courier returns `409` — [remove](/api-reference/connect/remove-participant) the first for reassignment. ## Path Thread ID (`th_…`) or your `reference`. ## Request `customer`, `merchant`, or `courier`. Your ID for this person. Name shown to other participants. BCP-47 code for this participant's reading language. ## Response The created participant with server ID (`prt_…`). Updated lane states — the joining role's lanes become `active`. ```bash cURL theme={"dark"} curl -X POST https://api.open.cx/connect/threads/order_84421/participants \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "role": "courier", "external_id": "drv_318", "display_name": "Aleksei", "language": "en" }' ``` ```json 201 theme={"dark"} { "participant": { "id": "prt_d55", "role": "courier", "external_id": "drv_318", "display_name": "Aleksei", "language": "en" }, "lanes": [ { "id": "customer_merchant", "status": "active" }, { "id": "customer_courier", "status": "active" } ] } ``` # Create a Connect thread Source: https://docs.open.cx/api-reference/connect/create-thread POST /connect/threads Create an order-scoped thread with its initial participants. Idempotent on reference, so call it from your order pipeline without duplicate-guarding. Create the conversation for an order. Call it when the order is confirmed — the `customer_merchant` lane opens immediately; the `customer_courier` lane waits for a courier. ## Request Your order ID. Unique per organization and the idempotency key: posting an existing `reference` returns the existing thread with `200`. Initial participants. Usually the customer and merchant; couriers are [added later](/api-reference/connect/add-participant). `customer`, `merchant`, or `courier`. Your ID for this person or store. Also accepts an OpenCX `contact_id` for customers already known to your workspace. Name shown to the other participants. Never expose contact details here. BCP-47 code (`ar`, `en`, `hi`…). The Supervisor renders every message in this language for this participant. Defaults to your org's primary language. Lanes to enable. Defaults to `["customer_merchant", "customer_courier"]`; include `"merchant_courier"` to open the third lane. Override the org-default [Supervisor](/connect/ai-supervisor) config for this thread. `observe`, `assist`, or `resolve` (org default applies when omitted). Per-lane overrides: `{ "customer_courier": { "mode": "assist", "nudge_after_seconds": 300 } }`. Override org defaults: `wrap_up_hours` (default `24`), `retention_days` (days after locking before the thread is archived; not set by default, so a locked thread stays locked until you set it). Archiving never deletes thread history. Arbitrary JSON stored on the thread and surfaced to the Supervisor as answering context — order total, vertical, promo flags. ## Response Thread ID (`th_…`). Your order ID. `open` on creation. Lifecycle: `open → wrap_up → locked → archived`. Starts at `confirmed`; you drive it via [Update thread](/api-reference/connect/update-thread). Participants with server-assigned `id` (`prt_…`) alongside your `external_id`. Lane objects: `id`, `status` (`active` | `waiting` | `closed`), `supervisor_mode`. ```bash cURL theme={"dark"} curl -X POST https://api.open.cx/connect/threads \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reference": "order_84421", "participants": [ { "role": "customer", "external_id": "cus_991", "display_name": "Fatima", "language": "ar" }, { "role": "merchant", "external_id": "mrc_204", "display_name": "Shawarma House", "language": "en" } ], "custom_data": { "vertical": "food", "total": 86.5, "currency": "QAR" } }' ``` ```json 201 theme={"dark"} { "id": "th_9f2c4e", "reference": "order_84421", "status": "open", "order_status": "confirmed", "participants": [ { "id": "prt_c81", "role": "customer", "external_id": "cus_991", "display_name": "Fatima", "language": "ar" }, { "id": "prt_m32", "role": "merchant", "external_id": "mrc_204", "display_name": "Shawarma House", "language": "en" } ], "lanes": [ { "id": "customer_merchant", "status": "active", "supervisor_mode": "resolve" }, { "id": "customer_courier", "status": "waiting", "supervisor_mode": "resolve" } ], "custom_data": { "vertical": "food", "total": 86.5, "currency": "QAR" }, "created_at": "2026-07-30T14:03:11Z" } ``` # Create a participant token Source: https://docs.open.cx/api-reference/connect/create-token POST /connect/threads/{thread_id}/tokens Mint a short-lived scoped token for one participant to power the drop-in thread component or a headless WebSocket — their lanes and language only. Mint a client token for one participant. Pass it to the `` component or use it to open the thread WebSocket for a custom UI. The token grants exactly that participant's view: their lanes, their language, read/send only while the thread accepts messages. Tokens expire after 24 hours by default — mint them server-side when your app opens the order screen, not at order creation. ## Path Thread ID (`th_…`) or your `reference`. ## Request Participant ID (`prt_…`) or their `external_id`. Token lifetime, max 7 days. ## Response The scoped participant token. ISO 8601 expiry. ```bash cURL theme={"dark"} curl -X POST https://api.open.cx/connect/threads/order_84421/tokens \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "participant_id": "cus_991" }' ``` ```json 201 theme={"dark"} { "token": "cntk_eyJhbGciOiJIUzI1NiJ9...", "expires_at": "2026-07-31T14:05:00Z" } ``` # Get a Connect thread Source: https://docs.open.cx/api-reference/connect/get-thread GET /connect/threads/{thread_id} Fetch one thread by ID or by your order reference — participants, lanes, lifecycle status, Supervisor config, and custom data in a single read. Fetch a single thread. Accepts either the thread ID (`th_…`) or your order reference — `GET /connect/threads/order_84421` resolves the same thread, so you never store our IDs if you don't want to. ## Path Thread ID (`th_…`) or your `reference` (order ID). ## Response The full thread object — same shape as [Create thread](/api-reference/connect/create-thread), plus live lane state and counters. Per-lane message totals, including `supervisor` messages — a quick deflection read without listing messages. Sessions this thread escalated into: `{ session_id, reason, created_at }`. ```bash cURL theme={"dark"} curl https://api.open.cx/connect/threads/order_84421 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json 200 theme={"dark"} { "id": "th_9f2c4e", "reference": "order_84421", "status": "wrap_up", "order_status": "delivered", "participants": [ { "id": "prt_c81", "role": "customer", "display_name": "Fatima", "language": "ar" }, { "id": "prt_m32", "role": "merchant", "display_name": "Shawarma House", "language": "en" }, { "id": "prt_d55", "role": "courier", "display_name": "Aleksei", "language": "en" } ], "lanes": [ { "id": "customer_merchant", "status": "active", "supervisor_mode": "resolve" }, { "id": "customer_courier", "status": "active", "supervisor_mode": "resolve" } ], "message_counts": { "customer_merchant": 6, "customer_courier": 4, "supervisor": 3 }, "escalations": [], "created_at": "2026-07-30T14:03:11Z" } ``` # Connect API overview Source: https://docs.open.cx/api-reference/connect/index REST reference for Connect: order-scoped threads with customer, merchant, and courier participants, lane-scoped messages, participant tokens, and lifecycle updates. The Connect API manages **threads** — order-scoped conversations between a `customer`, a `merchant`, and a `courier`, supervised by your AI agent. Concepts are covered in the [Connect overview](/connect/introduction); this section is the REST surface. All endpoints live under `https://api.open.cx/connect` and use [Bearer authentication](/api-reference/authentication). ## Resources | Resource | Endpoints | | - | - | | Threads | [Create](/api-reference/connect/create-thread) · [Get](/api-reference/connect/get-thread) · [List](/api-reference/connect/list-threads) · [Update](/api-reference/connect/update-thread) | | Participants | [Add](/api-reference/connect/add-participant) · [Remove](/api-reference/connect/remove-participant) · [Create token](/api-reference/connect/create-token) | | Messages | [Send](/api-reference/connect/send-message) · [List](/api-reference/connect/list-messages) | ## The two calls that matter A full integration is thread creation at order confirmation and participant addition at courier assignment — the lifecycle, translation, moderation, and AI resolution run on their own. Everything else here exists for headless UIs, ops tooling, and audit. ## Idempotency `POST /connect/threads` is idempotent on `reference` (your order ID): repeat calls return the existing thread with `200` instead of creating a duplicate. Design your order pipeline to fire-and-retry safely. # List Connect messages Source: https://docs.open.cx/api-reference/connect/list-messages GET /connect/threads/{thread_id}/messages Page through a thread’s messages and events — filter by lane, include Supervisor interventions, and export full conversation history for audits and disputes. List a thread's messages, oldest first, cursor-paginated. Includes participant messages, system chips, and Supervisor messages — the complete record of the order conversation. ## Path Thread ID (`th_…`) or your `reference`. ## Query Restrict to one lane. Interleave non-message events (joins, removals, status changes, safety masks) as typed entries — the full audit trail for disputes. Cursor from a previous response. Page size, max 200. ## Response Message objects (same shape as [Send message](/api-reference/connect/send-message)); event entries carry `type` instead of `sender` when `include_events` is set. Present when more results exist. ```bash cURL theme={"dark"} curl "https://api.open.cx/connect/threads/order_84421/messages?lane=customer_courier&include_events=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json 200 theme={"dark"} { "data": [ { "id": "msg_1q8x", "lane": "customer_courier", "sender": { "participant_id": "prt_c81", "role": "customer" }, "text": "البوابة رقم ٤ — الرمز 4411", "translations": { "en": "Gate 4 — the code is 4411" }, "created_at": "2026-07-30T14:31:05Z" }, { "id": "evt_9k2m", "type": "supervisor.safety_mask", "lane": "customer_courier", "detail": "Masked a phone number in an outbound courier message", "created_at": "2026-07-30T14:33:47Z" } ], "next_cursor": null } ``` # List Connect threads Source: https://docs.open.cx/api-reference/connect/list-threads GET /connect/threads Page through threads filtered by status, order status, participant, or date range. Use for ops dashboards, dispute lookups, and archive queries. List threads for your organization, newest first, cursor-paginated. ## Query Filter by lifecycle status: `open`, `wrap_up`, `locked`, `archived`. Filter by the order status you last reported (`confirmed`, `picked_up`, `delivered`…). Threads that include this participant — every conversation a given courier or merchant has been part of. Only threads that escalated into a support session. ISO 8601 lower bound on creation time. Cursor from a previous response. Page size, max 100. ## Response Thread objects (same shape as [Get thread](/api-reference/connect/get-thread)). Present when more results exist. ```bash cURL theme={"dark"} curl "https://api.open.cx/connect/threads?status=open&escalated=true&limit=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json 200 theme={"dark"} { "data": [ { "id": "th_9f2c4e", "reference": "order_84421", "status": "open", "order_status": "picked_up", "escalations": [{ "session_id": "ses_77a1", "reason": "sentiment", "created_at": "2026-07-30T14:41:02Z" }] } ], "next_cursor": "eyJvZmZzZXQiOjUwfQ" } ``` # Remove a Connect participant Source: https://docs.open.cx/api-reference/connect/remove-participant DELETE /connect/threads/{thread_id}/participants/{participant_id} Remove a participant from a thread — courier reassignment being the common case. Access is revoked instantly; lane history stays with the thread. Remove a participant. Their tokens are revoked immediately and their apps lose the thread; the lane's history remains for the other side and for audit. The lane returns to `waiting` until a replacement joins. ## Path Thread ID (`th_…`) or your `reference`. Participant ID (`prt_…`) or their `external_id`. ## Response Always `true` on success. Updated lane states. ```bash cURL theme={"dark"} curl -X DELETE https://api.open.cx/connect/threads/order_84421/participants/drv_318 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json 200 theme={"dark"} { "removed": true, "lanes": [ { "id": "customer_merchant", "status": "active" }, { "id": "customer_courier", "status": "waiting" } ] } ``` # Send a Connect message Source: https://docs.open.cx/api-reference/connect/send-message POST /connect/threads/{thread_id}/messages Send a message into a lane on behalf of a participant — the server-side path for headless UIs and system notices. Translation and safety run before delivery. Send a message into a lane on behalf of a participant. This is the headless path — apps using the drop-in component send through their [participant token](/api-reference/connect/create-token) instead. Every message passes through the Supervisor's pipeline before delivery: safety masking, then translation into each recipient's language. Sending into a `locked` thread returns `409`. ## Path Thread ID (`th_…`) or your `reference`. ## Request Sender — participant ID (`prt_…`) or their `external_id`. Use `"system"` for order-event notices ("Your order was picked up"), rendered as system chips, not chat bubbles. `customer_merchant`, `customer_courier`, or `merchant_courier`. The sender's role must belong to the lane. Message text. Required unless `attachments` is present. Files by URL: `[{ "url": "https://…", "content_type": "image/jpeg" }]`. ## Response The created message, including `translations` keyed by language and the post-masking `text`. ```bash cURL theme={"dark"} curl -X POST https://api.open.cx/connect/threads/order_84421/messages \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "participant_id": "mrc_204", "lane": "customer_merchant", "text": "Out of garlic sauce — swap for tahini or refund the item?" }' ``` ```json 201 theme={"dark"} { "message": { "id": "msg_4t1a", "lane": "customer_merchant", "sender": { "participant_id": "prt_m32", "role": "merchant" }, "text": "Out of garlic sauce — swap for tahini or refund the item?", "translations": { "ar": "نفدت صلصة الثوم — نستبدلها بالطحينة أم نسترد قيمة الصنف؟" }, "created_at": "2026-07-30T14:12:40Z" } } ``` # Update a Connect thread Source: https://docs.open.cx/api-reference/connect/update-thread PATCH /connect/threads/{thread_id} Report order status to drive the thread lifecycle, adjust Supervisor modes per lane, or force-lock a thread. The one write you call as the order progresses. Update a thread as the order progresses. Reporting `order_status` is the normal use — the [lifecycle](/connect/threads) reacts automatically. ## Path Thread ID (`th_…`) or your `reference`. ## Request `confirmed`, `preparing`, `ready`, `picked_up`, `delivered`, or `cancelled`. `delivered` starts the wrap-up window; `cancelled` locks immediately. Custom statuses pass through to the Supervisor and the UI status chip. Force a lifecycle transition: `locked` locks now (skips the wrap-up window). Reopening a locked thread is not supported — escalate to a [session](/api-reference/chat-sessions/create) instead. Same shape as on [create](/api-reference/connect/create-thread): thread- or lane-level `mode`, `nudge_after_seconds`, escalation rules. Enable an optional lane mid-thread, e.g. `["merchant_courier"]` when a pickup goes sideways. Merged into existing custom data (top-level keys replace). ## Response The updated thread object. ```bash cURL theme={"dark"} curl -X PATCH https://api.open.cx/connect/threads/order_84421 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "order_status": "delivered" }' ``` ```json 200 theme={"dark"} { "id": "th_9f2c4e", "reference": "order_84421", "status": "wrap_up", "order_status": "delivered", "wrap_up_ends_at": "2026-07-31T15:20:44Z" } ``` # Add a company to a contact Source: https://docs.open.cx/api-reference/contacts/companies/link post /contacts/{contact_id}/companies Add an existing company to a contact. Adds rather than replaces, so a contact can belong to several companies. # List a contact's companies Source: https://docs.open.cx/api-reference/contacts/companies/list get /contacts/{contact_id}/companies List every company a contact belongs to, newest link first. An empty list means the contact belongs to none. # Remove one company from a contact Source: https://docs.open.cx/api-reference/contacts/companies/unlink delete /contacts/{contact_id}/companies/{external_id} Remove one company from a contact. The contact's other companies stay, and the company record itself is not deleted. # Create a contact Source: https://docs.open.cx/api-reference/contacts/create post /contacts Create a new contact. Use when your application needs to create, look up, or maintain the contact records that the AI agents converse with. # Delete a contact Source: https://docs.open.cx/api-reference/contacts/delete delete /contacts/{contact_id} Delete a contact. Use when your application needs to create, look up, or maintain the contact records that the AI agents converse with. # Get a contact Source: https://docs.open.cx/api-reference/contacts/get get /contacts/{contact_id} Get a single contact by its ID. Use when your application already holds a contact ID and needs that contact's details and custom data. # Get contact structured field values Source: https://docs.open.cx/api-reference/contacts/get-structured-field-values get /contacts/{contact_id}/structured-field-values Get the Structured Field values recorded on a contact, plus the fields that currently apply to it. Use when your application reads profile facts, such as plan or tier, kept on the contact. # Introduction to contacts Source: https://docs.open.cx/api-reference/contacts/introduction The contact APIs let you create, get, list, update, and delete contacts and read their custom data. ## What is a contact? A contact is a user that can interact with the chatbot, phone AI or email AI, a single contact might have multiple conversations across different channels. ## How do I create a contact? You can create a contact in your organization in two ways: #### 1. Using the API We have a simple public API that allows you to manage contacts in your organization, you can use the API to [create a contact](/api-reference/contacts/create) or to [authenticate a contact](/api-reference/widget/authenticate-contact). #### 2. Using the widget Whenever you embed the copilot widget in your application, you can use the widget options to create a contact, read more about this option in the [widget documentation](/widget/introduction). ### Adding custom data to a contact You can add any custom data you want to associate with the contact. This data will be returned in the response and can be used for any purpose you want, just make sure it's a valid JSON string object. The custom data is just a simple JSON object, you can add any key-value pairs you want to it, for example: ```jsx theme={"dark"} const widgetOptions = { initialMessage: "Hello! How can I help you?", token: "your_copilot_token_goes_here", user: { data: { name: "your_user_name", email: "your_user_email", customData: { // any custom data you want to send to the copilot backend "key1": "value1", "balance": 100, "age": 25, } } }, ``` You can use the API or the widget to add custom data to a contact. ### Reading custom data [Get a contact](/api-reference/contacts/get), [list contacts](/api-reference/contacts/list) and contact search return two custom data objects: * `custom_data`: data from verified sources, such as the API or an authenticated widget user. * `non_verified_custom_data`: data from unverified sources, such as an anonymous widget visitor. It is kept separate so it is never mistaken for trusted data. These responses include only keys with text values. A key whose value is a number, boolean or nested object is left out of the response, while every other key is still returned. To read a value through the API, store it as text, for example `"balance": "100"`. # List contacts Source: https://docs.open.cx/api-reference/contacts/list get /contacts List contacts. Use when your application needs to create, look up, or maintain the contact records that the AI agents converse with. # Check contact segment membership Source: https://docs.open.cx/api-reference/contacts/segments/check get /contacts/segments/{segment_id}/contacts/{contact_id} Check whether a specific contact is a member of a specific segment. Returns a single boolean — useful for gating behavior on cohort membership. # Create a contact segment Source: https://docs.open.cx/api-reference/contacts/segments/create POST /contacts/segments Create a contact segment through the public API with segment write permission. Learn the request fields, responses, and membership behavior. Requires `segments:write`. Supply `name` and `definition`. Omit `description` to use `null`. Membership is calculated in the background. A `null` definition matches all contacts. See [contact segment guidance](/api-reference/contacts/segments/introduction) for filter examples and membership timing. # Delete a contact segment Source: https://docs.open.cx/api-reference/contacts/segments/delete DELETE /contacts/segments/{segment_id} Delete a contact segment through the public API with segment write permission. Learn the request fields, responses, and membership behavior. Requires `segments:write`. Deletes the definition and its stored memberships, retaining contacts. Returns 204 with no body, or 404 if the segment does not exist in your organization. References in other configurations are not rewritten; review them before deletion. See [contact segment guidance](/api-reference/contacts/segments/introduction) for filter examples and membership timing. # Get a contact segment Source: https://docs.open.cx/api-reference/contacts/segments/get get /contacts/segments/{segment_id} Get a single contact segment definition by ID, including the underlying filter rules. # Introduction to contact segments Source: https://docs.open.cx/api-reference/contacts/segments/introduction Create and manage contact segments through the API, then check membership to target the right contacts with rules and personalized support. ## What is a contact segment? A **contact segment** is a named group of contacts based on rules you define in the dashboard or through the API (for example, "VIP customers" or "Trial users"). OpenCX evaluates those rules in the background and stores membership, so API reads are fast. A contact can belong to multiple segments at once. ## Reading segments from the API Reading endpoints under `/contacts/segments` requires `segments:read`. They answer four common questions: * **What segments do I have?** → [List segments](/api-reference/contacts/segments/list) * **What does this segment look like?** → [Get a segment](/api-reference/contacts/segments/get) * **Which segments is this contact in?** → [List segments for a contact](/api-reference/contacts/segments/list-for-contact) * **Is this contact in this specific segment?** → [Check membership](/api-reference/contacts/segments/check) ## Managing segments through the API Writing requires `segments:write`, independently of `segments:read`: * [Create a segment](/api-reference/contacts/segments/create) with a name and filter definition. Description is optional and defaults to `null`. * [Update a segment](/api-reference/contacts/segments/update) by sending only changed fields. A supplied `definition` replaces the entire rule set; it is not merged. Send `description: null` to clear the description. * [Delete a segment](/api-reference/contacts/segments/delete) to remove its definition and stored memberships. Contacts are retained. Missing segment IDs return 404. Filter definitions combine `or` groups containing `and` conditions. For example, match contacts whose email domain is `example.com`: ```json theme={"dark"} { "name": "Example customers", "definition": { "or": [{ "and": [{ "type": "contacts_email_domain_name_in", "domain_name_in": ["example.com"] }] }] } } ``` A `null` definition matches all contacts. An empty `or` list is invalid; an empty `and` group matches all contacts. Creation and filter changes calculate membership in the background, so immediate membership reads may reflect the previous state. Renaming or changing a description does not trigger recalculation. Deleting a segment does not rewrite references in other segment definitions, actions, instructions, or workflows. Review those references before deletion. Companion supports creating, updating, and deleting segments for users with contact write permission. MCP clients expose the same operations as `create_contact_segment`, `update_contact_segment`, and `delete_contact_segment`. ## Viewing and editing in the dashboard Open **Contacts → Segments** to inspect saved segments, including those created through Companion, MCP, or the API. The list shows a condition summary; open it to see the complete definition with its groups. Name the segment, then choose **Add first condition**. Search for a contact detail, activity, or status. Add a description if useful. Select **Include all contacts** when everyone should belong. Use **Add condition** to narrow the audience: a contact must satisfy every condition in that section. **Add an alternative** creates another way to qualify, shown as **Or include contacts who…**. For example, one section can include Pro customers in France, and another can include VIP customers anywhere. A contact only needs to qualify for one section. For lists of names, email addresses, email domains, phone numbers, or contact IDs, use **Add value** for each additional entry. Each field is one value; line breaks inside a saved value stay part of that value. For new **Contact attributes** conditions, select an attribute name and an operator: **equals**, **not equals**, **contains**, **exists**, or **not exists**. Only comparison operators need a value. Use **Add attribute** to require another attribute to match. This is the single option for filtering saved contact fields, such as plan or country. Existing equality-only conditions remain editable with their saved grouping and matching behavior. **Session attributes** is separate: it matches attributes saved on a contact's sessions, using exact key/value matches. Other condition types retain their own matching behavior; the five contact-attribute operators do not apply to every field. Existing contact and session attribute alternatives keep their original grouping, including untouched empty alternatives when you edit another alternative. Dates retain saved bounds and precision until edited; a missing lower or upper bound stays open-ended. References retain their saved IDs even if their display names cannot be loaded. Renaming a segment leaves its conditions unchanged. Empty groups and empty lists imported through the API are preserved and described in the editor; they are not silently removed. If a newer API introduces an unsupported condition, the editor preserves the complete definition and allows only name and description changes until the dashboard supports it. Contact read permission allows inspection; contact write permission is required to create, edit, or delete. Saving recalculates membership in the background when the definition changes. ## Where segments are used Segments are used to control AI behavior at runtime in two places: ### Actions Every [action](/build-agents/actions) has a **Restricted to segments** list: * **Empty list**: action is available to all contacts (default). * **Non-empty list**: action is available only if the contact belongs to at least one listed segment. A contact outside those segments cannot use or trigger that action. Anonymous contacts (no `contactId` yet) only get unrestricted actions. ### AI knowledge / training instructions Every [AI instruction](/training/instructions) also has **Restricted to segments**, with the same behavior: * **Empty list**: instruction applies to everyone. * **Non-empty list**: instruction applies only to matching contacts. ### How the restriction is enforced When a message arrives, OpenCX checks the contact's segment memberships and keeps only actions and instructions that apply to that contact. Membership is precomputed in the background, so runtime checks stay fast. ## Use cases for segmentation Beyond gating actions and instructions, segments are useful anywhere behavior should vary by cohort: * **Workflow branching** — use the [Check Contact In Segment](/build-agents/workflows) action to read `isMember` and route a workflow down a different branch (e.g. send a different email template to *VIP* vs *Trial* contacts). * **Outbound sequencing** — target a [sequence](/api-reference/sequences/index) at exactly the contacts in a segment instead of hand-curating a list. * **Routing & SLAs** — close *VIP* sessions faster by attaching a stricter [SLA policy](/api-reference/sla/policies) when the contact belongs to the segment. * **Reporting & analytics** — slice CSAT, handoff rate, or volume by segment to see how each cohort actually behaves. * **Programmatic gating from your stack** — use [`check_contact_in_segment`](/api-reference/contacts/segments/check) from your backend or OpenCX MCP tools to gate features at the call site (for example, showing a different help-center widget per cohort). # List contact segments Source: https://docs.open.cx/api-reference/contacts/segments/list get /contacts/segments List contact segment definitions for the organization. Use when you need to discover what segments exist before checking membership or rendering a picker. # List segments for a contact Source: https://docs.open.cx/api-reference/contacts/segments/list-for-contact get /contacts/segments/contacts/{contact_id} List every segment a specific contact currently belongs to. A contact can be a member of multiple segments at once. # Update a contact segment Source: https://docs.open.cx/api-reference/contacts/segments/update PATCH /contacts/segments/{segment_id} Update a contact segment through the public API with segment write permission. Learn the request fields, responses, and membership behavior. Requires `segments:write`. Supply at least one of `name`, `description`, or `definition`. Omitted fields remain unchanged. A supplied definition replaces all filter rules; `null` matches all contacts. Filter changes recalculate membership in the background. See [contact segment guidance](/api-reference/contacts/segments/introduction) for filter examples and membership timing. # Set contact structured field values Source: https://docs.open.cx/api-reference/contacts/set-structured-field-values put /contacts/{contact_id}/structured-field-values Set or clear Structured Field values on a contact. Use when your application records profile facts such as plan or tier on the contact, validated against the same fields sessions use. # Update a contact Source: https://docs.open.cx/api-reference/contacts/update patch /contacts/{contact_id} Update a contact. Use when your application needs to create, look up, or maintain the contact records that the AI agents converse with. # Cancel active crawl Source: https://docs.open.cx/api-reference/crawl/cancel-crawl delete /crawl/{id}/crawl Cancel the currently active crawl for the given website datasource. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed… # Create a website datasource Source: https://docs.open.cx/api-reference/crawl/create-datasource post /crawl Create a new website datasource and optionally start an initial crawl. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating… # Delete a website datasource Source: https://docs.open.cx/api-reference/crawl/delete-datasource delete /crawl/{id} Delete a website datasource and all its crawl jobs and pages. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed pages. # Exclude pages from sync Source: https://docs.open.cx/api-reference/crawl/exclude-pages post /crawl/{id}/pages/exclude Exclude pages from sync — Bulk exclude pages from future syncs. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed… # Get crawl job status Source: https://docs.open.cx/api-reference/crawl/get-crawl-job get /crawl/{id}/crawls/{jobId} Retrieve the status and details of a specific crawl job. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed pages. # Get website datasource details Source: https://docs.open.cx/api-reference/crawl/get-datasource get /crawl/{id} Get website datasource details — Retrieve a website datasource with page statistics and active crawl job information. # Include excluded pages back into sync Source: https://docs.open.cx/api-reference/crawl/include-pages post /crawl/{id}/pages/include Bulk include previously excluded pages back into the sync cycle. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed… # Crawl Source: https://docs.open.cx/api-reference/crawl/index The Crawl API allows you to programmatically crawl and index websites into your knowledge base. The Crawl API allows you to programmatically crawl and index websites into your knowledge base. This enables your AI agents to access and reference content from your website, documentation, or any other web-based resources when responding to customer inquiries. ## Overview Website crawling enables: * **Automated Content Indexing** - Automatically extract and index content from websites * **Knowledge Base Integration** - Crawled content is added directly to your knowledge base * **Real-time Status Tracking** - Monitor crawl progress and completion status * **Flexible Configuration** - Control include/exclude paths, page limits, and crawl intervals * **Protected Sites** - Crawl sites behind HTTP Basic, bearer token, cookie, or custom-header authentication * **Page Management** - Exclude, include, delete, or resync individual pages ## How It Works 1. **Create a Datasource** - Provide a website URL and configuration options 2. **Crawl Starts Automatically** - By default, a crawl begins immediately after creation 3. **Monitor Progress** - Check crawl status and track page processing 4. **Manage Pages** - Review crawled pages, exclude irrelevant ones, or resync outdated content 5. **Scheduled Recrawls** - Datasources automatically recrawl on a configurable interval ## Site Authentication To crawl a site that isn't publicly accessible, set the optional `auth` field when creating or updating a datasource. The credentials are sent to your site as request headers on every crawl and are encrypted at rest — the API only ever returns `auth_configured` and `auth_type`, never the secrets themselves. * **`basic`** - HTTP Basic authentication (`username` + `password`) * **`bearer`** - `Authorization: Bearer ` * **`cookie`** - A raw `Cookie` header value (e.g. `session=…; token=…`) * **`headers`** - Arbitrary custom headers (e.g. `X-Api-Key`) On update, omit `auth` to leave it unchanged, pass `null` to clear it, or pass a new object to replace it. The `User-Agent` header is reserved — crawls always identify as `OpenCX-Crawler/1.0 (+https://open.cx)`. ## Crawl Job Statuses * **`pending`** - Crawl job created, waiting to start * **`scraping`** - Crawl is actively running and extracting content * **`completed`** - Crawl finished successfully, content has been indexed * **`failed`** - Crawl encountered an error and could not complete * **`cancelled`** - Crawl was manually cancelled before completion ## Page Sync Statuses * **`synced`** - Page content is indexed in the knowledge base * **`pending`** - Page is waiting to be synced * **`error`** - Page failed to sync * **`excluded`** - Page is excluded from syncing Crawling large websites can take significant time and resources. Use include/exclude paths to focus on relevant content and set appropriate page limits. ## Available Endpoints ### Datasource Management Create a new website datasource and start crawling List all website datasources for your organization Get datasource details with page stats Update datasource configuration Delete a website datasource ### Crawl Operations Start a new crawl for a datasource Cancel an active crawl View crawl history for a datasource Check the status of a specific crawl job ### Page Management List crawled pages with filtering options Exclude pages from future syncs Re-include previously excluded pages # List crawl history Source: https://docs.open.cx/api-reference/crawl/list-crawl-jobs get /crawl/{id}/crawls List crawl history — Retrieve a paginated list of crawl jobs for a website datasource. # List website datasources Source: https://docs.open.cx/api-reference/crawl/list-datasources get /crawl Retrieve a paginated list of all website datasources for your organization. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating… # List crawled pages Source: https://docs.open.cx/api-reference/crawl/list-pages get /crawl/{id}/pages List crawled pages — Retrieve a paginated list of pages for a website datasource. # Start a new crawl Source: https://docs.open.cx/api-reference/crawl/start-crawl post /crawl/{id}/crawl Start a new crawl for the given website datasource. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed pages. # Update website datasource settings Source: https://docs.open.cx/api-reference/crawl/update-datasource patch /crawl/{id} Update configuration for a website datasource. Use when driving the website crawler programmatically — queueing crawls, inspecting jobs, and curating indexed pages. # Check if CSAT is enabled for a channel Source: https://docs.open.cx/api-reference/csat/check_channel get /csat/settings/channel/{channel} Check whether CSAT collection is enabled for the specified channel. Use when integrating CSAT results into a downstream BI or analytics pipeline. # Check if CSAT has been sent for a session Source: https://docs.open.cx/api-reference/csat/check_sent get /csat/sessions/{session_id}/sent Check whether a CSAT request has been sent for the specified session. Use when integrating CSAT results into a downstream BI or analytics pipeline. # Get specific CSAT score Source: https://docs.open.cx/api-reference/csat/get get /csat/scores/{score_id} Retrieve details of a specific CSAT score. Use when integrating CSAT results into a downstream BI or analytics pipeline. # Get CSAT settings Source: https://docs.open.cx/api-reference/csat/get-settings get /csat/settings Retrieve your CSAT settings — per-channel toggles plus the custom feedback-page and request-email template overrides. # Get all CSAT scores Source: https://docs.open.cx/api-reference/csat/get_all get /csat/scores Get all CSAT scores — Retrieve all CSAT scores for the organization with pagination # Get session CSAT score Source: https://docs.open.cx/api-reference/csat/get_session get /csat/scores/session/{session_id} Retrieve CSAT score for a specific session. Use when integrating CSAT results into a downstream BI or analytics pipeline. # CSAT Source: https://docs.open.cx/api-reference/csat/index The CSAT (Customer Satisfaction) API allows you to programmatically manage customer satisfaction scores and settings within your organization. The CSAT (Customer Satisfaction) API allows you to programmatically manage customer satisfaction scores and settings within your organization. CSAT scores help you measure customer satisfaction and track the effectiveness of your customer support interactions. Looking for the setup guide? See [Collect Customer Satisfaction](/resources/csat) for how to enable CSAT, configure channels, and wire it into workflows. ## Overview CSAT functionality in our system enables: * **Customer Satisfaction Tracking** - Collect and analyze customer satisfaction scores for support interactions * **Channel-Specific Settings** - Configure CSAT collection for different communication channels (web, phone, SMS, etc.) * **Session Analytics** - Track CSAT scores per session and analyze customer satisfaction trends * **Automated Collection** - Automatically send CSAT requests after support interactions * **Performance Insights** - Monitor team performance and customer satisfaction metrics ## Available Endpoints Retrieve all CSAT scores with pagination Get detailed information about a specific CSAT score Retrieve CSAT score for a specific session Check if CSAT is enabled for a specific channel Check if CSAT has been sent for a session # Update CSAT settings Source: https://docs.open.cx/api-reference/csat/update-settings patch /csat/settings Update your CSAT settings — customize the feedback-page and request-email templates. Omitted keys are preserved; null clears a template back to the default design. # Create a custom status Source: https://docs.open.cx/api-reference/custom-statuses/create post /custom-statuses Define a new custom session status under one of the built-in statuses. Names are unique per organization. # Delete a custom status Source: https://docs.open.cx/api-reference/custom-statuses/delete delete /custom-statuses/{id} Remove a custom status. Sessions that carried it keep their built-in status and lose the custom one. # Get a custom status Source: https://docs.open.cx/api-reference/custom-statuses/get get /custom-statuses/{id} Fetch one custom session status by id, including its parent status and customer-facing display fields. # Custom Statuses Source: https://docs.open.cx/api-reference/custom-statuses/index The Custom Statuses API lets you define the custom session statuses your team works with, and read or set the custom status on any session. Every session has one of three built-in statuses: `open`, `closed_resolved`, or `closed_unresolved`. A custom status refines one of them with a name your team recognizes, such as "Waiting on customer" or "Escalated to engineering", plus an optional customer-facing label, color, and icon. This API manages those definitions and reads or sets the custom status on a session. ## Key Concepts * **Custom status** — A named refinement of a built-in status. Each one has a `parent_status`; applying it to a session moves the session to that built-in status * **`sub_status_id`** — Every session object carries the id of its custom status, or `null` when only the built-in status applies. Resolve ids with the list endpoint * **Applying and clearing** — Set a session's custom status with `PUT /chat/sessions/{session_id}/sub-status`; clear it with `DELETE` on the same path. Clearing keeps the built-in status exactly as it is * **Filtering** — `POST /chat/sessions/filter` accepts `sub_status_ids` to find sessions in any of the given custom statuses * **Team access** — `allowed_group_ids` lists the teams whose agents may set the status from the inbox. An empty list allows every team. Deleting a team removes it from every list, so a status whose only allowed team is deleted becomes available to every team. Admins, API keys, and workflows can always set it, and a status already on a session stays visible to everyone * **Customer-facing fields** — `display_name` and `display_description` are what customers may see; `name` and `description` are for your team only * **Timeline** — Every set and clear leaves an event in the session history, attributed to the API ## Available Endpoints List every custom status in your organization Fetch one custom status by id Define a new custom status under a built-in one Change any subset of a custom status Remove a custom status Apply a custom status to a session Remove the custom status from a session # List custom statuses Source: https://docs.open.cx/api-reference/custom-statuses/list get /custom-statuses List every custom session status in your organization. Use it to resolve a session's sub_status_id to a name, or to pick an id before applying one. # Update a custom status Source: https://docs.open.cx/api-reference/custom-statuses/update patch /custom-statuses/{id} Change any subset of a custom status. Omitted fields stay as they are; send null to clear a display field, color, or icon. # Assign insight to a group (team) Source: https://docs.open.cx/api-reference/customer-insights/assign post /insights/{id}/assign Assign a customer insight to a specific group or team. Use when routing insights to the right owner or integrating with an external triage workflow. # Get categories Source: https://docs.open.cx/api-reference/customer-insights/categories/get get /insights/categories Retrieve the categories OpenCX uses to group customer-feedback insights. Use when driving the customer insights categorisation workflow from external code. # Get one Source: https://docs.open.cx/api-reference/customer-insights/get get /insights/{id} Get a specific customer insight — Retrieve detailed information about a specific insight # Get customer insights Source: https://docs.open.cx/api-reference/customer-insights/get_all post /insights Retrieve every customer insight the AI has surfaced, paginated. Use when driving the customer insights categorisation workflow from external code. # Customer Insights Source: https://docs.open.cx/api-reference/customer-insights/index The Customer Insights API allows you to programmatically access and manage AI-generated insights from your customer interactions. The Customer Insights API allows you to programmatically access and manage AI-generated insights from your customer interactions. Our AI automatically analyzes every customer interaction across all channels (email, SMS, phone, WhatsApp, Slack, and web) to extract meaningful patterns and insights that help you understand your customers better. ## Overview Customer Insights are automatically generated from your customer interactions and consist of: * **Pattern Recognition** - AI identifies recurring themes and issues * **Sentiment Analysis** - Understanding customer satisfaction levels * **Topic Clustering** - Grouping similar feedback and concerns * **Cross-channel Intelligence** - Combining insights from all communication channels ## Available Endpoints Retrieve all customer insights with optional filtering Get detailed information about a specific insight Mark an insight as resolved Assign an insight to a team member # Mark an insight as resolved Source: https://docs.open.cx/api-reference/customer-insights/resolve post /insights/{id}/resolve Mark an insight as resolved — Resolves a customer insight and optionally adds resolution details # Delete a custom domain Source: https://docs.open.cx/api-reference/email/delete_domains delete /email/domains/{domainName} Delete a custom domain from your organization. Use when managing email-channel provisioning — domain verification, unsubscribe handling, outbound email injection. # Send emails Source: https://docs.open.cx/api-reference/email/email_api post /email Send emails to multiple contacts, possibly with a time delay. Use when managing email-channel provisioning — domain verification, unsubscribe handling, outbound email injection. # List all custom domains Source: https://docs.open.cx/api-reference/email/get_domains get /email/domains List all custom domains for your organization, including their verification status and DNS records. Each domain includes a nullable `region` field. New domains record `eu-west-1` (EU, Ireland) or `us-east-1` (US, North Virginia). Older domains may return `null` when their region has not been recorded; this does not change where they send email from. See [Sending Domains & DNS](/email/sending-domains) for region selection and verification. # Email Support Source: https://docs.open.cx/api-reference/email/new Open can handle support mail that you forward into the platform: conversations appear in the Open inbox, autopilot or agents can reply, and outbound mail is… Open can handle support mail that you **forward** into the platform: conversations appear in the Open inbox, autopilot or agents can reply, and outbound mail is sent from your **registered** support address (after domain verification). ## Documentation (forwarding) * **[Email introduction](/email/introduction)** — what the channel does, flow, integration surfaces, and handoff overview. * **[Connect email forwarding](/email/forwarding)** — verify domain, register your address, forward to `forwarding@opencopilot.so`, complete the connection test. * **[Email Flow](/email/forwarding-rules)** — routing, tokens, session matching, and outbound format. ## API reference (domains and sends) For **programmatic** domain verification and **transactional** sends to contacts (separate from the forwarding inbox), use the **AI Email** endpoints in this section—for example [email API](/api-reference/email/email_api) and [domains](/api-reference/email/get_domains). ## Handoff Same handoff model as other channels: see [Human handoff](/handoff/introduction). # Start verification process for a custom domain Source: https://docs.open.cx/api-reference/email/patch_domains_verify patch /email/domains/{domainName}/verify Start the verification process for a custom domain. Use when managing email-channel provisioning — domain verification, unsubscribe handling, outbound email injection. # Add or update a custom domain for email sending Source: https://docs.open.cx/api-reference/email/put_domains put /email/domains Add or update a custom domain for sending emails. Use when managing email-channel provisioning — domain verification, unsubscribe handling, outbound email injection. Use the optional `region` field when adding a sending domain: | Value | Sending region | | - | - | | `eu-west-1` | EU (Ireland), the default for new domains | | `us-east-1` | US (North Virginia) | ```json theme={"dark"} { "domainName": "support.example.com", "region": "eu-west-1" } ``` The region cannot be changed after adding the domain. Adding an existing domain returns its existing configuration; it does not change the region. See [Sending Domains & DNS](/email/sending-domains) for setup and verification. # Unsubscribe Contacts Source: https://docs.open.cx/api-reference/email/unsubscribe_api Your contacts must be able to unsubscribe/opt out of receiving marketing emails. Your contacts must be able to unsubscribe/opt out of receiving marketing emails. You can set the `email_is_transactional` field when sending an email to specify its type. If a contact has unsubscribed, and the `email_is_transactional` attribute is set to `false`, the contact will *not* receive the email. Open enables opting out contacts in two ways: * An API for generating links that your contacts can click to unsubscribe * An API for unsubscribing contacts from within your apps ## Unsubscribe Link You can call [this endpoint](./unsubscribe_url_api) to generate a link that you can put in the body of any email, or anywhere in your application. You can generate this link using the contact's email, or their contact ID. ## Unsubscribe via API call from your apps In case you want your contacts to unsubscribe in a way other than clicking a link and being redirected to Open, you can directly call [this endpoint](./unsubscribe_directly_api) to mark a contact as unsubscribed from all email. # Directly Unsubscribe a Contact Source: https://docs.open.cx/api-reference/email/unsubscribe_directly_api POST https://api-v2.opencopilot.so/backend/email/{org_id}/unsubscribe/{contact_id} Mark a contact as unsubscribed from outbound email. Use when enforcing an opt-out from an external unsubscribe workflow or suppression-list sync. ## Path Parameters Your copilot/organization ID. The contact's ID. ## Body This endpoint does not accept a body. # Get an Unsubscribe Link Source: https://docs.open.cx/api-reference/email/unsubscribe_url_api GET https://api-v2.opencopilot.so/backend/email/{org_id}/unsubscribe/url Create a link that your contact can click to unsubscribe. This link will redirect your contact to Open. ## Path Parameters Your copilot/organization ID. ## Query Parameters The contact's ID. The contact's ID. You must provide either the `contactId` or the `contactEmail` query parameter. Otherwise, there is no way for Open to identify the contact. # Add user Source: https://docs.open.cx/api-reference/groups/add_user post /groups/{groupId}/users Add user. Use when maintaining group membership and scoping from an external directory. # Create Source: https://docs.open.cx/api-reference/groups/create post /groups Create. Use when maintaining group membership and scoping from an external directory. # Delete Source: https://docs.open.cx/api-reference/groups/delete delete /groups/{groupId} Delete. Use when maintaining group membership and scoping from an external directory. # Get Source: https://docs.open.cx/api-reference/groups/get get /groups/{groupId} Get. Use when maintaining group membership and scoping from an external directory. # Get all Source: https://docs.open.cx/api-reference/groups/get_all get /groups Get all. Use when maintaining group membership and scoping from an external directory. # Groups Source: https://docs.open.cx/api-reference/groups/index The Groups API allows you to programmatically manage user groups within your organization. The Groups API allows you to programmatically manage user groups within your organization. Groups are used to organize users, manage permissions, and automate task assignments, making it easier to handle customer support and insights effectively. ## Overview Groups in our system enable: * **Permission Management** - Control access to features and data based on group membership * **Support Hours & Team Assignment** - Define support schedules and automatically route customer inquiries to the right team * **Automated Task Assignment** - Automatically assign customer insights and AI recommendations to specific teams * **Team Organization** - Structure your support and operations teams efficiently ## Available Endpoints Get detailed information about a specific group Create a new group Update an existing group Delete a group Get all groups in the organization Add a user to a group Remove a user from a group # Remove user Source: https://docs.open.cx/api-reference/groups/remove_user delete /groups/{groupId}/users/{userId} Remove user. Use when maintaining group membership and scoping from an external directory. # Update Source: https://docs.open.cx/api-reference/groups/update put /groups/{groupId} Update. Use when maintaining group membership and scoping from an external directory. # List handoff events Source: https://docs.open.cx/api-reference/handoff-analytics/events get /handoff-analytics/events List handoff events — a paginated list of individual AI-to-human handoff events, each with the AI-written summary, explanation, handoff reasons, sentiment, and session ID. # Get handoff analytics Source: https://docs.open.cx/api-reference/handoff-analytics/get get /handoff-analytics Get handoff analytics — Get a comprehensive overview of AI-to-human handoff events for a date range, including reason breakdowns, sentiment analysis, trends, and referenced… # Handoff Analytics Source: https://docs.open.cx/api-reference/handoff-analytics/index Handoff analytics provide visibility into why and when the AI agent hands conversations off to human agents. Handoff analytics provide visibility into why and when the AI agent hands conversations off to human agents. Use this data to improve your AI's performance by identifying gaps in knowledge, tools, or instructions. ## What are Handoffs? A handoff occurs when the AI agent determines it cannot fully resolve a customer's request and transfers the conversation to a human agent. Each handoff event captures: * **Reason** — Why the AI handed off (missing knowledge, missing tools, unclear instructions, user request, prohibited topic) * **Sentiment** — The customer's sentiment at the time of handoff * **References** — What knowledge articles, tools, or instructions were involved ## Available Endpoints Get a comprehensive handoff analytics overview List individual handoff events with per-session reasons and summaries # List the articles that link to a help center article Source: https://docs.open.cx/api-reference/help-center/articles/backlinks get /hc/{helpCenterId}/articles/{entityId}/backlinks List the other articles that link to a help center article: the languages each one is live in with the link, and unsaved drafts that still carry it. Read it before deleting or unpublishing. # Create a help center article Source: https://docs.open.cx/api-reference/help-center/articles/create post /hc/{helpCenterId}/articles Create a help center article as a draft in the primary language under a category. Publish it separately to make it live and available to the AI. # Delete a help center article Source: https://docs.open.cx/api-reference/help-center/articles/delete delete /hc/{helpCenterId}/articles/{entityId} Permanently delete a help center article, its versions, and its entry in the AI index. Use unpublish instead to take it offline without losing it. # Get a help center article Source: https://docs.open.cx/api-reference/help-center/articles/get get /hc/{helpCenterId}/articles/{entityId} Fetch one help center article with its source version and every locale version. Use to read the current draft and published content. # Get the per-language state of a help center article Source: https://docs.open.cx/api-reference/help-center/articles/language_states get /hc/{helpCenterId}/articles/{entityId}/language-states Per language: whether the article is published, whether it has unpublished changes, and whether its translation is behind the source. Read it before publishing to know what will change. # List help center articles Source: https://docs.open.cx/api-reference/help-center/articles/list get /hc/{helpCenterId}/articles List every article in a help center with its locale for the requested language. Use to find article ids before updating or publishing. # Publish a help center article (all locales) Source: https://docs.open.cx/api-reference/help-center/articles/publish post /hc/{helpCenterId}/articles/{entityId}/publish Publish a help center article in every locale that has a version. This puts it on the public site and in the AI index in the same call. # Publish one locale of a help center article Source: https://docs.open.cx/api-reference/help-center/articles/publish_locale post /hc/{helpCenterId}/articles/{entityId}/locales/{languageCode}/publish Publish a single locale of a help center article. Use to ship the primary language now and release translations when they are reviewed. # Restore a deleted help center article Source: https://docs.open.cx/api-reference/help-center/articles/restore post /hc/{helpCenterId}/articles/{entityId}/restore-deleted Bring a deleted article back with its content and place it back in the tree. Only works while the article is still soft-deleted. # Retranslate a help center article into one language Source: https://docs.open.cx/api-reference/help-center/articles/retranslate post /hc/{helpCenterId}/articles/{entityId}/retranslate Regenerate the translation of a help center article for one language from its source version. Use after editing the source or the glossary. # Retranslate a help center article into every enabled language Source: https://docs.open.cx/api-reference/help-center/articles/retranslate_all post /hc/{helpCenterId}/articles/{entityId}/retranslate-all Regenerate the translations of a help center article for every enabled language. Use after a source edit that should reach all locales. # Unpublish a help center article Source: https://docs.open.cx/api-reference/help-center/articles/unpublish post /hc/{helpCenterId}/articles/{entityId}/unpublish Take a help center article off the public site for one locale or all of them. Unpublishing the primary locale also removes it from the AI. # Update a help center article Source: https://docs.open.cx/api-reference/help-center/articles/update put /hc/{helpCenterId}/articles/{entityId} Create a new draft version of a help center article with new title or content. Publish it afterwards to replace the live version. The body is an HTML fragment in `content_html`; omit it to change only the title or search metadata and keep the existing body. Without `language_code` the update applies to the article's source language. Publishing is a separate call. # List versions of a help center article Source: https://docs.open.cx/api-reference/help-center/articles/versions get /hc/{helpCenterId}/articles/{entityId}/versions List the version history of a help center article for a language, with who saved and who published each version. Use to audit changes or pick a version id to publish as a rollback. # Create a help center topic Source: https://docs.open.cx/api-reference/help-center/categories/create post /hc/{helpCenterId}/categories Create a help center category in the primary language, optionally nested under a parent. Translations are generated in the background. # Delete a help center topic Source: https://docs.open.cx/api-reference/help-center/categories/delete delete /hc/{helpCenterId}/categories/{categoryEntityId} Delete a help center category. Its articles and subcategories are deleted with it unless move_contents_to names another category to re-home them under. # Get a help center topic Source: https://docs.open.cx/api-reference/help-center/categories/get get /hc/{helpCenterId}/categories/{categoryEntityId} Fetch one help center category with all of its locale versions. Use to read its name, description, icon, and parent before updating. # List help center topics Source: https://docs.open.cx/api-reference/help-center/categories/list get /hc/{helpCenterId}/categories List every category of a help center with its locale for the requested language. Top-level categories have a null parent id. # Restore a deleted help center topic Source: https://docs.open.cx/api-reference/help-center/categories/restore post /hc/{helpCenterId}/categories/{categoryEntityId}/restore-deleted Bring a deleted category back and place it back in the tree. Only works while the category is still soft-deleted. # Retranslate a help center topic into one language Source: https://docs.open.cx/api-reference/help-center/categories/retranslate post /hc/{helpCenterId}/categories/{categoryEntityId}/retranslate Regenerate the translation of a help center category for one language. Use after renaming it or changing its description in the source. # Retranslate a help center topic into every enabled language Source: https://docs.open.cx/api-reference/help-center/categories/retranslate_all post /hc/{helpCenterId}/categories/{categoryEntityId}/retranslate-all Regenerate the translations of a help center category for every enabled language after a change to its source name or description. # Update a help center topic Source: https://docs.open.cx/api-reference/help-center/categories/update put /hc/{helpCenterId}/categories/{categoryEntityId} Update the name, description, or icon of a help center category in one language. Writing the primary language retranslates the others; another language edits only that translation. # Get a help center Source: https://docs.open.cx/api-reference/help-center/get get /hc/{id} Read one help center: its name, description, domain, logo, languages, theme, custom CSS and head scripts, and per-language title and description overrides. # Add a glossary term Source: https://docs.open.cx/api-reference/help-center/glossary/create post /hc/{id}/glossary Add a term the translator must keep as defined. It applies to translations made from then on; existing translations are unchanged until retranslated. # Delete a glossary term Source: https://docs.open.cx/api-reference/help-center/glossary/delete delete /hc/{id}/glossary/{termId} Remove a glossary term. Later translations are free to render it however they like. # Import glossary terms from the organization glossary Source: https://docs.open.cx/api-reference/help-center/glossary/import post /hc/{id}/glossary/import-from-translation Copy the organization-wide translation glossary into this help center. Terms that already exist here are left alone. # List the translation glossary Source: https://docs.open.cx/api-reference/help-center/glossary/list get /hc/{id}/glossary List the translation glossary: terms the translator renders exactly as defined, such as product and feature names. # Update a glossary term Source: https://docs.open.cx/api-reference/help-center/glossary/update put /hc/{id}/glossary/{termId} Change a glossary term or its definition. It applies to translations made from then on; translations that already exist keep their wording until they are retranslated. # Help Center API Source: https://docs.open.cx/api-reference/help-center/index Create, update, publish, and delete help center articles and categories from your own systems, and manage the site itself — languages, glossary, branding, and ordering. The Help Center API lets you manage the customer-facing knowledge base you author in OpenCX from your own systems. Anything you can do in the [Help Center](/help-center/overview) editor you can do here: create and edit articles and categories, publish and unpublish them, manage translations and the glossary, order the tree, upload media, and change the site's own settings. Publishing an article's primary language ships it to your public help site **and** the AI's knowledge in the same action. There is no separate "train the AI" step. Other languages publish to the site only; the AI always follows the primary language. ## Scopes | Scope | Grants | | - | - | | `help-center-content:read` | List and fetch articles, categories, versions, the content tree, and media | | `help-center-content:write` | Create, update, publish, unpublish, move, reorder, restore, delete, retranslate, upload | | `help-center:read` | List and fetch help centers, their language settings, and their glossary | | `help-center:write` | Change a help center's settings, served languages, and glossary | `help-center:read` and `help-center:write` also grant the matching content scope, so an existing key keeps working unchanged. Create an API key under **Settings → API keys**. See [Authentication](/api-reference/authentication). ## Key concepts * **Help center id** — every endpoint is scoped to one help center. Call [List help centers](./list) first to find the id. * **Categories** — every article belongs to exactly one category. Categories can be nested; `parent_id` is `null` for top-level ones. * **Source language** — each article records the language it was written in. Every other language is a translation of it. Pass `language_code` on [Create](./articles/create) when the article is authored in something other than the help center's primary language; it cannot be changed later. * **Draft and published** — creating or updating an article writes a new **version**. Nothing is visible to customers or the AI until you call [Publish](./articles/publish), which ships the newest stored version. [Language states](./articles/language_states) tells you, per language, what is live and what is waiting. * **Locales** — each enabled language has its own published state. [Publish](./articles/publish) releases every locale at once; [Publish one locale](./articles/publish_locale) lets you ship the primary language before translations are reviewed. Only the primary language feeds the AI: publishing a translation while the primary language is still unpublished makes that translation public without adding the article to the AI's knowledge. * **Per-language structure** — where an article or category sits, and in what order, is recorded per language. A [move](./structure/move_article) changes one language's tree and leaves the others alone. * **Unpublish, delete, restore** — [Unpublish](./articles/unpublish) takes an article offline while keeping its content and history; unpublishing the primary language also removes it from the AI's knowledge. [Delete](./articles/delete) is reversible through [Restore](./articles/restore) while the article is still soft-deleted. Articles are either unpublished or published. There is no separate internal-only visibility today: an article published in its primary language is on the public site and in the AI's knowledge together. For content the AI should know but customers should not browse, use the [AI Training API](/api-reference/training/index) instead. ## Available Endpoints ### Help centers Find the help center id Settings, branding, and custom code Name, domain, CSS, head scripts ### Languages and glossary Served languages and auto-translate Change what is served What is still translating Terms the translator keeps Protect a product name Change a term or definition Remove a term Copy the organization glossary ### Articles All articles for a language One article with its versions New article under a category New stored version Go live in every locale Go live in one language What is live, what is waiting Take offline, keep content Remove from the tree Bring a deleted article back Version history One language or all ### Categories All categories for a language One category with locales Top-level or nested Rename or re-icon, per language With or without its contents Bring a deleted category back One language or all ### Structure One language, whole tree, in order Into a category, at a position Nest it or lift it to the top Order one category's children ### Media Images for article HTML Library contents and URLs Remove a file ## Best practices * Create, review, then publish. An unpublished article never reaches customers or the AI. * Article bodies are HTML fragments in `content_html`, on reads and writes. It replaces the former `content_markdown` field, which is no longer accepted. * Read the article before you update it and send the whole `content_html` back — an update replaces the body rather than patching it. * Read [Language states](./articles/language_states) before publishing so you know which locales the call will change. * `enabled_languages` and `localizations` **replace** what is stored rather than merging. Read the current value first and send it back whole. * Set a stable `slug` on create when you need permanent URLs; otherwise it is derived from the title. * Upload images through [Upload media](./media/upload) and reference the returned URL in `content_html`. * Embed a video with `
`. `data-embed` is `youtube`, `vimeo`, `loom` or `wistia`; `data-embed-id` is the video's id from its link — for an unlisted Vimeo video, `76979871/8272103f6e`. An embed with another host or a malformed id is removed on save, and `