# Confirm an agent create Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/confirm-assistant-create api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/confirm-create Second step of a safe create. Send ONLY the confirmationId returned by proposeAssistantCreate — the server creates the assistant from the proposal it stored; any other body field is ignored. Each proposal is confirmed ONCE — the confirmationId is consumed as soon as the confirm's checks pass, immediately before the assistant is created; to create another agent, run proposeAssistantCreate again and confirm the new confirmationId. If a confirm errors or times out after the proposal was spent, check listAssistants for the draft FIRST — the creation may have completed — before proposing again. PROPOSAL_NOT_FOUND (404) means the proposal expired or was already confirmed: re-run proposeAssistantCreate with the same body and confirm again with the new confirmationId. New assistants are created as DRAFTS; a human reviews and publishes them in the app. Second step of the propose → confirm flow. See [Propose an agent edit](/api-reference/claudia/endpoints/propose-assistant-edits) for the full walkthrough. Each confirmationId from [proposeAssistantCreate](/api-reference/claudia/endpoints/propose-assistant-create) is single-use: it is consumed as soon as the confirm's checks pass, right before the agent is created — even if the creation itself then fails — so a retry needs a fresh propose call. If the confirm errored or timed out, list the agents first: the creation may have completed, and re-proposing blindly would duplicate the draft. # Confirm an agent edit Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/confirm-assistant-edits api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/{id}/confirm-edits Second step of a safe edit. Send ONLY the confirmationId returned by proposeAssistantEdits, plus If-Match=baseUpdatedAt as a header — the server replays the exact proposal it stored and writes it; any other body field is ignored. PROPOSAL_NOT_FOUND (404) means the proposal expired: re-run proposeAssistantEdits with the same body and confirm again with the confirmationId it returns. The echo returns the persisted STRUCTURE (version, updated_at, config); prompt bodies come back as a stub — you already have the text, and readAssistantText re-reads it. Second step of the propose → confirm flow. See [Propose an agent edit](/api-reference/claudia/endpoints/propose-assistant-edits) for the full walkthrough, with a complete request/response example. # Create an agent Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/create-assistant api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants Creates a supervisor or react agent for this tenant — a knowledge-base Q&A agent included, as `react` with `graphId: qna_agent` — answering the same passthrough shape getAssistant returns. Build `config` in the shape listAssistants and getAssistant return for an existing agent of the same `deployment` — read one first and follow it. `metadata` is server-owned here: anything you send in it is IGNORED, and the created agent carries only the draft markers this endpoint stamps. The agent is created as a draft; a human reviews and publishes it in the ClaudIA app — it does not answer customers until then. This surface has no opt-out: there is no way to create an already-published agent here, and no way to attach the draft to an agent already in production. # Delete a draft agent Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/delete-assistant api-reference/specs/claudia/v1.json DELETE /v1/tenants/{tenant}/assistants/{id} Permanently deletes a DRAFT agent — only drafts can be removed through this API, published agents are managed in the ClaudIA app. Irreversible: there is no undo, and nothing previews the deletion first, so confirm the target with getAssistant before calling this. `id` is the `assistant_id` listAssistants returned; `role` is that row's `deployment_role` — the same pair getAssistant takes. Deleting a draft that a draft supervisor routes to ALSO removes that reference from the supervisor — the routing entry goes with the agent. A draft referenced by an agent this API cannot modify (a published supervisor, or one in a project outside your token) answers 409 instead: remove the reference in the ClaudIA app first. Deleting a draft anchored to a published agent (`metadata.productionAssistantId`) also discards any pending proposed edits it held — they live on the draft. 409 covers three distinct causes: the agent is not a draft (only drafts can be deleted); a draft supervisor referencing this agent is being edited right now and holds its lease (retry shortly); or this draft is referenced by an agent this API cannot modify — a published supervisor, or one in a project outside your token (remove the reference in the ClaudIA app first). # Get one agent Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/get-assistant api-reference/specs/claudia/v1.json GET /v1/tenants/{tenant}/assistants/{id} One agent's STRUCTURE: config.configurable (mcp_servers, agents, model_name, …), name, description and metadata, in the same passthrough shape listAssistants trims. Long prompt fields (system_prompt, tone_of_voice_prompt, and any other key ending in `_prompt`) are NOT returned in full: the key stays, and its body is replaced by a placeholder naming the text's real length, so you can see the field exists and what reading it would cost. Where listAssistants OMITS those keys, this endpoint stubs them — neither answers the prompt text itself. `id` is the `assistant_id` listAssistants returned; `role` is that row's `deployment_role`. A `tenant`/`id` pair your credentials do not cover — including an id from a project your credentials don't reach on this tenant — answers the same 404 as an id that does not exist. # Get one saved version of an agent Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/get-assistant-version api-reference/specs/claudia/v1.json GET /v1/tenants/{tenant}/assistants/{id}/versions/{version} One saved version of an agent, in the same passthrough shape getAssistant returns for the current one: config.configurable, name, description and metadata as they were saved in that version. Long prompt fields (system_prompt, tone_of_voice_prompt, and any other key ending in `_prompt`) are NOT returned in full: the key stays, and its body is replaced by a placeholder naming the text's real length; the full text of a saved version is not available on this surface. `mcp_servers` entries carry only `id`, `tools` and `allTools`: credentials are never returned. `version` is a number listAssistantVersions returned; `id`/`role` are the same pair getAssistant takes. A version that does not exist, a version saved while the agent belonged to a project your credentials don't reach, and a `tenant`/`id` your credentials do not cover — including an id from a project your credentials don't reach on this tenant — answer the same 404 as an id that does not exist. # Get a conversation summary Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/get-conversation-summary api-reference/specs/claudia/v1.json GET /v1/conversations/{id}/summary Lean conversation summary — status, resolution and messages — by CloudChat conversation id or your helpdesk's ticket id. An id from a project your credentials don't cover answers the same 404 as an id that does not exist. # Get a message's execution trace Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/get-message-trace api-reference/specs/claudia/v1.json GET /v1/messages/{messageId}/trace The execution tree behind one processed message: the agent hand-offs, tool calls, generations and routing decision that produced it, with per-step timing and status. Take `messageId` and `conversationId` from getConversationSummary's message list — a response message's own id resolves it directly; a customer message's id also resolves the same trace, as a fallback. Prompt and generation CONTENTS (the assembled system prompt, tool payloads, model completions) and section content are never included — resolve a section's text with `getEntry`. The response also carries `modelDecision`, ClaudIA's persisted record of what the model saw and used: the sections retrieval brought (with score and selection flags), the ids it declared it used, the classifier's routing (RESPOND/CLARIFY/NO_VALID_CONTENT) and the written reasoning, including the routing judge's. # Chat with one agent in a test conversation Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/invoke-assistant api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/{id}/invoke Starts or continues a TEST CONVERSATION with ONE agent — a draft included, which is what this is for: chat with an agent you just created or edited before a human publishes it. Each call is one turn: you send what the customer would say, the agent answers. The first call opens a new conversation and returns its `threadId`; send that `threadId` back on the following calls to keep talking in the same conversation — the agent remembers everything already said in it, so you can test follow-up questions, corrections and multi-step flows the way a real customer would go through them. Omit `threadId` to start over from scratch. The agent runs isolated: only its own prompt, tools and knowledge, with NONE of the ClaudIA pipeline around it (no classification, no hand-off rules, no ticket, and nothing reaches a customer or an inbox) — to see what a customer would actually receive from the whole pipeline, use runPlayground. This call spends real model tokens on the tenant's account. It blocks until the agent is done, which is seconds to a couple of minutes. `id`/`role` are the same pair getAssistant takes. # List agent tools Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/list-agent-tools api-reference/specs/claudia/v1.json GET /v1/tenants/{tenant}/agent-tools Lists every tool available to the tenant, aggregated across its MCP servers. Each tool carries a `ref` field — its canonical id in the form {slug}__{tool_name} — and the `mcpServerId` of the server it comes from. When setting an assistant's toolset on createAssistant, group the tools by server: one mcp_servers entry per distinct `mcpServerId`, shaped {id: , tools: [, …]}. For a server whose catalog depends on the caller, send {id: , allTools: true} with no `tools` instead — not a workaround for a server that failed to list. Put the `ref` value VERBATIM into mcp_servers[].tools. Do NOT compose the id yourself from the slug and name, and never use the display `name` — the runtime whitelist matches `ref` byte-for-byte, so any hand-built or mismatched id is silently dropped or hard-rejected. This catalog lists published tools only — unpublished (draft) tools never appear here. The catalog can also be PARTIAL: a server that did not answer is named in `failedServerSlugs` and its tools are simply missing from this 200, so a non-empty `failedServerSlugs` means "incomplete", not "that tool does not exist". # List an agent's saved versions Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/list-assistant-versions api-reference/specs/claudia/v1.json GET /v1/tenants/{tenant}/assistants/{id}/versions Every saved version of one agent, newest first: its version number, when it was saved, and whether it belongs to a draft. Versions only, no configuration: read one version's configuration with getAssistantVersion. Every save creates a version, including a save that changed only metadata, so two consecutive versions can carry the same configuration. A draft of a published agent is a separate agent with its own id and its own history: to follow a draft's saves, pass the draft's id (`metadata.draftAssistantId` on the published agent getAssistant returns). `id`/`role` are the same pair getAssistant takes; a `tenant`/`id` your credentials do not cover — including an id from a project your credentials don't reach on this tenant — answers the same 404 as an id that does not exist. # List agents Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/list-assistants api-reference/specs/claudia/v1.json GET /v1/tenants/{tenant}/assistants Every agent (assistant) configured for this tenant: the supervisor(s) that orchestrate and the react specialists they route to. Each row is trimmed for discovery — long prompt fields (system_prompt, tone_of_voice_prompt and similar) are OMITTED — and carries the `assistant_id` getAssistant takes, plus `deployment_role`, which is the value getAssistant's `role` parameter expects (`supervisor`, `react`, or `qna` for knowledge-base Q&A agents). Discover the tenants your credentials cover with listMyClaudiaProjects. # Propose an agent create Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/propose-assistant-create api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/propose-create First step of a safe two-step create — nothing is created yet. Send the full create body (for a knowledge-base Q&A agent: `deployment: react`, `graphId: qna_agent`, and `knowledge_base_ids` inside `config`): `config` takes the shape listAssistants and getAssistant return for an existing agent of the same `deployment`, and must carry `project`. `metadata.productionAssistantId` is the ONLY metadata key accepted here — set it to anchor the new draft to a PUBLISHED agent of the same `deployment` on this tenant, which is the pairing proposeAssistantEdits coaches you toward when a published agent has no draft yet; any other metadata key is refused. Returns `message`, `diff`, `preview` and `confirmationId`: `diff` renders the whole state as `+` lines with prompt bodies STUBBED to their real length (`[N chars — use readAssistantText]`) — the full text is exactly what you authored and is what gets created — while `preview` is that same state as it will persist (server defaults and coordinates applied), prompt bodies stubbed the same way. Review the diff, then call confirmAssistantCreate with just that confirmationId; no other body field is read. The agent is ALWAYS created as a DRAFT — a human reviews and publishes it in the ClaudIA app, and this surface has no opt-out. Same two-step contract as editing — see [Propose an agent edit](/api-reference/claudia/endpoints/propose-assistant-edits) for the full walkthrough. The differences here: the confirm step needs no `If-Match` (a create has no prior state to pin), and `metadata.productionAssistantId` is how you anchor the new draft to a published agent — the way to edit a published agent through this API, since edits only ever land on drafts. # Propose an agent edit Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/propose-assistant-edits api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/{id}/propose-edits First step of a safe two-step edit — nothing is written yet. Send anchored `edits` (each replacing ONE excerpt copied verbatim from the current text — read it first with getAssistant/readAssistantText), whole-value `config`/`name`/`description` keys (RFC 7386 merge patch: omit a key to keep it, null clears it, arrays are replaced whole), or both in the same call. `config` takes the agent's config keys DIRECTLY — never nested under `configurable`. At least one of `edits`, `config`, `name` or `description` must carry a change. Returns {message, diff, confirmationId, baseUpdatedAt}: review the diff line by line — especially `-` lines — then call confirmAssistantEdits with just confirmationId and If-Match=baseUpdatedAt; no other body field is read, the server replays what it stored. Only a DRAFT accepts edits: a published agent answers 409 DRAFT_REQUIRED, naming its draft or how to create one. ## The propose → confirm flow Every write to an agent's config, name, or description goes through two calls: a **propose** that previews the exact change as a diff and mints a confirmation token, and a **confirm** that sends the token back and applies exactly what was previewed — nothing else. This section walks through one complete round trip for an edit; [proposeAssistantCreate](/api-reference/claudia/endpoints/propose-assistant-create) and [confirmAssistantCreate](/api-reference/claudia/endpoints/confirm-assistant-create) follow the same shape for creating a new agent. **Draft-first, always.** Both pairs only ever write to a **draft** agent. A published agent answers `proposeAssistantEdits` with `409 DRAFT_REQUIRED` — you cannot edit production directly. If the published agent already has a paired draft, the error names it (`metadata.draftAssistantId`); otherwise, create one by calling `proposeAssistantCreate`/`confirmAssistantCreate` with `metadata.productionAssistantId` set to the published agent's id (same `deployment`, same tenant). That anchors the new draft to it, which is how you stage a change to a live agent. Publishing the draft to production is a human action in the ClaudIA app — no public endpoint does it. ### 1. Propose Send the change: one or more anchored `edits` (each replacing a verbatim excerpt of existing text), whole-value `config`/`name`/`description` keys, or both. ```json Request theme={null} POST /claudia/v1/tenants/acme/assistants/asst_9f2a.../propose-edits?role=react { "edits": [ { "path": "config.tone_of_voice_prompt", "old_string": "Always answer in a formal, corporate tone.", "new_string": "Answer warmly and use the customer's first name when known." } ] } ``` The response previews the write and never applies it: ```json Response theme={null} { "message": "THESE ARE THE CHANGES YOU ARE ABOUT TO APPLY — REVIEW the diff (git unified format) LINE BY LINE, paying special attention to lines starting with `-` (removals). If they are correct, confirm by calling confirmAssistantEdits with just confirmationId=... and If-Match=...", "diff": "--- current\n+++ proposed\n@@ -4,7 +4,7 @@\n model_name: gpt-4o\n system_prompt: You are ClaudIA, a customer support agent for Acme.\n tenant: acme\n- tone_of_voice_prompt: Always answer in a formal, corporate tone.\n+ tone_of_voice_prompt: Answer warmly and use the customer's first name when known.\n tools: []\n deployment_role: react\n updated_at: 2026-08-20T14:03:11.482Z", "confirmationId": "8f0c8e2a1e6b4a6e9c1a2f7d3b8e9c10", "baseUpdatedAt": "2026-08-20T14:03:11.482Z" } ``` ### 2. Review the diff `diff` is a unified diff (`--- current` / `+++ proposed`) computed over the **whole assistant**, not just the field(s) you touched: both sides are rendered as sorted `key: value` lines (two spaces per nesting level, long prompts as block scalars) and diffed, so neighboring fields show up as context lines around your change — as in the example above, where `model_name`/`system_prompt`/`tenant` and `tools`/`deployment_role`/`updated_at` frame the one line that actually changed. Read it line by line, paying special attention to `-` lines — those are what disappears. Nothing has been written yet: if the diff looks wrong, discard it and send a corrected `proposeAssistantEdits` call instead of confirming. ### 3. Confirm Send back **only** `confirmationId`, plus `baseUpdatedAt` as the `If-Match` header. No other body field is read — the server replays the exact proposal it stored server-side. ```json Request theme={null} POST /claudia/v1/tenants/acme/assistants/asst_9f2a.../confirm-edits?role=react If-Match: 2026-08-20T14:03:11.482Z { "confirmationId": "8f0c8e2a1e6b4a6e9c1a2f7d3b8e9c10" } ``` ### 4. The stubbed echo The 200 response is the persisted agent, in the same shape `getAssistant` returns — including the same stub on long prompt fields. You already authored the new text in step 1, so it is not echoed back in full; read it again with `readAssistantText` if you need it. ```json Response theme={null} { "assistant_id": "asst_9f2a...", "deployment_role": "react", "updated_at": "2026-08-20T14:05:47.201Z", "config": { "configurable": { "tone_of_voice_prompt": "[59 chars — use readAssistantText]" } } } ``` If `confirmationId` no longer resolves — the proposal's TTL expired — the 404 names `PROPOSAL_NOT_FOUND` and coaches re-running `proposeAssistantEdits` with the same body. Unlike the create pair, an edits confirm is **not** consumed on success: retrying the exact same confirm after it already applied does not 404\. Its own write moved `updated_at`, so the retry's `If-Match` is now stale against the current agent and it fails the same way any other stale confirm does — `412 STALE_PRECONDITION`. If you see that, re-read the agent with `getAssistant` first: the change may already be applied, in which case there is nothing left to do; only propose again if it genuinely is not there. # Read one prompt field's full text Source: https://docs.cloudhumans.com/api-reference/claudia/endpoints/read-assistant-text api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/{id}/text Reads the FULL text of ONE field getAssistant stubs out or omits — the same prompt bodies it replaces with a `[N chars — use readAssistantText]` placeholder: `config.system_prompt`, `config.tone_of_voice_prompt`, and any other `config.` ending in `_prompt`; the top-level `name`/`description` also resolve. Answers the WHOLE value in one response — expect it to be large, and make one call per field: unlike the internal reader, there is no windowing, pagination or search on this surface. `id`/`role` are the same pair getAssistant takes; a `tenant`/`id` your credentials do not cover — including an id from a project outside them — answers the same uniform 404 as getAssistant. # Sign in Source: https://docs.cloudhumans.com/api-reference/claudia/sign-in Get the token every Claudia API call needs, including the two-step path. Every Claudia API call carries the `id_token` from `POST /auth/v1/signin` as `Authorization: Bearer ` — not the `access_token`, which does not carry the identity we authorize on. One token works across every Cloud Humans API, and the account you reach is derived from it, so there is nothing else to configure. Sign in below to get one. **Your password goes to `api.cloudhumans.com`, and nowhere else.** It is sent there once, over HTTPS, to get you a token — this page makes no other request with it, stores nothing, and logs nothing. You can confirm that in your browser's network tab. `api.cloudhumans.com` is also the only place your token should *ever* go. Any other site offering to "check" or "decode" it for you is not one to trust. ## Not every sign-in ends in a token Accounts with a temporary password, or with two-step verification turned on, come back with a **challenge** instead: no token yet, and a `session` to answer it with. The form above handles that for you — the second step appears only when there is one, already carrying what the first step returned. Building this into your own client means writing a loop rather than a single call, because answering a challenge can return **another** one: | The response contains | What it is | What you send back | | - | - | - | | `id_token` | Done. | Nothing. Use it. | | `challenge: "SMS_MFA"` | A code was just texted to the phone on the account. | `code`, with the `session` | | `challenge: "NEW_PASSWORD_REQUIRED"` | The password is temporary — first access, or it was reset. | `new_password`, with the `session` | | `error` | Nothing to answer. | Start over. | **Branch on the body, not on the status** — both outcomes are `200`. A first-access user who also has two-step verification really does go `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, and each step returns a **new** `session` that replaces the one before it. Three details worth knowing before your own client meets a real user: **Send the code as a string.** Codes can start with a zero, and `"012345"` parsed into a JSON number is a different code. A number is rejected with a `400` rather than coerced, precisely so this shows up on your first test instead of as one user in ten who "can't log in". **A wrong code and an expired session look identical.** Both come back `400 CodeMismatchException`, so your UI cannot honestly say which happened. Offer a retry, and next to it a way back to the start — retrying against an expired session never recovers. **The session is not yours to keep.** It belongs to one sign-in attempt, so there is nothing to persist and nothing to resume. When it stops being accepted, the only move is `/auth/v1/signin` again, which sends a fresh code. Full request and response shapes are on the [sign-in](/api-reference/claudia/v1/auth/authentication/sign-in-and-get-a-token) and [challenge](/api-reference/claudia/v1/auth/authentication/answer-a-sign-in-challenge) pages. # Answer a sign-in challenge Source: https://docs.cloudhumans.com/api-reference/claudia/v1/auth/authentication/answer-a-sign-in-challenge /api-reference/specs/auth/v1.json post /v1/signin/challenge Finish a sign-in that came back with a `challenge` instead of a token. Send the `session` from that response together with the answer the challenge asks for — the SMS code, or the new password. Answer with the token in mind, not the step: this endpoint returns the **same body shape** as `/v1/signin`, so it can hand back another challenge rather than a token. A user on first access *with* two-step verification really does go `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, each step handing you a fresh `session`. Loop until `id_token` shows up. The `session` belongs to the sign-in attempt that produced it and is not built to be kept — there is nothing to persist and nothing to resume. Once it stops being accepted, call `/v1/signin` again, which sends a new code. # Sign in and get a token Source: https://docs.cloudhumans.com/api-reference/claudia/v1/auth/authentication/sign-in-and-get-a-token /api-reference/specs/auth/v1.json post /v1/signin Authenticate with email and password and receive the token used by every other API. One token covers Claudia, Cloud Chat and the services that follow — the account you see is derived from it, and the same token is valid against production and staging. **A `200` does not always carry a token.** Accounts with a temporary password, or with two-step verification turned on, come back with a *challenge* instead: no token yet, and a `session` to answer it with at `POST /v1/signin/challenge`. Branch on the body, not on the status — `id_token` present means done, `challenge` present means one more step. # Approve suggestions Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-improvements/approve-suggestions /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/improvements/approve Publish suggested content so Claudia starts answering with it. Idempotent and tolerant: read the per-id outcome — an id already decided is skipped, and one that still carries a blank is blocked until you edit it with updateImprovement. # Count the review queue Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-improvements/count-the-review-queue /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/improvements/summary How many suggestions are waiting, and how many of those can be approved as they are — the rest still carry a blank to fill in first. # Edit a suggestion before approving it Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-improvements/edit-a-suggestion-before-approving-it /api-reference/specs/claudia/v1.json put /v1/knowledge-bases/{kbId}/improvements/{entryId} Rewrite a pending suggestion — typically to fill in a `[COMPLETAR]` blank that blocks approval. Only suggestions can be edited here; ordinary content is edited through updateEntry. # List suggested content Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-improvements/list-suggested-content /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/improvements The review queue: content Claudia proposes for questions your knowledge base did not answer. Each row carries why it was proposed and how many conversations hit the gap, so you can decide what is worth publishing. # Reject suggestions Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-improvements/reject-suggestions /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/improvements/reject Dismiss suggestions you do not want. Nothing is deleted — they leave the queue, and the pipeline will not propose the same gap again. # Approve proposed rewrites Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-quality/approve-proposed-rewrites /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/improvements/quality/approve Apply the proposed text to your content, so Claudia starts answering with it. Idempotent and tolerant: read the per-id outcome — an id already decided is skipped, a restructuring proposal is blocked, and an id that failed to apply was rolled back and is safe to retry. # Count the quality review Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-quality/count-the-quality-review /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/improvements/quality/summary How many rewrites are waiting, how many were already decided, and what the waiting ones would do to your content. # Edit a proposal before approving it Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-quality/edit-a-proposal-before-approving-it /api-reference/specs/claudia/v1.json put /v1/knowledge-bases/{kbId}/improvements/quality/{id} Adjust the proposed text on a proposal that is still waiting. This changes the proposal, not your content — nothing is published until you approve it. # List proposed rewrites Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-quality/list-proposed-rewrites /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/improvements/quality The quality review queue, most impactful first. Each row pairs the answer as analysed with the rewrite proposed for it, plus the diagnosis and the performance that justify the change. # Reject proposed rewrites Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-quality/reject-proposed-rewrites /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/improvements/quality/reject Dismiss rewrites you do not want. Your content is never touched — the proposals simply leave the queue. # Create or update a content source Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-sources/create-or-update-a-content-source /api-reference/specs/claudia/v1.json patch /v1/knowledge-bases/{kbId}/scraper-config Send configId to change an existing source, or omit it to create one — a configId that matches no source of this knowledge base is refused, never created under the id you sent. Every field is optional and an omitted one keeps what is stored. maxAgeMs is the exception worth reading: omit it to keep the current freshness window, send null to restore the crawler's default, send a number of milliseconds to apply it — 0 forces a full re-crawl on the next run. # Delete a content source Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-sources/delete-a-content-source /api-reference/specs/claudia/v1.json delete /v1/knowledge-bases/{kbId}/scraper-config/{configId} Stop crawling a site. Content already imported from it stays in the knowledge base — remove it with deleteEntry if you no longer want it. # List content sources Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-sources/list-content-sources /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/scraper-config The sites configured to be crawled into this knowledge base. Each source says where the crawl starts, what it may follow, and when it runs. The content a crawl produces is read through listEntries like any other. # Run a content source now Source: https://docs.cloudhumans.com/api-reference/claudia/v1/content-sources/run-a-content-source-now /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/scraper-config/{configId}/run Starts a full run of one content source right away, instead of waiting for its cron: the site is crawled, the pages are processed and the resulting entries are written into this knowledge base. The run happens in the background and this call answers as soon as it is accepted — it does not wait for the crawl to finish, and there is no completion signal on this surface: read the imported content through listEntries afterwards (a large site takes minutes). Only one run per source at a time: a second call while a run is in progress is refused with 409; once it finishes, another call starts another full run. # Create a content entry Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/create-a-content-entry /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/entries Add a single piece of content. Use bulk import when loading many at once. # Delete a content entry Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/delete-a-content-entry /api-reference/specs/claudia/v1.json delete /v1/knowledge-bases/{kbId}/entries/{entryId} Permanently remove an entry. # Enable or disable an entry Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/enable-or-disable-an-entry /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/entries/{entryId}/toggle-disable Flip an entry between active and disabled. Disabled content stays stored but is not used to answer. # Get a content entry Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/get-a-content-entry /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/entries/{entryId} A single piece of content by id. # Import content in bulk Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/import-content-in-bulk /api-reference/specs/claudia/v1.json post /v1/knowledge-bases/{kbId}/entries/import Create up to 200 entries in one call. Each item is validated on its own and the response reports successes and failures separately, keyed by your clientRef, so one bad row does not sink the batch. # List and search content Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/list-and-search-content /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/entries Paginated content listing. Filter exactly by label, topic, status or source, or set semanticSearch=true to rank by meaning instead. Interactive flows are never returned here. # List tags in use Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/list-tags-in-use /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/entries/tags Every tag currently applied to content in this knowledge base. # List topics in use Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/list-topics-in-use /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId}/entries/topics Every topic currently applied to content in this knowledge base. # Update a content entry Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-base-content/update-a-content-entry /api-reference/specs/claudia/v1.json put /v1/knowledge-bases/{kbId}/entries/{entryId} Replace the fields of an existing entry. # Create a knowledge base Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-bases/create-a-knowledge-base /api-reference/specs/claudia/v1.json post /v1/knowledge-bases Create an empty knowledge base under one of your accounts. Name the account by its accountId + instance pair — the pair, not accountId alone, identifies an account. Knowledge-base reach follows the accounts provisioned to your tenants, carried in the token, NOT your CloudChat memberships: discover the pairs you can create under with an unfiltered listMyKnowledgeBases, where every row carries them. An account that holds no knowledge base yet does not appear there; take its pair from listMyCloudChatAccounts then, knowing that listing is CloudChat membership and an account from it may still be refused here with 403 — nothing is created when that happens. The new base starts with no entries and is not linked to any project; add content with createEntry or importEntries, or configure a crawl with upsertContentSource. # Get a knowledge base Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-bases/get-a-knowledge-base /api-reference/specs/claudia/v1.json get /v1/knowledge-bases/{kbId} A single knowledge base by id. Every other endpoint takes this id, so this is how you confirm one is reachable with your credentials before operating on it. # List knowledge bases Source: https://docs.cloudhumans.com/api-reference/claudia/v1/knowledge-bases/list-knowledge-bases /api-reference/specs/claudia/v1.json get /v1/knowledge-bases Every knowledge base this client can manage. Accounts come from the token: if your credentials cover several, all of them are listed together and each row carries the accountId + instance pair that identifies its account. accountId and instance are optional filters — they narrow the result, never widen it. An account is named by the pair, not by accountId alone: the same accountId exists in more than one instance, so filtering on accountId by itself can return rows from several accounts, which may belong to different customers. Send both to address one account. listMyCloudChatAccounts returns each accountId together with its instance. # Delete an MCP server Source: https://docs.cloudhumans.com/api-reference/claudia/v1/mcp-servers/delete-an-mcp-server /api-reference/specs/claudia/v1.json delete /v1/tenants/{tenant}/mcp-servers/{id} Remove a server and its stored credential. Agents configured to use it stop being able to call its tools, so check what references the slug before deleting. Servers CloudHumans owns cannot be deleted. # List MCP servers Source: https://docs.cloudhumans.com/api-reference/claudia/v1/mcp-servers/list-mcp-servers /api-reference/specs/claudia/v1.json get /v1/tenants/{tenant}/mcp-servers Every MCP server registered for this tenant, including the ones CloudHumans provides to everyone. Start here: the ids returned are what the update, delete and probe operations take. Credentials are never part of the answer — `credentialsStored` only says whether one is on file. # Probe an MCP server Source: https://docs.cloudhumans.com/api-reference/claudia/v1/mcp-servers/probe-an-mcp-server /api-reference/specs/claudia/v1.json post /v1/tenants/{tenant}/mcp-servers/validate Ask a server for its tool list, with the same call the agent runtime makes, and report what happened. Nothing is registered or changed. Use it before createMcpServer to check an endpoint and its credential, and afterwards with `serverId` to check whether a server you already registered is still answering. # Register an MCP server Source: https://docs.cloudhumans.com/api-reference/claudia/v1/mcp-servers/register-an-mcp-server /api-reference/specs/claudia/v1.json post /v1/tenants/{tenant}/mcp-servers Add a server so its tools become available to this tenant's agents. Probe the endpoint with validateMcpServer first: a passing probe is what lets you send `verified: true`, which is what makes the runtime call the url exactly as you wrote it. # Update an MCP server Source: https://docs.cloudhumans.com/api-reference/claudia/v1/mcp-servers/update-an-mcp-server /api-reference/specs/claudia/v1.json put /v1/tenants/{tenant}/mcp-servers/{id} Change a registered server. Omitted fields keep what is stored, so send only what changes. Two rules are worth knowing before you call: switching `authType` requires sending `credentials` in the same request, and `customHeaders` replaces the stored map wholesale rather than merging into it. Servers CloudHumans owns cannot be changed. # Send a message to ClaudIA and read its answer Source: https://docs.cloudhumans.com/api-reference/claudia/v1/playground/send-a-message-to-claudia-and-read-its-answer /api-reference/specs/claudia/v1.json post /v1/playground/invoke Runs your ClaudIA for real against a message you write — the live project your customers are served by, with its own prompts, knowledge base, tools, model, classification and hand-off rules — and answers with what the customer would have received and where the ticket would have gone. Nothing reaches a customer and no production ticket is created; the run is mirrored into your CloudChat playground inbox, so it leaves a trace you can open. The run is a real one: it spends LLM tokens you are billed for and it calls the project's tools for real, which may touch external systems. Treat every call as costly and deliberate — ask the question a customer would ask, read the answer, and change something before asking again; never loop on it to see whether the reply changes. Pass `abKey` to run an A/B variant of the configuration instead of the live one, which is how two configurations are compared without editing the project. The call blocks until ClaudIA is done, which is seconds to a couple of minutes; a run that outlives its budget answers 504 rather than a partial reply. # Change a project's settings Source: https://docs.cloudhumans.com/api-reference/claudia/v1/project-settings/change-a-projects-settings /api-reference/specs/claudia/v1.json patch /v1/projects/{project}/settings Changes only the settings you send. A key you omit keeps its stored value, so send the keys you mean to change and nothing else — this is a merge patch, not a replacement. Nested settings are sent nested (`{"greetingsSettings": {"staticGreeting": "Oi!"}}`), which changes that one greeting and leaves the rest of the section alone. Read getProjectSettings first: the keys it returns to you are the keys accepted here, and anything else — a field your role cannot write, or a key that is not a setting at all — is refused by name with nothing saved. Every accepted call is recorded in getProjectSettingsHistory with the value on each side of the change, which is what makes it reversible. # List a project's A/B variants Source: https://docs.cloudhumans.com/api-reference/claudia/v1/project-settings/list-a-projects-ab-variants /api-reference/specs/claudia/v1.json get /v1/projects/{project}/ab-keys The A/B variant keys this project declares. A variant replaces the settings it declares — prompt, model, whatever it overrides — for a single run, which is how one configuration is compared against another without editing the project. Send one of these as `abKey` to runPlayground; a key that is not listed here is refused there rather than silently ignored. Most projects declare none and this answers an empty list. # Read a project's settings Source: https://docs.cloudhumans.com/api-reference/claudia/v1/project-settings/read-a-projects-settings /api-reference/specs/claudia/v1.json get /v1/projects/{project}/settings Every setting of this project your role is allowed to see, as one object. This is also the discovery call for writing: the keys it returns, minus the ones it lists in `readOnlyPaths`, are exactly the keys updateProjectSettings will accept from you. What you may see is decided per account by CloudChat and resolved on every call, so read this against the account you intend to write through. # Read who changed a project's settings, and to what Source: https://docs.cloudhumans.com/api-reference/claudia/v1/project-settings/read-who-changed-a-projects-settings-and-to-what /api-reference/specs/claudia/v1.json get /v1/projects/{project}/settings/history Every save made to this project's settings, newest first, with the value on each side of each change — which is how a change is undone: patch the field back to its `before`. Filtered by the same permissions as getProjectSettings, so a save that touched a field you cannot read is still listed but shows only the changes you can see. Pass `field` to follow a single setting's whole history. # Find your credentials Source: https://docs.cloudhumans.com/api-reference/cloudchat/credentials Sign in, then get the cloudchat-instance header and account id every call needs. Every Cloud Chat API call needs three things: a token, your `cloudchat-instance` header, and one of your account ids. This page gets you all three, and ends with a request you can run as-is. ## Sign in Some accounts do not get a token on the first try — a temporary password or two-step verification comes back as a **challenge** instead. The form handles that: the second step appears only when there is one, already carrying what the first step returned, so the only thing left to type is the code from the text message. Writing this into your own client means writing a loop, not a single call. [Signing in is a loop, not a call](/api-reference/cloudchat/overview#signing-in-is-a-loop-not-a-call) on the overview page has the state machine and the three things worth knowing before a real user meets it. ## Find your instance and account Paste the `id_token` from above. The values are remembered, so they show up on every endpoint page from here on. **Your password and your token go to `api.cloudhumans.com`, and nowhere else.** Each is sent there over HTTPS — the password once, to get you a token; the token once, to look up your account names. This page makes no other request with either, stores neither, and logs nothing. You can confirm that in your browser's network tab. That is also why the token is pasted rather than carried over for you: nothing here is kept, so there is no credential of yours sitting at rest in your browser. `api.cloudhumans.com` is also the only place your token should *ever* go. Any other site offering to "check" or "decode" it for you is not one to trust. ## If the values are missing or don't work | Symptom | What it means | | - | - | | Account names never appear, only ids | The name lookup is unavailable from your browser. Harmless — the ids shown are correct and every call works with them. | | The box says the token carries no accounts | You pasted the `access_token` instead of the `id_token`, or your user has no Cloud Chat access yet. | | An account you expect is missing | If the access was granted after you signed in, your token predates it. Sign in again for a fresh one. | | An account you no longer have still appears | Same cause in reverse: your token predates the change. It disappears once the name lookup answers, or after you sign in again. | | `400` with `cloudchat-instance header is required` | The header never arrived. It is rejected before authentication, so this is not a token problem. | | `401` on every call | Expired or malformed token. Sign in again; `expires_in` in the sign-in response says how long the new one lasts. | | `404` with a correct-looking token | Right token, wrong pairing — and this is the common one. Either the account id isn't on the instance you sent, or your user has no access to it. Those two answer identically on purpose, so re-check both values above. | | `403` with a correct-looking token | Different problem: you *do* have access and the account is **suspended**. Nothing will work on it until that's resolved. | | An account the box won't generate a call for | The token names an internal instance, which this API does not serve. | Both of the box's sources are things you can call yourself. **Ask the API** — this one includes account names: ```bash theme={null} curl https://api.cloudhumans.com/auth/v1/me/cloudchat-accounts \ -H "Authorization: Bearer $ID_TOKEN" ``` ```json theme={null} [{ "instance": "1", "accountId": 1, "accountName": "Cloudhumans" }] ``` **Read the token** — ids only, and no network call. A JWT payload is base64url JSON, so the claim is one command away: ```bash jq theme={null} echo "$ID_TOKEN" | cut -d. -f2 \ | tr '_-' '/+' \ | base64 -d 2>/dev/null \ | jq -r '."custom:cloudchat_accounts"' ``` ```python python theme={null} import base64, json, os payload = os.environ["ID_TOKEN"].split(".")[1] payload += "=" * (-len(payload) % 4) claims = json.loads(base64.urlsafe_b64decode(payload)) print(claims["custom:cloudchat_accounts"]) ``` You get something like `1:1:1,1:2:0`, read as `instance:account:role`. The role is a number — `0` agent, `1` administrator, `2` supervisor, `3` assistant, `4` cx\_engineer — and it has no effect on the values you send; it is your permission level inside the account, which Cloud Chat enforces on its own. **The endpoint is the authority; the claim is an offline approximation of it.** Cloud Chat authorizes on your live access, which is what the endpoint reads. The claim is a photograph taken when you signed in, so it can still list an account whose access was revoked since, or miss one granted since. That is why the box shows the endpoint's answer whenever it gets one, rather than merging the two — a merged list would offer you accounts your calls would only 404 on. Use the claim when you want an answer with no network call, and re-read it after signing in again. If you have devtools open you may see the name lookup fail with a CORS error while that endpoint is still rolling out. Nothing is broken — it is why the box falls back to reading your token, and the ids it shows are the ones the API accepts. # Add team members Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/add-team-members api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/teams/{teamId}/members Administrator only. **Additive and idempotent — it never removes a member left out of the payload.** There is no endpoint to remove one through this API. Refused with 403 on a `system_managed` team. Any `user_ids` entry that is not a member of the account — including a hidden admin's id, indistinguishable from an unknown one — fails the whole call with 404; nothing is added. # Create a campaign Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-campaign api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/campaigns **Creating a campaign sends it.** There is no separate step that starts the send, and no way to call it back: whatever you create here goes out. `confirm_recipients` has to equal the `reachable` the preview returned for the same audience in the same channel; if it does not, the answer is 409 with both numbers and nothing is created. **Do not repeat this call after a timeout.** A repeat creates a second campaign and sends a second time, and every WhatsApp message is billed. Reading the campaign back only helps if you already hold its `id`: there is no listing and no idempotency key, so a lost response cannot be resolved through this API — open the campaign dashboard in Cloud Chat to see whether it was created before you send again. The channel follows from what you send. A campaign carrying `email_campaign_sender_id` is an e-mail campaign; otherwise the channel comes from the `channel_type` of the inbox behind `inbox_id`: `Channel::Whatsapp` sends a WhatsApp template, `Channel::WebWidget` sends a widget message. **Check `channel_type` first**, because nothing cross-checks your intent: a website inbox chosen while you meant to send WhatsApp delivers the template name to your contacts as the literal text of the message. **This response is not a delivery confirmation.** Sending happens after it returns and can still fail; what actually reached your contacts is in the Cloud Chat campaign dashboard. Every WhatsApp message is billed and reaches a real person, so do not send one merely to rehearse. To check an audience without sending, use the preview endpoint. # Create a canned response Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-canned-response api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/canned-responses Both fields are required. `short_code` has to be unique within the account — reusing one is a 422, not an overwrite. # Create a category Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-category api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/portals/{portalSlug}/categories Create a category in the portal. Its `id` is what goes into `category_id` when [creating an article](/api-reference/cloudchat/endpoints/create-an-article), and its locale is what decides that article's language and translation target. The locale has to be one of the portal's `allowed_locales` — [list portals](/api-reference/cloudchat/endpoints/list-portals) shows the current list and [updating the portal](/api-reference/cloudchat/endpoints/update-a-portal) is how you add one. Requires being an administrator of the account or a member of the portal. # Create a macro Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-macro api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/macros Open to any account member. `name` and `actions` are required; each action's `action_name` must be one of the values on the `MacroAction` schema — anything else is refused as `validation_failed`, naming the unsupported actions. **An agent's macro is always forced to `visibility: personal`**, whatever the payload sends. Only an administrator can create a `global` one. # Require a native attribute Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-native-attribute-requirement api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/native-attribute-requirements Administrator only. Starts requiring one native conversation attribute before a conversation can be resolved. `key` is required and must be one of the fixed set — a key the account already requires is refused with 422. # Create a portal Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-portal api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/portals Create a new help center site on the account. This is the first step of building one entirely through the API: create the portal, [allow the locales you need](/api-reference/cloudchat/endpoints/update-a-portal), [create categories](/api-reference/cloudchat/endpoints/create-a-category), then [create articles](/api-reference/cloudchat/endpoints/create-an-article). The portal is created **already reachable** by visitors, but empty: only `published` articles are ever served and articles start as drafts, so nothing leaks while you fill it in. Its `allowed_locales` starts as exactly the `default_locale` you send, which matters immediately — a category can only exist in an allowed locale, so a second language needs an update first. Requires being an administrator of the account. # Create a team Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-a-team api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/teams Administrator only. `name` is required and unique within the account (stored lowercased, so the comparison is case-insensitive). # Create an agent Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-an-agent api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/agents Administrator only. Invites the agent by email; someone who already has a CloudChat login on another account becomes a member of this one, keeping that login. Consumes one agent license seat — a 402 means the account has none left. # Create an article Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-an-article api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/portals/{portalSlug}/articles Create one article in the portal. It starts as `draft` — invisible on the public help center — unless the payload sends an explicit `status`; the usual flow is create as draft, review, then [publish by updating the status](/api-reference/cloudchat/endpoints/update-an-article). Authorship is always the authenticated user behind the token. The article's locale is inherited from its category, or from the portal default when no `category_id` is sent. Requires being an administrator of the account or a member of the portal. # Create an availability reason Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/create-an-availability-reason api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/availability-reasons Administrator only. `reason` is required. The reason is always created for the `busy` availability — there is no field to choose another, whatever the payload sends. # Delete a canned response Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/delete-a-canned-response api-reference/specs/cloudchat/v1.json DELETE /v1/accounts/{accountId}/canned-responses/{cannedResponseId} Permanent, and it takes any attachments with it. There is no undo and no trash to restore from. # Delete a category Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/delete-a-category api-reference/specs/cloudchat/v1.json DELETE /v1/accounts/{accountId}/portals/{portalSlug}/categories/{categoryId} Permanently remove an **empty** category. Expect the refusal to be the normal answer. A category that still holds articles is refused with 422 and the number of articles; clearing it first — move them with [update an article](/api-reference/cloudchat/endpoints/update-an-article) `category_id`, or delete them — is the main path, not an edge case. Retrying the call unchanged will not start working. The refusal exists because deleting a full category would not fail: it would silently detach its contents, and an article with no category has lost where its language comes from. Categories nested under it and categories linked to it as translations block it the same way, each reported separately under `error.details` with its own count. Archived articles count too — archived is content you can publish again, not deleted content. The response has no body, and there is nothing to restore from afterwards. Requires being an administrator of the account or a member of the portal. # Stop requiring a native attribute Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/delete-a-native-attribute-requirement api-reference/specs/cloudchat/v1.json DELETE /v1/accounts/{accountId}/native-attribute-requirements/{nativeAttributeRequirementId} Administrator only. **This is a hard delete of the requirement, and it is fully reversible**: nothing else references it, and requiring the same key again with [create](/api-reference/cloudchat/endpoints/create-a-native-attribute-requirement) restores the exact same behavior. Conversations keep the attribute itself — agents just stop being forced to fill it before resolving. # Delete an article Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/delete-an-article api-reference/specs/cloudchat/v1.json DELETE /v1/accounts/{accountId}/portals/{portalSlug}/articles/{articleId} Permanently remove an article. **This cannot be undone** and there is no copy to restore from, so use it only to clean up something created by mistake. To take a published article off the air, prefer [update an article](/api-reference/cloudchat/endpoints/update-an-article) with `status: archived`: the public URL stops serving it immediately, exactly as a delete would, and the article can be published again later. Deleting is the wrong tool for retiring content whose link may already be circulating. Unlike a category, an article carries no guard: it holds nothing that would be orphaned. Articles that reference it as their translation stay where they are; only the link between them is dropped. Requires being an administrator of the account or a member of the portal. # Delete an availability reason Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/delete-an-availability-reason api-reference/specs/cloudchat/v1.json DELETE /v1/accounts/{accountId}/availability-reasons/{availabilityReasonId} Administrator only. **This is a soft delete** — the reason drops out of [list availability reasons](/api-reference/cloudchat/endpoints/list-availability-reasons) but is not destroyed, so the availability history of agents who used it stays intact. There is no way to undelete through this API. # Find a conversation by any identifier Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/find-a-conversation api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/conversations/find Locates conversations from an identifier in whatever form you have it — the number in the dashboard URL (**display_id**), the ticket id in your own helpdesk (Zendesk, Intercom and the like), or the conversation **uuid**. Every interpretation that matches is returned, newest first, capped at 10, and `matched_by` names the one that matched. Visibility follows the same rule as the list: an administrator token reaches every inbox of the account, an agent token only its own inbox memberships. An empty `matches` means the id does not exist on this account or is outside the token's reach — the two are deliberately indistinguishable. # Get a campaign Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-campaign api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/campaigns/{campaignId} Read one campaign back by the id you were given when it was created. **No endpoint lists campaigns**, so an id you did not keep cannot be recovered. **`campaign_status` is not a delivery report.** `completed` means a worker took the campaign over, not that anything arrived: on WhatsApp the status is set before the audience is even resolved, and `started_at` stays null for the whole life of a WhatsApp campaign. For what was actually delivered, open the campaign dashboard in Cloud Chat. `reachable_count` is not part of this response: reach is computed when the campaign is created and never stored. # Get a canned response Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-canned-response api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/canned-responses/{cannedResponseId} A single canned response by id. Useful to re-read one after an edit, or to check `updated_at` before overwriting something another user may have changed. # Get a category Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-category api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals/{portalSlug}/categories/{categoryId} Read one category by its numeric id. Useful to confirm what an update stored, or to read the `locale` an article filed under it will inherit. To browse or search, [list categories](/api-reference/cloudchat/endpoints/list-categories) returns the same fields for the whole portal. Any member of the account may read it; the listing carries the same gate. # Get a conversation Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-conversation api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/conversations/{conversationId} A single conversation by its **display_id** — the number in the dashboard URL and in webhooks, not the internal database id. Visibility follows the same rule as the list: an administrator token can reach every inbox of the account, an agent token only its own inbox memberships. A conversation whose inbox is outside that reach answers 404, indistinguishable from an id that does not exist. # Get a macro Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-macro api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/macros/{macroId} A single macro by id. A `global` macro is visible to any member; a `personal` one only to its author — a personal macro of someone else answers 404, the same as an id that does not exist. # Get a portal Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-portal api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals/{portalSlug} Read one portal by its slug. Useful to confirm what an update stored, or to read `allowed_locales` before creating a category — a category can only exist in a locale that list contains. If you want every portal of the account, [list portals](/api-reference/cloudchat/endpoints/list-portals) returns the same fields for all of them in one call. Any member of the account may read it; the listing carries the same gate. # Get a team Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-a-team api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/teams/{teamId} A single team by id. Open to any member. # Get agent access Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-agent-access api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/agents/{agentId}/access Which Claudia projects and Eddie workspaces `agentId` can currently act as (`agent_access`), and your own ceiling for this account (`manageable`) — the exact set of projects and workspaces your credential is allowed to grant or revoke via setAgentAccess. Administrator only. Both objects always list `claudia_projects` and `eddie_workspaces`, even when a dimension is empty — never `null`. # Get an agent Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-an-agent api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/agents/{agentId} A single agent by id. Answers 404 for a hidden admin's id, the same as for an id that does not exist — there is no way to distinguish the two through this endpoint. # Get an article Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-an-article api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals/{portalSlug}/articles/{articleId} One article with its full Markdown content and publication status — drafts are readable here, unlike on the public help center. Requires being an administrator of the account or a member of the portal. # Get an automation rule Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-an-automation-rule api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/automation-rules/{automationRuleId} A single automation rule by id. **Administrator only — an agent token gets a 403.** # Get an inbox Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/get-an-inbox api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/inboxes/{inboxId} A single inbox by id. Answers 404 for an inbox outside the token's reach (an agent that is not a member of it), the same as for an id that does not exist. **No channel credential is ever included.** # List agents Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-agents api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/agents Every human and AI agent of the account, paginated. Add `search` to filter, `sort` to order. A hidden admin — the platform's own support user — never appears in this list. # List articles Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-articles api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals/{portalSlug}/articles The portal's articles — drafts and archived included, unlike the public help center — most recently updated first, paginated without a total count (`has_next`). Filters combine with AND. `query` is full-text: it matches word prefixes across title, description and content. `category_slug` filters by the category's slug (not id); `status` accepts exactly `draft`, `published` or `archived` — anything else is a 400 whose `details` names the parameter. Like every article endpoint, it requires being an administrator of the account or a member of the portal — the listing returns drafts with their full content, so it carries the same gate as reading one article. # List automation rules Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-automation-rules api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/automation-rules Every automation rule of the account, paginated. **Administrator only — an agent token gets a 403.** Add `search` to filter, `sort` to order; the default `execution_order` matches the order the rules actually run in for the same event. # List availability reasons Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-availability-reasons api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/availability-reasons Every availability reason of the account, paginated. Open to any member. A soft-deleted reason never appears here — its history on past agent availability changes still preserves the reference. # List campaign variables Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-campaign-variables api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/campaigns/variables The placeholder vocabulary of your account, for use in `template_params` and in the body of an e-mail or widget campaign. The set is account-specific, because your custom contact attributes extend it — list it rather than guessing a name, since a placeholder outside this catalogue is refused when the campaign is created. # List canned responses Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-canned-responses api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/canned-responses Every canned response on the account, paginated. Add `search` to filter, `sort` to order. # List categories Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-categories api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals/{portalSlug}/categories One portal's categories, paginated, in the order the public help center renders them (`position`, ascending). Use it to pick the `category_id` for an article — the category also decides the locale the article is created in and translated to. There is no total count in the meta: walk forward while `has_next` is `true`. # List conversation messages Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-conversation-messages api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/conversations/{conversationId}/messages The messages of one conversation, oldest first by default, paginated without a total count — walk forward while `meta.has_next` is `true`. **Attachments and `content_attributes` are not exposed in this version** — only the fields on the `Message` schema. A private note comes back like any other message, with `private: true`; filter it out client-side if you don't want it. A hidden admin's message reports `sender.id: null` with a masked name, and `sender` itself is `null` on an `activity` message (status/assignment changes and the like). # List conversations Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-conversations api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/conversations The account's conversations, newest activity first, paginated. Filters are a fixed allowlist and combine with AND — send only the ones you need. An invalid filter value is a 400 whose `details` array names each offending parameter and says what it accepts. The date filters compare full ISO 8601 timestamps, never calendar days: `*_after` is inclusive (`>=`), `*_before` exclusive (`<`), so chaining windows never double-counts a conversation. `per_page` is clamped, never rejected — above 100 you get 100, non-positive falls back to 25. The ordering is always `last_activity_at` descending; there is no `sort` parameter. # List inboxes Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-inboxes api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/inboxes The inboxes the token can see — every inbox of the account for an administrator, only its memberships for an agent — paginated. Add `search` to filter, `sort` to order. **No channel credential (access token, phone number id, API key, or similar) is ever included**, on any channel type. # List labels Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-labels api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/labels Every label defined on the account, paginated. Add `search` to filter, `sort` to order. This is the label catalog, not conversations tagged with one — see `label` on [list conversations](/api-reference/cloudchat/endpoints/list-conversations) for that. # List macros Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-macros api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/macros The account's `global` macros plus the caller's own `personal` macros, paginated. Add `search` to filter, `sort` to order. A personal macro belonging to someone else never appears. # List message templates Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-message-templates api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/inboxes/{inboxId}/message-templates The approved WhatsApp templates an inbox can send, which is the only set a campaign accepts in `message`. **The listing is paginated**, and a busy inbox holds more templates than a single page. Page through it, or narrow it with `search`, rather than concluding from the first page that a template is missing. **Match the name exactly.** Near-identical names that differ only by a suffix are common, and they expect different numbers of parameters; choosing by resemblance sends the wrong count and the send fails. The list is scoped to the inboxes assigned to you. An inbox that is not a WhatsApp inbox on the Meta Cloud API answers with an empty page rather than an error, so an empty result means "not this kind of inbox, or nothing approved" — never that your account has no templates. # List native attribute requirements Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-native-attribute-requirements api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/native-attribute-requirements Every native conversation attribute the account currently requires agents to fill before resolving a conversation, paginated. Open to any member. The full set of native attributes is fixed — `priority`, `assignee`, `tag`, `team` — so a key absent here is simply not required. # List portals Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-portals api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/portals Every help center site (portal) of the account, ordered by id — with the `slug` the other help center endpoints take in the path. Start here when you don't know the slug. Not paginated on purpose: an account holds a handful of portals, and consumers need the whole list to resolve a slug. # List team members Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-team-members api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/teams/{teamId}/members Every agent on the team, paginated. Open to any member. A hidden admin never appears here, even if internally on the team. # List teams Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/list-teams api-reference/specs/cloudchat/v1.json GET /v1/accounts/{accountId}/teams Every team of the account, paginated. Add `search` to filter, `sort` to order. Open to any member. # Preview campaign recipients Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/preview-campaign-recipients api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/campaigns/preview-recipients Resolve an audience and count it, without creating anything. Run this before creating a campaign: the `reachable` it returns is what the create endpoint expects in `confirm_recipients`, and any other value is refused with 409. Measure the channel the campaign will really use. The create endpoint takes no channel — it follows from the inbox, or from the e-mail sender — so previewing one channel and creating in another gives two different numbers. On `whatsapp` only contacts with a phone number count: a contact you can otherwise message through a Meta business-scoped id does not receive campaigns. On `email` the figure is an estimate rather than a promise, because the dispatcher still drops addresses that fail its format check or sit on your suppression list. # Set agent access Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/set-agent-access api-reference/specs/cloudchat/v1.json PUT /v1/accounts/{accountId}/agents/{agentId}/access Grant or revoke this agent's access to Claudia projects and Eddie workspaces. Administrator only. Each dimension — `claudia_projects` and `eddie_workspaces` — is optional and independently declarative: send it as the full set you want that dimension to hold, drawn from your own `manageable` ceiling (see getAgentAccess) — items you leave out of a dimension you send are revoked, items you add are granted; a dimension you omit from the request entirely is left untouched. You can only grant or revoke within your own ceiling: items the target already holds outside it are preserved, never touched by your request. Naming an item outside your ceiling is refused with 422 (`outside_ceiling`, field names the offending dimension). The target's CloudChat role must be `administrator` or `cx_engineer`, or the whole request is refused with 422 (`ineligible_role`, field `agent`). An all-empty request (revoking everything) is accepted regardless of the target's role — it is the cleanup path for an agent demoted after receiving access. A target that has no hub user yet is provisioned automatically by this operation when the grant is non-empty. A grant only takes effect at the target agent's next login — that is when the new claims are provisioned — so an agent already signed in keeps its old access until it logs in again. This operation never removes the agent from the CloudChat account itself. # Translate an article Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/translate-an-article api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/portals/{portalSlug}/articles/{articleId}/translate Overwrite the article's title, content and — when it has one — description with their machine translation into the article's own target locale — the locale of its category, or the portal default when it has no category. There is no request body. An article without a description is translated without one and is never refused for it, and the `slug` is never translated: it forms the public address of a published article, so translating it would break every link already pointing there. This does **not** create a new article. To publish a translation: create the target-locale copy (filed under a category of the target locale, linked through `associated_article_id`), then call this endpoint on the copy. Each call runs a language model server-side — it takes seconds and overwrites whatever title, content and description the article had, so don't loop or retry it blindly. A failed translation never applies partially: on any failure the article is left exactly as it was and the endpoint answers 500. A 200 always means the translation was applied. # Update a canned response Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-a-canned-response api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/canned-responses/{cannedResponseId} A partial update: send `short_code`, `content`, or both. The field you leave out keeps its current value. A payload carrying neither of them is a 400 — nothing to apply is treated as a mistake rather than a no-op success. # Update a category Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-a-category api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/portals/{portalSlug}/categories/{categoryId} Change a category's name, description, URL handle or translation link. Send only the fields you are changing; a body with none of them is a 400, and a field sent as `null` is cleared. This is how a naming mistake gets fixed after the fact — including the slug, which is exactly why the category is addressed here by numeric id and not by slug. `locale` is **not** writable. An article inherits the language of its category, so changing it would move the language of every article inside without touching one of them; move an article to a category of the other language with [update an article](/api-reference/cloudchat/endpoints/update-an-article) instead. `position` and `parent_category_id` are not writable here either — ordering and nesting stay in the dashboard. Requires being an administrator of the account or a member of the portal. # Update a macro Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-a-macro api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/macros/{macroId} Requires being the macro's author, or an administrator editing a `global` macro. Change one or more writable fields; what is not sent stays untouched. At least one of `name`, `visibility` or `actions` must be present — an empty payload is a 400, not a no-op. The same `action_name` allow-list from creation applies to `actions` here. # Update a portal Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-a-portal api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/portals/{portalSlug} Change a portal's name, the locales it may hold content in, or its logo. Send only the fields you are changing; a body with none of them is a 400. This is where a portal becomes usable in more than one language: `allowed_locales` is what a category's locale is checked against. Note that it **replaces** the stored list instead of adding to it — the field description spells out both refusals you can expect. Requires being an administrator of the account. # Update a team Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-a-team api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/teams/{teamId} Administrator only. Change one or more writable fields; what is not sent stays untouched. At least one writable field must be present — an empty payload is a 400, not a no-op. Refused with 403 on a `system_managed` team. # Update an agent Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-an-agent api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/agents/{agentId} Administrator only. Change one or more writable fields; what is not sent stays untouched. At least one writable field must be present — an empty payload is a 400, not a no-op. Refused with 403 on the platform-managed AI agent. `name` is the user's global display name, so changing it here changes it on every account the user belongs to. # Update an article Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-an-article api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/portals/{portalSlug}/articles/{articleId} Change one or more writable fields; what is not sent stays untouched. At least one writable field must be present — an empty payload is a 400, not a no-op. Publishing is this endpoint: send `status: published`. Taking an article down is `status: archived` (or back to `draft`). Requires being an administrator of the account or a member of the portal. # Update an availability reason Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/update-an-availability-reason api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/availability-reasons/{availabilityReasonId} Administrator only. Change one or both writable fields; what is not sent stays untouched. At least one writable field must be present — an empty payload is a 400, not a no-op. # Upload an image Source: https://docs.cloudhumans.com/api-reference/cloudchat/endpoints/upload-an-image api-reference/specs/cloudchat/v1.json POST /v1/accounts/{accountId}/uploads Store one image and get back the URL to embed in article Markdown (`![alt](file_url)`). Exactly one input mode per call: `external_url` — the server fetches the image from a public URL — or `attachment_base64` — the bytes travel in the request. Both share the same contract: PNG, JPEG or WEBP, up to 10 MB. The stored type is sniffed from the actual bytes, so a mislabeled file is still refused; SVG and GIF are rejected. This is the whole upload surface of the public API — it exists for help center images. Other file types, larger files and multipart uploads are not part of the v1 contract. # Cloud Chat API Source: https://docs.cloudhumans.com/api-reference/cloudchat/overview Base URL, authentication, the instance header, and the conventions every endpoint follows. The Cloud Chat API lets you read and manage your Cloud Chat data from your own systems. It is a curated surface: a set of endpoints we commit to and version, not a window onto everything the Cloud Chat app does internally. The endpoints available today are listed in the sidebar, and the set grows over time. Everything on this page applies to all of them. Each endpoint's own page covers what is specific to it. ```text theme={null} https://api.cloudhumans.com/cloudchat/v1 ``` ## Two headers, every call ```bash theme={null} curl https://api.cloudhumans.com/cloudchat/v1/accounts/1/canned-responses \ -H "Authorization: Bearer $ID_TOKEN" \ -H "cloudchat-instance: 1" ``` **`Authorization`** carries the `id_token` from [`POST /auth/v1/signin`](/api-reference/cloudchat/v1/auth/authentication/sign-in-and-get-a-token) — not the `access_token`, which does not carry the identity we authorize on. One token works across every Cloud Humans API, and expires after the `expires_in` seconds the sign-in response reports. **`cloudchat-instance`** says which Cloud Chat instance holds your data. Cloud Chat runs on several instances with independent databases, and your company lives on exactly one of them, so the value is fixed for you: set it once in your client's configuration and forget it. You are told it at onboarding, and it is also written into your own token — the [Find your credentials](/api-reference/cloudchat/credentials) page reads it out for you. Omit `cloudchat-instance` and the call is rejected before authentication, with a `400` that does not use the normal error envelope: ```json theme={null} { "error": "" } ``` Send the *wrong* instance and you get a `401` or a `404` instead: either that instance does not accept your token, or the account you asked for simply isn't in its database. You never reach someone else's data by guessing. ## Signing in is a loop, not a call `POST /auth/v1/signin` does not always answer with a token. Some accounts have a temporary password, and some have two-step verification turned on — for those, a `200` carries a **challenge** instead: no token, and a `session` to answer it with. So the shape to write is a loop, not a single request. Post credentials, then keep answering whatever comes back at [`POST /auth/v1/signin/challenge`](/api-reference/cloudchat/v1/auth/authentication/answer-a-sign-in-challenge) until a token appears. To just *get* a token now — challenge and all — the [Find your credentials](/api-reference/cloudchat/credentials) page runs the whole flow for you and asks only for the code from the text message. | The response contains | What it is | What you do | | - | - | - | | `id_token` | Done. | Use it as `Authorization: Bearer`. Stop. | | `challenge: "SMS_MFA"` | A code has just been texted to the phone on the account. | Ask for the code, post it back with the `session`. | | `challenge: "NEW_PASSWORD_REQUIRED"` | The password is temporary — first access, or it was reset. | Ask for a new password, post it back with the `session`. | | `error` | Nothing to answer. | Show the failure and start over. | **Branch on the body, not on the status.** Both outcomes are `200`, and both endpoints return the same two shapes — which is also why answering a challenge can hand you *another* one. A first-access user who also has two-step verification genuinely goes `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, and each step returns a **new** `session` that replaces the one you just used. ```bash theme={null} # 1. credentials in curl -sX POST https://api.cloudhumans.com/auth/v1/signin \ -H "Content-Type: application/json" \ -d '{"email":"you@company.com","password":"..."}' # {"challenge":"SMS_MFA","session":"AYABeF7x..."} # 2. the code the user just received, answered against that session curl -sX POST https://api.cloudhumans.com/auth/v1/signin/challenge \ -H "Content-Type: application/json" \ -d '{"challenge":"SMS_MFA","session":"AYABeF7x...","email":"you@company.com","code":"123456"}' # {"id_token":"eyJ...","access_token":"eyJ...","expires_in":3600,"token_type":"Bearer"} ``` ### If you are putting this on a screen Three things are worth knowing before the first user hits it. **Send the code as a string.** Codes can start with a zero, and `"012345"` parsed into a JSON number is a different code. A number is rejected with a `400` rather than coerced, precisely so this shows up on your first test instead of as one user in ten who "can't log in". **A wrong code and a dead session look identical.** Both come back `400 CodeMismatchException`, so the screen cannot honestly say which happened. Offer a retry, and next to it a way back to the beginning — retrying against an expired session never recovers, and a user with no way out is stuck on a screen that only ever says the code is wrong. **The session is not yours to keep.** It belongs to one sign-in attempt, so there is nothing to persist and nothing to resume — and it stops being accepted well before the token it leads to would have expired. When that happens the only move is `/auth/v1/signin` again, which sends a fresh code. Accounts configured for a challenge type other than these two — an authenticator app, for instance — cannot finish signing in through this API, and have to use the Cloud Chat interface to get a token. ## Accounts are explicit The account id travels in the path: `/v1/accounts/{accountId}/...`. It has to be an account your token grants membership on, and it has to live on the instance you named — the two are checked together. That is deliberately unlike the [Claudia API](/api-reference/claudia), where the account is derived from the token alone. Cloud Chat users routinely hold several accounts, so making the caller name one is clearer than picking for them. ## Errors Every response the API itself produces uses one envelope: ```json theme={null} { "error": { "code": "validation_failed", "message": "The request payload is invalid.", "details": [ { "field": "short_code", "code": "taken", "message": "has already been taken" } ] } } ``` | `code` | HTTP | When | | - | - | - | | `bad_request` | 400 | The request shape is wrong | | `unauthorized` | 401 | Missing, expired, or malformed Bearer token | | `forbidden` | 403 | The account is suspended | | `not_found` | 404 | The account or the resource is not there for you | | `validation_failed` | 422 | Values rejected by a validation rule; `details` names the fields | | `internal_error` | 500 | A failure on our side, already reported to our monitoring | Branch on `code`. It is stable. `message` is not: it is localized to your account's language — English, Spanish or Brazilian Portuguese, falling back to English — and the wording changes between releases. `details`, when present, stays in English regardless, because it describes fields rather than speaking to an end user. One `401` comes from outside that envelope: send a token that is invalid or expired and the request can be rejected before it reaches the API — like the `400` for a missing `cloudchat-instance`, and with the same bare shape: ```json theme={null} { "message": "invalid or expired token" } ``` Treat it the same way you treat the envelope's `unauthorized`: sign in again for a fresh token. A request with *no* token still reaches the API and gets the regular `unauthorized` envelope. ### 404 covers "not yours", not just "not there" One distinction is worth internalizing before you write error handling, because it is the opposite of what most APIs do. **Asking for an account you are not a member of returns `404`, not `403`** — identical to asking for an account that does not exist, same body, and the message stays in English in both so it cannot even leak the account's configured language. That is on purpose: nobody can walk the `accountId` space to learn which accounts exist. The cost is that a `404` doesn't tell you which of the two happened, so there is nothing to retry — check the `accountId`, the `cloudchat-instance` value, and that your user is actually a member. `403` is left with exactly one meaning: you *are* a member, and the account is **suspended**. Nothing on it can be read or written until that is resolved. ### 400 is about shape, 422 is about values A `400` always means the request itself was unreadable — invalid JSON, a missing or non-object payload wrapper, or a query parameter sent as an array or nested object (`?page[]=1`). A `422` means the request was understood and a value was refused. Retrying a `400` unchanged never helps; the fix is always in how the request is built. ## Rate limits Requests carrying a valid token are rate limited per user — calls you make through different machines or tools share one budget, and nobody else's traffic eats into yours. Requests without a token are limited per source IP instead. The limit is not something to hardcode — read it from the response, because every response carries the current state: | Header | Meaning | | - | - | | `ratelimit-limit` | Requests allowed in the current window | | `ratelimit-remaining` | How many you have left | | `ratelimit-reset` | Seconds until the window resets | | `Retry-After` | On a `429` only: seconds to wait before retrying | Over the limit you get `429` with a bare `message`, since the request never reached the API: ```json theme={null} { "message": "API rate limit exceeded" } ``` Handle it by waiting, not by retrying immediately: sleep for `Retry-After` seconds and try again. If you are running a batch, watch `ratelimit-remaining` as you go and pause when it approaches zero — that keeps you from ever seeing a `429`, and it adapts on its own if the limit changes. # Answer a sign-in challenge Source: https://docs.cloudhumans.com/api-reference/cloudchat/v1/auth/authentication/answer-a-sign-in-challenge /api-reference/specs/auth/v1.json post /v1/signin/challenge Finish a sign-in that came back with a `challenge` instead of a token. Send the `session` from that response together with the answer the challenge asks for — the SMS code, or the new password. Answer with the token in mind, not the step: this endpoint returns the **same body shape** as `/v1/signin`, so it can hand back another challenge rather than a token. A user on first access *with* two-step verification really does go `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, each step handing you a fresh `session`. Loop until `id_token` shows up. The `session` belongs to the sign-in attempt that produced it and is not built to be kept — there is nothing to persist and nothing to resume. Once it stops being accepted, call `/v1/signin` again, which sends a new code. # Sign in and get a token Source: https://docs.cloudhumans.com/api-reference/cloudchat/v1/auth/authentication/sign-in-and-get-a-token /api-reference/specs/auth/v1.json post /v1/signin Authenticate with email and password and receive the token used by every other API. One token covers Claudia, Cloud Chat and the services that follow — the account you see is derived from it, and the same token is valid against production and staging. **A `200` does not always carry a token.** Accounts with a temporary password, or with two-step verification turned on, come back with a *challenge* instead: no token yet, and a `session` to answer it with at `POST /v1/signin/challenge`. Branch on the body, not on the status — `id_token` present means done, `challenge` present means one more step. # Bucket layout Source: https://docs.cloudhumans.com/data-export/bucket-layout How files are organized under your bucket: contract version, tables, date partitions, and how to tell when your data is ready to read. Every key in the bucket follows the same shape — once you can read one path, you can read all of them. ```text theme={null} s3:///v1/datamart_claudia/claudia_conversation/snapshot_date=2026-08-06/0000_part_00.parquet │ │ │ │ │ │ │ │ │ └── file │ │ │ └── daily partition │ │ └── table │ └── source schema └── data contract version ``` | Path segment | What it is | | - | - | | `v1/` | Contract version. An incompatible layout change becomes `v2/`, and `v1/` stays in place | | `datamart_claudia/` | Source schema. There are two today, `datamart_claudia` and `datamart_cloudchat_saas` — the examples on this page use the first one, but treat the schema as something to discover, not a constant | | `/` | One folder per exported table. See the [table catalog](/data-export/catalog/overview) for every table delivered and what one row of it means | | `snapshot_date=/` | Hive-style partition, with `` in `YYYY-MM-DD` format. Athena, Spark, and DuckDB recognize it with no extra configuration | | `NNNN_part_MM.parquet` | `NNNN` is the UNLOAD slice, `MM` is the size-based split. Files are split at a maximum of 256 MB | | `_SUCCESS` | Written last. **Only exists once the partition is complete** | | `_STATE` | Internal control for our ingestion, at the table root. Present only in incremental tables. Safe to ignore | Beyond the tables, there's a manifest prefix: ```text theme={null} s3:///v1/_manifests/snapshot_date=/manifest.json ``` One manifest per snapshot date, recording what that day's delivery holds. Its fields are: | Field | What it tells you | | - | - | | `complete` | `false` means the day is partial. Fall back to the latest `snapshot_date` holding `_SUCCESS` | | `tables_delivered`, `tables_missing` | Which tables arrived and which did not. Every missing entry carries a `reason` | | `mode` | How the table is delivered: `full_refresh` or `incremental_append` | | `read` | How to read it: `latest_snapshot`, or `union_all_partitions` for an incremental table | | `reseed` | `true` on an incremental table rebuilt from scratch. Drop your copy and ingest it again instead of appending, which would count every row twice | ## How to tell if a partition is ready **Always check for `_SUCCESS` before processing the partition.** It's written last, after every Parquet file in that partition has been closed. A partition without `_SUCCESS` is either still in progress or failed. ```bash theme={null} aws s3 ls s3:///v1/datamart_claudia/
/snapshot_date=/_SUCCESS \ --profile cloudhumans-export ``` Empty output, exit code 1: not ready yet. # claudia_contents Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_contents What the agent could draw on when it answered. Join from `claudia_prompt_revision` to see which section produced a given reply. What the agent could draw on when it answered. Join from `claudia_prompt_revision` to see which section produced a given reply. one content section Rewritten daily 21 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_contents/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `content_id` instead. **Join key.** Unique id of the content section. Matches the `response_used_content_id` columns on [`claudia_prompt_revision`](/data-export/catalog/claudia/claudia_prompt_revision). Project whose knowledge base the section belongs to. Title of the section, as written by whoever maintains the base. The trigger text: what a customer question has to look like for this section to be retrieved. The answer the agent is expected to give when it uses this section. Subject tag the section is filed under. Whether reaching this section escalates the conversation. `true` marks sections that exist to hand the customer to a person. Topic grouping above the tag. `N1` for a section that answers, `N2` for one that escalates, `INTERACTIVE` for one that offers the customer options. Controlled flow this section starts, when it starts one. `NULL` for a plain answer. When the section was created. When the section was last edited. How the section entered the base: `csv-import`, `scrapper` for content pulled from a public page, or `auto-suggestion` for one proposed automatically and then accepted. Whether the section is still a draft and not yet usable by the agent. Whether the section is turned off. Disabled sections stay in the export for history. Whether the section was deleted. Filter both this and `disabled_flag` out to get the base as the agent sees it today. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. When the datamart last wrote this row. Use it to spot sections that changed since your previous load. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # claudia_conversation Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_conversation The conversation-level fact table, and only for conversations the ClaudIA agent took part in. Start here to count conversations, split N1 from N2, and read why a conversation was escalated. For everything in the helpdesk, whether or not an agent was involved, use [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). The conversation-level fact table, and only for conversations the ClaudIA agent took part in. Start here to count conversations, split N1 from N2, and read why a conversation was escalated. For everything in the helpdesk, whether or not an agent was involved, use [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). one ClaudIA conversation Rewritten daily 56 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_conversation/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `conversation_id` instead. Internal warehouse key for the project. Not stable between deliveries — use `project_id` or `project_name`. Internal warehouse key for the satisfaction answer. Not stable between deliveries — the score itself is on this row in `csat_value`. Identifier of the Cloud Chat conversation this one ran in. Joins to `conversation_uuid` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Conversations served by another helpdesk have no counterpart there, so this join does not cover every row — identify those through `helpdesk_id`. Identifier of the conversation in whichever helpdesk served it. When that helpdesk is Cloud Chat, it is the same number as `conversation_display_id` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations), kept as text here. When the conversation ran in an external helpdesk, it is that system's own identifier. Conversation start, in UTC. Conversation start, in the project's local timezone. Use this one when your report has to line up with the operation's working day. Ticket status as emitted by the platform. The value is a fully-qualified internal class name whose exact string is not part of the delivery contract: read the trailing segment (`Open`, `Resolved`, `Closed`) and prefer `is_conversation_open_flag` for filtering. Whether the conversation reached N2 at any point. Equivalent to `resolution_level_class = 'N2'`. Status of the conversation on the agent's side. Ticket status as reported by the channel connector, in the same fully-qualified form as `status_class`. The two disagree while a status change is still propagating. Whether the conversation was still open at the time of the delivery. Whether the conversation was handled by N2. Set independently of `is_n2_flag`, which only records that N2 was reached. Whether the escalation to N2 was forced rather than decided by the agent. Resolution level: `N1` when the agent closed the conversation on its own, `N2` when it was escalated to a person. This is the column to group by for a retention rate. When the conversation was closed. `NULL` while it is still open. When the conversation was marked resolved. Can precede `close_date`, since a resolved conversation stays open until it is closed. **Join key.** Unique id of the conversation. Joins to [`claudia_prompt_revision`](/data-export/catalog/claudia/claudia_prompt_revision) and, through `cloudchat_id`, to the Cloud Chat tables. Project the conversation belongs to. Environment the conversation ran in. Only `PRODUCTION` rows are delivered. How many times the agent produced an answer it judged low confidence. A high count on N2 conversations usually points at missing content. How many clarifying questions the agent asked before answering. How many times the agent retried before resolving. Ranges from 0 to 3. How many times the agent asked the customer whether their problem was solved. How many times the conversation was escalated for going unanswered. Intent the agent considered active when the conversation ended. Controlled flow active when the conversation ended. Joins to `typebot_id` on [`eddie_result_outcomes`](/data-export/catalog/claudia/eddie_result_outcomes). `NULL` when no flow was running. Run of the active flow. Use it with `active_flow_id` to identify a single pass through the flow. Whether the customer confirmed their problem was solved. `NULL` when they were never asked or never answered — this is not the same as `false`. Raw resolution reason as emitted by the platform. Wording changes without notice; use `resolution_reason_refined` for anything you report on. Subject tag assigned to the conversation. When the conversation was last modified. Name of the A/B configuration the conversation ran under. `NULL` when the project had no test running. Automated quality rating of the conversation, from 1 to 5. Independent of CSAT: it is produced by evaluating the transcript, not by asking the customer. Written justification for the `gencx_value` rating. Estimated customer frustration on a 0 to 100 scale, from the wording of the customer's messages. When the frustration score was last recalculated. `frustration_score_value` bucketed into a label: `CALM`, `IRRITATED`, `FRUSTRATED` or `HIGHLY_FRUSTRATED`. Written justification for the frustration classification. Whether the agent hit a rate limit while handling the conversation. Useful when explaining an unexpected escalation. Messages exchanged in the conversation, both sides included. Messages sent by the agent. Subtract from `messages_total_value` for the customer's share. Whether `gencx_value` fell in the range treated as a bad experience. Id of the project. Prefer it over `project_name`, which can be renamed. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Satisfaction score for the conversation, from 1 to 5. `NULL` when no survey was answered. Whether the score counts as positive. Whether the score counts as neutral. Whether the score counts as negative. Whether the answer is usable. Filter on this before averaging `csat_value`. Cloud Chat account the conversation belongs to. Cloud Chat instance the conversation was served from. Number shown to agents in Cloud Chat. Convenient when someone reports a conversation by the number they saw on screen; join on `conversation_id`. Resolution reason grouped into a readable label prefixed with `N1 - ` or `N2 - `. Prefer this column over `resolution_reason`. Older rows predate the refinement and still carry a raw English string, so filter on the prefix or fall back to `resolution_level_class`. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # claudia_conversation_feedback Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_conversation_feedback Your agents' quality reviews of the ClaudIA agent's work: whether the customer's problem was solved, whether the conversation should have been escalated, and whether the tag was right. A conversation nobody reviewed has no row here. Your agents' quality reviews of the ClaudIA agent's work: whether the customer's problem was solved, whether the conversation should have been escalated, and whether the tag was right. A conversation nobody reviewed has no row here. one review Rewritten daily 19 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_conversation_feedback/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `conversation_feedback_id` instead. **Join key.** Unique id of the review. **Join key.** Conversation being reviewed. Matches `cloudchat_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Project the reviewed conversation belongs to. Whether the reviewer judged the customer's problem solved. The reviewer's written comment. How serious the reviewer considered the problem, from 1 to 3, where 3 is the most severe. Whether the reviewer judged the conversation closed correctly. Whether the reviewer thought the conversation should have been escalated but was not. Rows with `true` are the missed-escalation cases. Whether the reviewer thought the agent could have resolved it without escalating. The mirror of `conversation_should_scaled_to_n2_flag`. Who left the review. When the review was left. When the review was last edited. Whether the reviewer agreed with the tag assigned to the conversation. The tag the reviewer says should have been used. Populated when `is_tag_correct_flag` is `false`. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. When the datamart last wrote this row. Use it to spot reviews added or edited since your previous load. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # claudia_csat Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_csat Satisfaction scores for ClaudIA conversations, with the raw answer kept alongside the normalized score. Satisfaction scores for ClaudIA conversations, with the raw answer kept alongside the normalized score. one CSAT answer Rewritten daily 13 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_csat/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `csat_id` instead. Project the conversation belongs to. **Join key.** Unique id of the survey answer. **Join key.** Conversation the answer belongs to. Matches `cloudchat_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Identifier of the conversation in whichever helpdesk served it, matching `helpdesk_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). The customer's answer exactly as collected. Survey wording differs per project, which is why the normalized `csat_value` exists. The answer normalized to a 1 to 5 score, comparable across projects. When the survey was sent. Free-text comment the customer left with the score. When the customer answered. The gap from `creation_date` tells you how long they took. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # claudia_messages Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_messages Every message on both sides of a conversation, with what triggered each agent reply. For the messages of every conversation in the workspace, whether or not the agent took part, use [`cloud_chat_messages`](/data-export/catalog/cloud-chat/cloud_chat_messages). Every message on both sides of a conversation, with what triggered each agent reply. For the messages of every conversation in the workspace, whether or not the agent took part, use [`cloud_chat_messages`](/data-export/catalog/cloud-chat/cloud_chat_messages). one message Daily delta on `etl_updated_at` 19 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_messages/etl_date=/ ``` Each partition holds only the rows the datamart ingested that day, so union every `etl_date=` partition to reconstruct the table. One of them is `etl_date=__initial__`, the first load, holding everything that preceded the earliest daily partition. Check for `_SUCCESS` before reading a partition, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `message_id` instead. Project the conversation belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Unique id of the message. **Join key.** Conversation the message belongs to. Matches `cloudchat_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Who sent the message: `USER` for the customer, `AGENT` for the agent or the person who took over. Message body as sent. Truncated at the column length for unusually long messages. When the message was sent. Order by this column within a conversation to reconstruct the transcript. Whether the message was an internal note rather than something the customer saw. Exclude these when you rebuild a transcript. How the message was produced: `STATIC` for a fixed text, `FLOW` for a step in a controlled flow, `OTHER` for everything else. The answer the agent built this message from. `NULL` on customer messages. The action the agent decided to take alongside sending the message. Whether the message was sent because the agent ran out of clarifying questions. The customer question this message answers, as the agent understood it. This is the interpreted query, not necessarily the customer's literal words. Which response strategy produced the message. The value is a fully-qualified internal class name and its exact string is not part of the delivery contract: read the trailing segment, for example `LowConfidenceResponse`, `ClarificationResponse` or `N2SectionResponse`. Medium of the message, in the same fully-qualified form as `originated_from_class`. The trailing segment is the useful part: `TextMessage`, `ImageMessage`, `AudioMessage` or `FileMessage`. When the datamart last wrote this row. This is the cursor for the daily delta: a partition holds the rows whose `etl_updated_at` fell on that day, so a message edited later reappears in a later partition. Partition column. The day whose rows this file holds, in `YYYY-MM-DD`. Not a date type, because the initial load carries the literal `__initial__` instead of a day. # claudia_prompt_revision Source: https://docs.cloudhumans.com/data-export/catalog/claudia/claudia_prompt_revision Handling a single conversation takes many calls to the language model, each with its own prompt. Every one of them is logged here, with the content sections retrieved and the action that followed. This is where you look to explain a reply. Handling a single conversation takes many calls to the language model, each with its own prompt. Every one of them is logged here, with the content sections retrieved and the action that followed. This is where you look to explain a reply. one model call Daily delta on `etl_updated_at` 33 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/claudia_prompt_revision/etl_date=/ ``` Each partition holds only the rows the datamart ingested that day, so union every `etl_date=` partition to reconstruct the table. One of them is `etl_date=__initial__`, the first load, holding everything that preceded the earliest daily partition. Check for `_SUCCESS` before reading a partition, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `prompt_revision_id` instead. **Join key.** Unique id of the decision. **Join key.** Conversation the decision belongs to. Matches `cloudchat_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Project the conversation belongs to. Environment the decision was made in. What the agent decided to do: `NO_ACTION` to keep talking, `FORWARD_N2` to escalate, `RESOLVE_TICKET` or `CLOSE_TICKET` to end the conversation. Whether the agent had exhausted its clarifying questions at this point. The customer question this decision responds to, as interpreted. Which response strategy the agent chose. The value is a fully-qualified internal class name and its exact string is not part of the delivery contract: read the trailing segment, for example `AcceptableConfidenceResponse` or `NoValidContentResponse`. When the decision was made. When the row was last modified. The answer the agent produced, before any formatting applied on the way out to the channel. Controlled flow active on the conversation when the decision was made. Run of that flow. Controlled flow this decision started or advanced. Run of the flow in `response_bot_flow_id`. Result of the flow run. Joins to `result_id` on [`eddie_result_outcomes`](/data-export/catalog/claudia/eddie_result_outcomes). The content section the answer was built from. Joins to `content_id` on [`claudia_contents`](/data-export/catalog/claudia/claudia_contents). This is the column that lets you attribute an answer to a piece of your base. Written reasoning recorded for the decision. First of the candidate sections retrieved for this question, in relevance order. The candidates are what was available to answer with, not necessarily what was used. Second candidate section retrieved. Third candidate section retrieved. Frustration estimated at this point in the conversation, from 0 to 100. `frustration_score` bucketed into a label. Written justification for the frustration classification. Error recorded while producing the answer. `NULL` on decisions that succeeded, which is the vast majority. First candidate section considered when the agent asked a clarifying question instead of answering. Second candidate section considered for the clarifying question. Third candidate section considered for the clarifying question. When the datamart last wrote this row. This is the cursor for the daily delta: a partition holds the rows whose `etl_updated_at` fell on that day. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Partition column. The day whose rows this file holds, in `YYYY-MM-DD`. Not a date type, because the initial load carries the literal `__initial__` instead of a day. # eddie_result_outcomes Source: https://docs.cloudhumans.com/data-export/catalog/claudia/eddie_result_outcomes Outcome of each controlled flow the agent triggered, joined to the resolution and CSAT of the conversation it belonged to. Outcome of each controlled flow the agent triggered, joined to the resolution and CSAT of the conversation it belonged to. one flow run Rewritten daily 41 ## Where it lands ```text theme={null} s3:///v1/datamart_claudia/eddie_result_outcomes/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns **Join key.** Unique id of the flow run. Matches `response_bot_result_id` on [`claudia_prompt_revision`](/data-export/catalog/claudia/claudia_prompt_revision). Project the flow belongs to. Id of the project. Prefer it over `project_name`, which can be renamed. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Id of the flow that ran. Matches `active_flow_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Name of the flow, as set by whoever built it. Instance the flow ran on. When the run started. Date of the run. Convenient for grouping without casting `result_created_at`. Whether the customer actually entered the flow. `false` means it was triggered but never began. Whether the customer reached the end of the flow. Whether the customer started the flow and stopped partway. Pair it with `last_group_title` to find the step people give up on. Id of the last step the customer reached. Title of the last step reached. This is the column to group by when looking for where a flow loses people. How the last step was determined: `entry` from the step the customer entered, `subflow` from a nested flow, `sem_edge` when the step had no outgoing path, `nao_encontrado` when it could not be resolved. Conversation the run belongs to. See `key_source` for which identifier this holds before joining on it. Which identifier `conversation_key` holds: `helpdesk` for the external helpdesk id, `session` for the flow session, `result_id` when neither was available. Check this column before you join. Whether a satisfaction survey was sent on the conversation. Whether the customer answered it. The score they gave, from 1 to 5. `NULL` when unanswered. Minutes to the first response, in wall-clock time. Minutes to the first response, counting only the operation's working hours. Use this one when comparing against an SLA. Minutes from start to resolution, in wall-clock time. Minutes from start to resolution, within working hours. Minutes the conversation was actively being handled. Minutes actively being handled, within working hours. Whether the conversation was handed to a person. Whether the conversation reached N2. `N1` or `N2`, matching the conversation's resolution level. Resolution reason of the conversation, in the same refined form as on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation). Whether the customer came back with the same subject after this run. A flow with a high completion rate and a high recontact rate is answering the wrong question. Who handled the conversation: `AI` or `HUMAN`. Whether an AI agent handled it. The boolean form of `agent_type`. Frustration estimated for the conversation, from 0 to 100. `frustration_score_value` bucketed into a label: `CALM`, `IRRITATED`, `FRUSTRATED` or `HIGHLY_FRUSTRATED`. Errors recorded during the run. Whether the run hit at least one error. `error_count > 0`. How the run ended, in one column: `completou` finished, `abandono` was abandoned partway, `escalou_n2` escalated, `n1_com_decisao` ended at N1 with a decision taken, `nao_iniciou` never started, `em_aberto` was still running, `sem_conversa` had no conversation attached. Labels are in Portuguese in the data. Identifier of the workspace the run belongs to. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_account_user_availability_histories Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_account_user_availability_histories Every time an agent went online, offline or busy, and the reason they selected when the workspace asks for one. Use it to reconstruct who was available at a given moment, which the daily [`cloud_chat_agent_metrics`](/data-export/catalog/cloud-chat/cloud_chat_agent_metrics) does not let you do. Every time an agent went online, offline or busy, and the reason they selected when the workspace asks for one. Use it to reconstruct who was available at a given moment, which the daily [`cloud_chat_agent_metrics`](/data-export/catalog/cloud-chat/cloud_chat_agent_metrics) does not let you do. one availability change Rewritten daily 18 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_account_user_availability_histories/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `account_user_availability_histories_id`. Internal warehouse key for the account. Use `account_id`. Internal warehouse key for the agent. Use `account_user_id`. Identifier of the workspace the change belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Unique id of the availability change. Instance the workspace is served from. Account the agent belongs to. **Join key.** Agent whose availability changed. Joins to `assigned_agent_id` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Name of that agent. Numeric form of `availability_description`. The state the agent moved to: `online`, `offline` or `busy`. Reason the agent picked, when the workspace asks for one. `NULL` when it does not. That reason as the agent sees it, such as a break or a meeting. Configured per workspace, so the wording is yours. When the agent moved to this state. When the record was last modified. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_agent_metrics Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_agent_metrics The headline support metrics for a single agent on a single day. Every duration is a sum, not an average — divide by the matching count column to get an average. The headline support metrics for a single agent on a single day. Every duration is a sum, not an average — divide by the matching count column to get an average. one agent, one day Rewritten daily 25 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_agent_metrics/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the row is identified by `agent_id` plus `metric_date`. Identifier of the workspace the agent belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** The agent. Matches `assignee_id` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Name of the agent. The day the metrics cover, in the operation's local calendar. Together with `agent_id` this is the grain of the table. Conversations created that day and assigned to this agent. Conversations this agent resolved that day. Not a subset of `tickets_created_assigned`: a conversation can be created one day and resolved the next. Total wall-clock minutes to resolution across `tickets_resolved_assigned`. Divide by that count for an average. The same total, counting only working hours. Total minutes conversations were actively handled. The same total, within working hours. Total minutes customers spent waiting on this agent. The same total, within working hours. Of the conversations resolved that day, how many the customer came back about within 24 hours. A resolution quality signal. Survey responses received on this agent's conversations. How many of those were positive. Divide by `csat_responses` for a satisfaction rate. Conversations where this agent sent the first reply. The denominator for `sum_first_reply_minutes`. Total minutes to first reply across those conversations. Conversations where this agent sent the first reply after an AI agent handed the conversation over. Total minutes to first reply on those conversations. The same total, within working hours. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_contacts Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_contacts The customer-side identity a conversation is attached to. Several columns hold your customers' personal data, marked as such below. The customer-side identity a conversation is attached to. Several columns hold your customers' personal data, marked as such below. one contact Rewritten daily 23 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_contacts/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `contact_id` instead. Identifier of the workspace the contact belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Unique id of the contact. Matches `contact_id` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). First name, or the whole name when the channel does not split it. Personal data of your customer. Email address. Personal data of your customer. Phone number, in the format the channel provided. Personal data of your customer. Account the contact belongs to. Instance the workspace is served from. Attributes the channel supplied about the contact, serialized as JSON. The keys depend on the channel. Your own identifier for the contact, when your systems set one. This is the column to join on if you want to match the contact to a record in your CRM. Custom attributes set in the workspace, serialized as JSON. When the contact was last active in any conversation. Numeric code for how the contact was created. `0` is a regular contact; other codes mark contacts created by the platform rather than by a real interaction. Middle name, when the channel supplies one. Personal data. Last name, when the channel supplies one. Personal data. Location as reported by the channel. Not verified. Country as reported by the channel. Whether the contact was blocked in the workspace. When the contact was first created. When the contact record was last modified. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_conversations Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_conversations The workspace-side conversation table: inbox, team, assignee, CSAT, durations and message counts on a single row. The widest table in the export. The workspace-side conversation table: inbox, team, assignee, CSAT, durations and message counts on a single row. The widest table in the export. one Cloud Chat conversation Rewritten daily 64 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_conversations/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `conversation_id` instead. Internal warehouse key for the inbox. Use `inbox_id`. Internal warehouse key for the user. Use `assignee_id`. Internal warehouse key for the account. Use `account_id`. Internal warehouse key for the account-user pairing. Internal warehouse key for the team. Use `team_id`. **Join key.** Unique id of the conversation. Every other Cloud Chat table joins back to this column. Account the conversation belongs to. Inbox that received the conversation. Team the conversation was routed to. `NULL` when it was never assigned to a team. Agent assigned when the snapshot was taken. This is the current assignee, not every agent who touched the conversation — for that, use [`cloud_chat_conversations_first_reply_metrics`](/data-export/catalog/cloud-chat/cloud_chat_conversations_first_reply_metrics). Number shown to agents in the workspace. Use `conversation_id` for joins: the display number is only unique within an account. Contact on the customer side. Joins to [`cloud_chat_contacts`](/data-export/catalog/cloud-chat/cloud_chat_contacts). The contact's identity within this specific inbox. Campaign that started the conversation. `NULL` for conversations the customer started. **Join key.** Stable identifier of the conversation across systems. Matches `cloudchat_id` on [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation) — this is how you connect the agent's view to the workspace's view. Identifier of the workspace the conversation belongs to. Each `customer_id` maps to exactly one `tenant_id`, but a tenant can have several. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Instance the workspace is served from. Conversation status as a numeric code stored as text: `0` open, `1` resolved, `2` pending, `3` snoozed. Higher codes appear as the product adds statuses, so prefer `resolved_flag` for resolution logic rather than comparing to `1`. Priority as a numeric code stored as text, ascending. Empty string and `NULL` both mean no priority was set — check for both. Human-readable inbox name. Email of the assigned agent. Personal data of your own staff; treat it accordingly. Name of the assigned agent. Name of the team the conversation was routed to. Name of the account. Tags applied, as a single delimited string, truncated at the column length. Use [`cloud_chat_conversations_tags`](/data-export/catalog/cloud-chat/cloud_chat_conversations_tags) whenever you need to group or filter by tag. Who handled the conversation: `AI` or `HUMAN`. Satisfaction score, from 1 to 5. `NULL` when no survey was answered. When the customer submitted the survey. Whether a survey was sent. Whether the customer answered. Divide by `csat_sent_flag` for a response rate. When a snoozed conversation is due to reopen. Only populated while `status_code` is `3`. Since when the conversation has been waiting on a reply. When the conversation was created. When the conversation record was last modified. When the customer was last active in the conversation. When any agent last opened the conversation. Last activity of any kind on the conversation. When the assigned agent last opened the conversation. The customer's first message. Differs from `created_at` on conversations the workspace started. First reply from the workspace side. `NULL` when nobody replied. Seconds from the customer's first message to the first reply. The same interval in minutes. Minutes to the first reply, counting only the operation's working hours. Use this one against an SLA — the wall-clock version penalizes overnight arrivals. Whether the workspace started the conversation rather than the customer. Whether an AI agent handled it. The boolean form of `agent_type`. Whether the conversation is resolved. Prefer this over comparing `status_code`. Whether the customer came back about the same subject shortly after this conversation was resolved. When the conversation was resolved. `NULL` while unresolved. Date of `resolved_at`. Convenient for grouping a resolution report by day. Minutes from creation to resolution, within working hours. Minutes from creation to resolution, in wall-clock time. Minutes the customer spent waiting on a reply, within working hours. Minutes the customer spent waiting, in wall-clock time. Minutes the conversation was actively handled, within working hours. Minutes actively handled, in wall-clock time. Messages sent by the workspace side. Messages received from the customer. The contact's custom fields, serialized as JSON. Parse on your side, or use the long-format columns on [`cloud_chat_conversations_custom_fields`](/data-export/catalog/cloud-chat/cloud_chat_conversations_custom_fields). The conversation's custom fields, serialized as JSON. When the datamart last wrote this row. Use it to spot conversations that changed since your previous load. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_conversations_custom_fields Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_conversations_custom_fields Custom field values in long format, one row per field. Pivot on your side — the set of fields differs per workspace, so the export cannot pivot for you. Custom field values in long format, one row per field. Pivot on your side — the set of fields differs per workspace, so the export cannot pivot for you. one conversation, one custom field Rewritten daily 19 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_conversations_custom_fields/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the row is identified by `conversation_id` plus `conversation_custom_field_name`. Internal warehouse key for the conversation. Use `conversation_id`. Account the conversation belongs to. Instance the workspace is served from. Identifier of the workspace the row belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Conversation the field belongs to. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Number shown to agents for that conversation. When the conversation was created. Name of the custom field. The set of names is defined per workspace, so this is the column you pivot on. Value of the field, always as text regardless of how it is typed in the workspace. Cast on your side. Id of the field definition in the workspace. When this value was last changed. Agent assigned to the conversation, carried here for convenience. Inbox of the conversation, carried here for convenience. Team of the conversation, carried here for convenience. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_conversations_first_reply_metrics Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_conversations_first_reply_metrics Response time per assignment, not per conversation. A conversation reassigned three times has three rows here. Response time per assignment, not per conversation. A conversation reassigned three times has three rows here. one agent assignment Rewritten daily 34 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_conversations_first_reply_metrics/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the row is identified by `conversation_id` plus `assignment_sequence`. Internal warehouse key for the conversation. Use `conversation_id`. Identifier of the workspace the row belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Instance the workspace is served from. **Join key.** Conversation that was assigned. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Not unique here: one row per assignment. Number shown to agents for that conversation. Account the conversation belongs to. Agent the conversation was assigned to in this row. Name of that agent. Whether the assignee was a person rather than an AI agent. Type of assignee: `AI` or `HUMAN`. Position of this assignment in the conversation, starting at 1. Filter to `1` for the first handler; the highest value is the last. Whether this assignment happened after an AI agent handed the conversation over. Use it to measure response time only on conversations a person inherited. When the assignment started. When it ended. `NULL` while the agent is still assigned, which is why `assignment_duration_seconds` can be `NULL` on open conversations. Whether the assignment was actually recorded against an agent. Seconds the agent held the conversation. The same duration in minutes. The agent's first reply during this assignment. Whether the agent replied at all during the assignment. `false` marks assignments that were reassigned before anyone answered. Seconds from assignment to first reply, in wall-clock time. The same interval in minutes. Seconds from assignment to first reply, within working hours. The same interval in minutes. This is the fairest measure of an agent's response time. Seconds from the moment the conversation became available to answer to the first reply, in wall-clock time. The same interval in minutes. The same interval in seconds, within working hours. The same interval in minutes, within working hours. Team the agent belonged to for this assignment. Name of that team. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_conversations_tags Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_conversations_tags One row per tag applied to a conversation. Use this instead of the `tags_list` column on `cloud_chat_conversations` when you need to group or filter by tag. One row per tag applied to a conversation. Use this instead of the `tags_list` column on `cloud_chat_conversations` when you need to group or filter by tag. one conversation, one tag Rewritten daily 17 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_conversations_tags/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the row is identified by `conversation_id` plus `tag_name`. Internal warehouse key for the conversation. Use `conversation_id`. Account the conversation belongs to. Instance the workspace is served from. Identifier of the workspace the row belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Conversation the tag was applied to. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Number shown to agents for that conversation. When the conversation was created. Carried here so you can filter by period without joining. The tag. Joins to `tag_name` on [`cloud_chat_tags`](/data-export/catalog/cloud-chat/cloud_chat_tags) when you also need the tag's description. When the tag was applied. The gap from `conversation_created_at` tells you whether it was tagged on arrival or at closing. Agent assigned to the conversation, carried here for convenience. Inbox of the conversation, carried here for convenience. Team of the conversation, carried here for convenience. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_csat Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_csat Workspace-side satisfaction responses, denormalized with the agent, team and inbox that handled the conversation. Workspace-side satisfaction responses, denormalized with the agent, team and inbox that handled the conversation. one survey response Rewritten daily 26 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_csat/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the response is identified by `conversation_id`. Identifier of the workspace the response belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Account the conversation belongs to. Instance the workspace is served from. **Join key.** Conversation the response is about. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Number shown to agents for that conversation. When the conversation was created. Internal warehouse key for the conversation. Use `conversation_id`. When the customer submitted the response. The score, from 1 to 5. Free-text comment the customer left. May contain personal data they typed themselves. Tags on the conversation at the time of the response, as a delimited string. Agent assigned when the survey was answered. Name of that agent. Inbox of the conversation. Name of that inbox. Team of the conversation. Name of that team. Contact who answered. Joins to [`cloud_chat_contacts`](/data-export/catalog/cloud-chat/cloud_chat_contacts). Name of that contact. Personal data of your customer. Phone number of that contact. Personal data of your customer. When the response record was last modified. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_messages Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_messages The text of every message in the workspace, in send order, with its channel status and whether it was private. The largest table in the export. For the conversation it belongs to, join [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations); for only the messages a ClaudIA agent took part in, use [`claudia_messages`](/data-export/catalog/claudia/claudia_messages). The text of every message in the workspace, in send order, with its channel status and whether it was private. The largest table in the export. For the conversation it belongs to, join [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations); for only the messages a ClaudIA agent took part in, use [`claudia_messages`](/data-export/catalog/claudia/claudia_messages). one message Daily delta on `etl_updated_at` 27 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_messages/etl_date=/ ``` Each partition holds only the rows the datamart ingested that day, so union every `etl_date=` partition to reconstruct the table. One of them is `etl_date=__initial__`, the first load, holding everything that preceded the earliest daily partition. Check for `_SUCCESS` before reading a partition, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `messages_id`. Internal warehouse key for the conversation. Use `conversation_id`. Internal warehouse key for the inbox. Use `inbox_id`. Internal warehouse key for the account. Use `account_id`. Identifier of the workspace the message belongs to. **Join key.** Unique id of the message within its instance. Instance the workspace is served from. Named `instance` here, and `instance_id` on the other tables. Account the message belongs to. Inbox the conversation arrived in. **Join key.** Conversation the message belongs to. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Id the message carries on the channel it came from. `NULL` when the channel sends none. Id of whoever sent it. An agent id when `sender_type` is `User`, a contact id when it is `Contact`. Which side sent the message: `Contact` for your customer, `User` for an agent. `NULL` on the automated entries the workspace writes itself. Numeric form of `message_type`. `incoming`, `outgoing`, `activity` or `template`. `activity` rows are the workspace's own notes, not something a person typed. Numeric form of `message_status`. Delivery state on the channel: `sent`, `delivered`, `read` or `failed`. It is the state on the day the row was delivered to you and is not restated later. What kind of content the message carries, such as plain text or an interactive reply. Macro that produced the message, when an agent sent it from one. `true` for an internal note, visible to agents only. Your customer never saw these. The message as it was sent. Free text written by your customer or your agent, so treat it as personal data. The same message after the workspace expands templates and variables. `NULL` when there was nothing to expand. When the message was sent. When the message record was last modified, for example when the channel confirmed delivery. Id of the tenant. `NULL` for rows with no tenant mapping. When the datamart first wrote this row. It decides the partition the message lands in and does not change afterwards, so a later edit to the message is not delivered again. Partition column. The day whose rows this file holds, in `YYYY-MM-DD`. Not a date type, because the initial load carries the literal `__initial__` instead of a day. # cloud_chat_outbound_events Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_outbound_events Proactive messages and campaign sends, with the delivery funnel timestamps and the provider error when a send failed. Proactive messages and campaign sends, with the delivery funnel timestamps and the provider error when a send failed. one outbound message Rewritten daily 26 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_outbound_events/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the send is identified by `conversation_id`. Identifier of the workspace the send belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. Internal warehouse key for the contact. Join [`cloud_chat_contacts`](/data-export/catalog/cloud-chat/cloud_chat_contacts) through the conversation instead. **Join key.** Conversation the message opened or belongs to. Joins to [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Number shown to agents for that conversation. Inbox the message was sent from. Name of that inbox. Campaign the send belongs to. `NULL` when `outbound_source_type` is `individual_proactive`. Name of the campaign. Name of the audience the campaign targeted. What produced the send: `campaign` for a bulk send, `individual_proactive` for one agent reaching out to one contact. When the send was created in the workspace. When the provider accepted the message. `NULL` when it never went out. When the message reached the contact's device. When the contact opened it. Always `NULL` on channels that do not report read receipts, which is not the same as unread. When the contact replied. Whether the message was sent. The funnel starts here. Whether it was delivered. Whether it was read. Whether the contact replied. The four flags give you the delivery funnel without comparing timestamps for `NULL`. Provider error code when the send failed. `NULL` on successful sends. The error code with a plain-language explanation, for example a rate limit, a policy block or an undeliverable number. `Other error` covers codes without a specific category. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_tags Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_tags The tag dimension. Join from `cloud_chat_conversations_tags` when you need a tag's description as well as its name. The tag dimension. Join from `cloud_chat_conversations_tags` when you need a tag's description as well as its name. one tag Rewritten daily 12 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_tags/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — join on `tag_id` or `tag_name`. Identifier of the workspace the tag belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** Unique id of the tag. Account the tag is defined in. Instance the workspace is served from. The tag as agents see it. Joins to `tag_name` on [`cloud_chat_conversations_tags`](/data-export/catalog/cloud-chat/cloud_chat_conversations_tags). What the tag is for, as written by whoever created it. Often the only place the intended meaning is recorded. When the tag was created. When the tag was last edited. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # cloud_chat_team_metrics Source: https://docs.cloudhumans.com/data-export/catalog/cloud-chat/cloud_chat_team_metrics The same headline support metrics as `cloud_chat_agent_metrics`, for a single team on a single day rather than a single agent. The same headline support metrics as `cloud_chat_agent_metrics`, for a single team on a single day rather than a single agent. one team, one day Rewritten daily 25 ## Where it lands ```text theme={null} s3:///v1/datamart_cloudchat_saas/cloud_chat_team_metrics/snapshot_date=/ ``` The partition delivered today holds the whole table. Read the latest `snapshot_date=` partition only, and check for `_SUCCESS` before you do, as described in [bucket layout](/data-export/bucket-layout#how-to-tell-if-a-partition-is-ready). ## Columns Internal warehouse key. Not stable between deliveries — the row is identified by `team_id` plus `metric_date`. Identifier of the workspace the team belongs to. Id of the tenant. `NULL` for rows with no tenant mapping. Name of the tenant. `NULL` for rows with no tenant mapping. **Join key.** The team. Matches `team_id` on [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations). Name of the team. The day the metrics cover. Together with `team_id` this is the grain of the table. Conversations created that day and assigned to this team. Conversations this team resolved that day. Total wall-clock minutes to resolution across `tickets_resolved_assigned`. Divide by that count for an average. The same total, counting only working hours. Total minutes conversations were actively handled. The same total, within working hours. Total minutes customers spent waiting on this team. The same total, within working hours. Of the conversations resolved that day, how many the customer came back about within 24 hours. Survey responses received on this team's conversations. How many of those were positive. Divide by `csat_responses` for a satisfaction rate. Conversations where this team sent the first reply. Total minutes to first reply across those conversations. Conversations where this team sent the first reply after an AI agent handed the conversation over. Total minutes to first reply on those conversations. The same total, within working hours. When the datamart last wrote this row. Partition column. Date of the delivery that wrote this file, in `YYYY-MM-DD`. # Table catalog Source: https://docs.cloudhumans.com/data-export/catalog/overview Every table delivered to your bucket, what one row means, and how it is refreshed. The tables delivered today belong to two business domains. **Cloud Chat** is the helpdesk itself: every conversation, contact and tag in the workspace, whether or not an AI agent took part. **Claudia** covers only the conversations the ClaudIA agent handled, seen from that agent's side. Each table arrives in one of two ways, because the two are ingested differently: either rewritten in full on every delivery, or as a daily delta. The mode is stated in the Delivery column below and on each table's page, and it decides which partitions you read. Every column of every table is defined on that table's own page, with the type the source warehouse declares. Parquet does not carry `varchar` lengths, so a length there tells you what the source declares, not something the file itself enforces. The schema name in each caption is the one you use in the S3 path and in your own external table definitions. ## Claudia tables `datamart_claudia` · 7 tables | Table | One row is | Delivery | | - | - | - | | [`claudia_conversation`](/data-export/catalog/claudia/claudia_conversation) | One conversation the ClaudIA agent handled, with its resolution level and reason | Rewritten daily | | [`claudia_messages`](/data-export/catalog/claudia/claudia_messages) | One message exchanged in a conversation, in send order | Daily delta on `etl_updated_at` | | [`claudia_contents`](/data-export/catalog/claudia/claudia_contents) | One knowledge-base content section available to the agent | Rewritten daily | | [`claudia_conversation_feedback`](/data-export/catalog/claudia/claudia_conversation_feedback) | One review of a ClaudIA conversation, left by one of your agents | Rewritten daily | | [`claudia_csat`](/data-export/catalog/claudia/claudia_csat) | One CSAT answer collected at the end of a conversation | Rewritten daily | | [`claudia_prompt_revision`](/data-export/catalog/claudia/claudia_prompt_revision) | One call to the language model while handling a conversation | Daily delta on `etl_updated_at` | | [`eddie_result_outcomes`](/data-export/catalog/claudia/eddie_result_outcomes) | One controlled-flow run, with how it ended | Rewritten daily | ## Cloud Chat tables `datamart_cloudchat_saas` · 12 tables | Table | One row is | Delivery | | - | - | - | | [`cloud_chat_conversations`](/data-export/catalog/cloud-chat/cloud_chat_conversations) | One Cloud Chat conversation, with assignment, status and reply timings | Rewritten daily | | [`cloud_chat_messages`](/data-export/catalog/cloud-chat/cloud_chat_messages) | One message in a conversation, on either side and including the automated ones | Daily delta on `etl_updated_at` | | [`cloud_chat_conversations_tags`](/data-export/catalog/cloud-chat/cloud_chat_conversations_tags) | One conversation, one tag | Rewritten daily | | [`cloud_chat_conversations_custom_fields`](/data-export/catalog/cloud-chat/cloud_chat_conversations_custom_fields) | One conversation, one custom field | Rewritten daily | | [`cloud_chat_conversations_first_reply_metrics`](/data-export/catalog/cloud-chat/cloud_chat_conversations_first_reply_metrics) | One agent assignment on a conversation, with its response timings | Rewritten daily | | [`cloud_chat_contacts`](/data-export/catalog/cloud-chat/cloud_chat_contacts) | One contact known to the workspace | Rewritten daily | | [`cloud_chat_csat`](/data-export/catalog/cloud-chat/cloud_chat_csat) | One CSAT survey response | Rewritten daily | | [`cloud_chat_tags`](/data-export/catalog/cloud-chat/cloud_chat_tags) | One tag defined in the workspace | Rewritten daily | | [`cloud_chat_outbound_events`](/data-export/catalog/cloud-chat/cloud_chat_outbound_events) | One outbound message sent to a contact | Rewritten daily | | [`cloud_chat_agent_metrics`](/data-export/catalog/cloud-chat/cloud_chat_agent_metrics) | One agent's headline support metrics for one day | Rewritten daily | | [`cloud_chat_team_metrics`](/data-export/catalog/cloud-chat/cloud_chat_team_metrics) | One team's headline support metrics for one day | Rewritten daily | | [`cloud_chat_account_user_availability_histories`](/data-export/catalog/cloud-chat/cloud_chat_account_user_availability_histories) | One availability change for an agent, with the reason they picked | Rewritten daily | **Rewritten daily** means the partition delivered today holds the whole table. Read the latest `snapshot_date=` partition and ignore the older ones. **Daily delta** means each partition holds only the rows the datamart ingested that day. Union every `etl_date=` partition to reconstruct the table, `etl_date=__initial__` included. See [bucket layout](/data-export/bucket-layout). Looking for a specific column? Search for its name with `⌘K`. The search covers every column definition in this catalog. # Downloading files Source: https://docs.cloudhumans.com/data-export/downloading Read the export with the AWS CLI, with boto3 in an orchestrator, or in place with a query engine. There are several ways to read the export. These are the ones we suggest, and which one fits depends on who is doing the reading. | Your case | Use | | - | - | | Exploring by hand, or a shell script | [The AWS CLI](#download-with-the-aws-cli) | | A pipeline or a scheduled job | [boto3](#download-with-boto3) | | Analysis, or you would rather not keep a copy of the data | [A query engine, with no download at all](#query-in-place-instead-of-downloading) | Credentials are the same question in all three: on your own machine you use the `cloudhumans-export` profile, and on AWS compute you use the role that compute already runs as, with no profile on the host. ## Download with the AWS CLI A quick inventory of what exists and how much space it takes: ```bash theme={null} aws s3 ls s3:///v1/ --recursive --human-readable --summarize \ --profile cloudhumans-export ``` The examples below partition by `snapshot_date=`. Tables delivered as a daily delta partition by `etl_date=` instead, and each table's page in the [table catalog](/data-export/catalog/overview) states which one it uses. Everything else about the commands is the same. One specific partition, the most common case day to day: ```bash theme={null} aws s3 sync \ s3:///v1/datamart_claudia/
/snapshot_date=/ \ ./data/
/snapshot_date=/ \ --profile cloudhumans-export ``` A whole table, incrementally. `sync` compares size and modification date, so running it again only pulls what changed: ```bash theme={null} aws s3 sync \ s3:///v1/datamart_claudia/
/ \ ./data/
/ \ --profile cloudhumans-export ``` On a table with more than 180 days of history this reaches archived files, which `sync` skips with a warning before exiting with code 2. See [storage classes](/data-export/storage-classes#restoring-archived-data). A date range, without downloading the entire history: ```bash theme={null} aws s3 sync \ s3:///v1/datamart_claudia/
/ \ ./data/
/ \ --exclude "*" \ --include "snapshot_date=2026-08-*" \ --profile cloudhumans-export ``` **Order matters:** `--exclude` and `--include` are evaluated left to right. `--exclude "*"` first, `--include` after. Reversed, you download everything. Check which storage class each file is in — useful for knowing, before you try, whether something will need a restore: ```bash theme={null} aws s3api list-objects-v2 \ --bucket \ --prefix v1/datamart_claudia/
/ \ --query 'Contents[].[StorageClass,Size,Key]' \ --output text \ --profile cloudhumans-export ``` ```text theme={null} STANDARD 991758 v1/datamart_claudia/
/snapshot_date=2026-08-05/0000_part_00.parquet STANDARD 0 v1/datamart_claudia/
/snapshot_date=2026-08-05/_SUCCESS STANDARD_IA 991758 v1/datamart_claudia/
/snapshot_date=2026-06-20/0000_part_00.parquet GLACIER_IR 991758 v1/datamart_claudia/
/snapshot_date=2026-04-10/0000_part_00.parquet GLACIER 77597818 v1/datamart_claudia/
/snapshot_date=2025-11-15/0000_part_01.parquet STANDARD 0 v1/datamart_claudia/
/snapshot_date=2025-11-15/_SUCCESS ``` Notice the `_SUCCESS` markers: they stay in `STANDARD` even in the oldest partition — [storage classes](/data-export/storage-classes#file-age-and-storage-class) explains why. ## Download with boto3 For use in an orchestrator (Airflow, Dagster, Prefect). Downloads a partition only after confirming the `_SUCCESS` marker, and handles the errors that actually happen instead of swallowing everything in a single `except Exception`. Before you process a day, read that snapshot date's manifest to confirm the delivery is complete, as described in [bucket layout](/data-export/bucket-layout). If the orchestrator runs on AWS compute, set `PROFILE = None`: the SDK then uses the role that compute already runs as, and no profile needs to exist on the host. See [local profile](/data-export/overview#local-profile). ```python download_partition.py lines expandable theme={null} """Download a partition from Cloud Humans Data Export.""" from __future__ import annotations from pathlib import Path import boto3 from boto3.s3.transfer import TransferConfig from botocore.exceptions import ClientError BUCKET = "" # None on AWS compute, so the SDK uses the role the environment already provides. PROFILE = "cloudhumans-export" # 20 threads instead of the default 10. Beyond that the gain flattens out and the risk of 503s increases. TRANSFER = TransferConfig(max_concurrency=20, multipart_threshold=16 * 1024 * 1024) class PartitionNotReady(Exception): """The partition exists but doesn't have the _SUCCESS marker yet.""" class ObjectArchived(Exception): """The object is in Glacier Flexible Retrieval and requires a restore before it can be read.""" def _client(): session = boto3.Session(profile_name=PROFILE) if PROFILE else boto3.Session() return session.client("s3") def partition_prefix(table: str, partition: str) -> str: """`partition` is the folder name, `snapshot_date=2026-08-06` or `etl_date=2026-08-06`.""" return f"v1/datamart_claudia/{table}/{partition}/" def is_ready(s3, prefix: str) -> bool: try: s3.head_object(Bucket=BUCKET, Key=f"{prefix}_SUCCESS") return True except ClientError as err: if err.response["Error"]["Code"] in ("404", "NoSuchKey"): return False raise def download_partition(table: str, partition: str, dest: Path) -> list[Path]: s3 = _client() prefix = partition_prefix(table, partition) if not is_ready(s3, prefix): raise PartitionNotReady(prefix) dest.mkdir(parents=True, exist_ok=True) written: list[Path] = [] for page in s3.get_paginator("list_objects_v2").paginate(Bucket=BUCKET, Prefix=prefix): for obj in page.get("Contents", []): key = obj["Key"] if key.endswith("/") or key.endswith("_SUCCESS"): continue target = dest / Path(key).name try: s3.download_file(BUCKET, key, str(target), Config=TRANSFER) except ClientError as err: code = err.response["Error"]["Code"] if code == "InvalidObjectState": raise ObjectArchived(key) from err raise written.append(target) return written if __name__ == "__main__": files = download_partition("
", "snapshot_date=", Path("./data")) print(f"{len(files)} files downloaded") ``` `InvalidObjectState` gets its own exception because it's the only failure in this list that is **not** a configuration problem — the data is there and intact, it only needs restoring first. See [storage classes](/data-export/storage-classes#restoring-archived-data) for how. ## Query in place instead of downloading The files are Parquet in Hive-style partitions, so a query engine can read them where they are. This is the shortest path for analysis and for anything you run once: no copy to keep in sync, and the engine reads only the partitions your `WHERE` clause touches. Run the engine in the bucket's region, `us-east-1`. Reading across regions is slower and the volumes here are large enough for it to show. **Spark** infers the partition columns from the folder names, so pointing it at the table root is enough. Needs the `hadoop-aws` connector: ```python theme={null} df = spark.read.parquet("s3a:///v1/datamart_claudia/
/") ``` **Athena** needs the schema up front, since an external table has no inference. Take the columns from that table's page in the [table catalog](/data-export/catalog/overview), or let a Glue crawler write the definition for you: ```sql theme={null} CREATE EXTERNAL TABLE
( -- one line per column, from the table's catalog page ) PARTITIONED BY (snapshot_date string) STORED AS PARQUET LOCATION 's3:///v1/datamart_claudia/
/'; MSCK REPAIR TABLE
; ``` Three things to know before you point an engine at the bucket: * **The markers do not get in the way.** `_SUCCESS`, `_STATE` and the manifests start with an underscore, which is exactly the convention Hive, Spark, Athena and Presto use for files to skip. They are not read as data. * **Declare the partition column of a daily delta table as a string.** Those tables partition by `etl_date=`, and one of the values is the literal `__initial__`, so a date type fails on it. That also rules out Athena partition projection over a date range for those two tables. * **Archived files are not readable in place.** Beyond 180 days the objects move to S3 Glacier Flexible Retrieval, and an engine reading them fails the same way a download does. Restore first, as described in [storage classes](/data-export/storage-classes#restoring-archived-data). # Overview and access Source: https://docs.cloudhumans.com/data-export/overview Data export gives you the raw data behind your ClaudIA and Cloud Chat operation. Ingest it into your own data lake to build analyses or integrate it with internal corporate systems. This guide walks you through extracting your data with the data export service, step by step. You'll start by setting up access to the S3 bucket that holds your operation's data. **S3** is the file storage service AWS provides. Think of it as a drive in the cloud, where your data is kept. We load your data every day. Delivery is D-1: today, you have access to everything up to yesterday. ## Requirements To use data export, you need: * **Data export enabled on your account.** Contact the support team to request it. * **Working knowledge of a few AWS services** — S3, IAM, and the AWS CLI. If you're not familiar with them, make sure your technical team is on hand to help. * **An AWS account.** **No AWS account?** You can still receive your data, through a different setup. Talk to your Cloud Humans support team to explore the alternatives. ## What you receive | Aspect | Details | | - | - | | **Where** | A dedicated S3 bucket for your company, in Cloud Humans' account, `us-east-1` region | | **What** | Parquet files, partitioned by date | | **When** | Daily delivery. The pipeline runs at 07:00 UTC | | **How you access it** | Directly, with your own AWS account's credentials. No credential from us, no portal, no SFTP | | **Retention** | Nothing is deleted on a timer. The full history stays available, though older files move to colder [S3 storage classes](/data-export/storage-classes) | You will receive **read-only** access, meaning you cannot write, overwrite, or delete anything. ## Granting access To get access to your bucket: **we don't issue credentials to you.** There's no Cloud Humans access key for you to store, rotate, or explain in an audit. Instead, you tell us the ARN of an IAM principal in **your own account**, and we authorize that ARN on the bucket. An **ARN** is an Amazon Resource Name, the unique identifier AWS gives every resource — a role, a user, a bucket. See [Amazon Resource Names (ARNs)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) in the AWS documentation for the full format. In practice: the credential is yours, the control is yours, and revoking access on your side is one line in your own policy. Nothing on our end needs to be undone. ### Send us your ARN We accept any of these formats: ```text theme={null} arn:aws:iam:::role/ arn:aws:iam:::user/ arn:aws:iam:::root ``` We recommend a **dedicated role**, not `:root` and not a user with a static key. A role lets you change who assumes it without talking to us and without generating a long-lived credential. ### What we apply on our side As soon as we have the ARN, this is the exact policy we attach to the bucket: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Sid": "DataExportClientRead", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam:::role/" }, "Action": ["s3:ListBucket", "s3:GetObject", "s3:RestoreObject"], "Resource": [ "arn:aws:s3:::", "arn:aws:s3:::/*" ] } ] } ``` `s3:RestoreObject` is there because files older than 180 days are archived; see [file age and storage class](/data-export/storage-classes#file-age-and-storage-class). ### What you need to configure on your side **Watch out! In cross-account access, the bucket policy is not enough on its own.** AWS documentation is explicit: *"When the principal and the resource are in separate AWS accounts, you must also use an identity-based policy to grant the principal access to the resource."* ([IAM User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html)) In short: we grant access on our end, but your role also needs explicit permission. If `AccessDenied` shows up on the first command, start here. Attach this to the role you gave us — or, if you use IAM Identity Center, to the permission set that provisions that role: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Sid": "CloudHumansDataExportRead", "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetObject", "s3:RestoreObject"], "Resource": [ "arn:aws:s3:::", "arn:aws:s3:::/*" ] } ] } ``` **Don't know your bucket name?** Ask the Cloud Humans support team — the bucket is in our account, so the name comes from us. ### Local profile The policies above say which ARN may read the bucket. The local profile is how your machine becomes that ARN when you run a command — without it, the AWS CLI acts as a different identity and you get `AccessDenied` even with both policies correct. If your ingestion runs on AWS compute, you can skip this section. Send us the ARN of the role that compute already runs as — an EC2 instance profile, an ECS task role, a Lambda or Glue execution role — and attach the identity-based policy to that role. The SDK picks the credentials up on its own, with nothing for you to store or rotate: `boto3.Session()`, no `profile_name`. Otherwise, which of the three setups below fits depends on who runs the command: * **A person at a terminal** — IAM Identity Center. Nothing long-lived stays on the machine. * **A scheduled ingestion job** — a role assumed from a base profile. IAM Identity Center cannot serve this: `aws sso login` needs a browser, and the session expires within hours. * **Static keys** — only when neither of the above is available to you. The three are mutually exclusive: set up one. This setup uses two blocks: the profile's `sso_session` is only a reference, and without the matching `[sso-session]` block, the CLI responds with `The specified sso-session does not exist`. ```ini ~/.aws/config theme={null} [profile cloudhumans-export] sso_session = cloudhumans-export sso_account_id = sso_role_name = region = us-east-1 output = json [sso-session cloudhumans-export] sso_start_url = https://.awsapps.com/start sso_region = ``` Before your first command, authenticate the session: ```bash theme={null} aws sso login --profile cloudhumans-export ``` This profile assumes a role, using your base profile for the underlying credentials: ```ini ~/.aws/config theme={null} [profile cloudhumans-export] role_arn = arn:aws:iam:::role/ source_profile = default region = us-east-1 ``` This setup uses long-lived static keys directly: ```ini ~/.aws/credentials theme={null} [cloudhumans-export] aws_access_key_id = aws_secret_access_key = ``` ```ini ~/.aws/config theme={null} [profile cloudhumans-export] region = us-east-1 ``` **Security:** the credentials in all three examples are **yours**, issued by your own account. Cloud Humans never asks for, receives, or stores a customer's key. If anyone asks you for yours on Cloud Humans' behalf, it isn't Cloud Humans. ## Smoke test A **smoke test** is a minimal command that confirms the access you just configured works. Before you pull 40 GB, spend three seconds confirming the door opens: ```bash theme={null} aws s3 ls s3:///v1/ --profile cloudhumans-export ``` Expected output: ```text theme={null} PRE _manifests/ PRE datamart_claudia/ PRE datamart_cloudchat_saas/ ``` One prefix per source schema, plus the manifests. For the tables inside each one, see the [table catalog](/data-export/catalog/overview). This command only needs `s3:ListBucket`. If it works and the download fails later, the problem is `s3:GetObject` in your policy — not the network, not the credential. That already rules out half the possible causes. # Performance and best practices Source: https://docs.cloudhumans.com/data-export/performance Tune AWS CLI concurrency for faster downloads, and the habits that keep your integration predictable. ## Tuning the AWS CLI Parquet files reach 256 MB, and a large table's partition passes 500 MB. The CLI default of 10 concurrent requests leaves bandwidth on the table. ```ini ~/.aws/config theme={null} [profile cloudhumans-export] region = us-east-1 s3 = max_concurrent_requests = 20 max_queue_size = 10000 multipart_threshold = 16MB multipart_chunksize = 16MB ``` Defaults, so you know what you're changing: | Parameter | Default | Worth changing? | | - | - | - | | `max_concurrent_requests` | 10 | Yes, it gives the best return. See the measurement below. | | `max_queue_size` | 1000 | Only for syncs with tens of thousands of files. | | `multipart_threshold` | 8 MB | Raising it to 16 MB reduces overhead on large Parquet files. | | `multipart_chunksize` | 8 MB | Keep it in sync with the threshold. | | `max_bandwidth` | no limit | Useful if the sync is choking the rest of your network. | We measured this by downloading a single 74 MB Parquet file from a `us-east-1` bucket to a machine outside AWS, varying only concurrency: | `max_concurrency` | Time | Throughput | | - | - | - | | 10 (default) | 5.9 s | 12.5 MB/s | | 20 | 4.8 s | 15.4 MB/s | | 30 | 3.9 s | 19.0 MB/s | In other words: roughly a 50% gain from tripling concurrency, in a scenario bound by AWS egress latency. Inside AWS, in the same region, the curve climbs further. Measure in your own environment — the number that matters is yours. **Performance tip:** the biggest win doesn't come from tuning — it comes from not downloading what you don't need. Filtering by `snapshot_date=` cuts volume by orders of magnitude; adjusting concurrency cuts it by tens of percent. Filter first. If you run on a high-network EC2 instance, the CRT client might be worth testing: ```ini ~/.aws/config theme={null} preferred_transfer_client = crt ``` In boto3, the equivalent is `TransferConfig`, covered in [Download with boto3](/data-export/downloading#download-with-boto3). And the reminder everyone needs exactly once: **run the download in the same region as the bucket** (`us-east-1`). Cross-region transfer is slower, and a job in `sa-east-1` pulling from `us-east-1` explains a lot of mysterious slowness. ## Best practices Three habits that make your life easier and your integration more predictable: * **Filter by partition instead of syncing the entire bucket** on every run. An incremental `sync` already does this naturally; `cp --recursive` doesn't, and will re-download everything, every time. * **Use `Bulk` as your default restore tier**, and `Expedited` only when there's real urgency. * **[Contact your Cloud Humans account team](/data-export/troubleshooting#support-and-contract-changes) before a large historical reprocessing** — restoring months of data or downloading the entire bucket. This isn't about asking permission, it's so we can help: above a few hundred objects, the efficient path is S3 Batch Operations, and that runs on our side. There's no volume limit and no quota on your access. The request exists only so we know when heavy load is coming. # Storage classes and restoring archived data Source: https://docs.cloudhumans.com/data-export/storage-classes How file age maps to S3 storage class, and how to restore files archived in Glacier Flexible Retrieval. ## File age and storage class As files age, they change storage class automatically. **Nothing is deleted.** What changes is how you read them — and only at the last step. | File age | Class | How you read it | | - | - | - | | 0 to 29 days | S3 Standard | Direct `GET` | | 30 to 89 days | S3 Standard-IA | Direct `GET`, same latency | | 90 to 179 days | S3 Glacier Instant Retrieval | Direct `GET`, same latency | | **180 days or more** | **S3 Glacier Flexible Retrieval** | **Requires `RestoreObject` first** | In other words: **nothing changes for you until day 179.** The only thing to watch in this whole guide is day 180. Two details that make your life easier: * **Small files are never archived.** The transition only applies to objects of 128 KB or more, so `manifest.json` and `_SUCCESS` stay permanently in S3 Standard — you can always list, browse, and find out what exists in the bucket without restoring anything, no matter how old the period is. * **Age is per file, not per partition.** A table with 2 years of history has the last 6 months directly readable and the rest archived, and that changes file by file, every day. Find out what's archived before you try to download it: ```bash theme={null} aws s3api list-objects-v2 \ --bucket \ --prefix v1/datamart_claudia/
/ \ --query 'Contents[?StorageClass==`GLACIER`].Key' \ --output text \ --profile cloudhumans-export ``` ## Restoring archived data If you try to download a file in S3 Glacier Flexible Retrieval without restoring it first, the API responds with `InvalidObjectState` (HTTP 403), and `aws s3 sync` skips the file with a warning and exits with code 2. It isn't a permission failure — it's the object telling you it's asleep. ### Request the restore ```bash theme={null} aws s3api restore-object \ --bucket \ --key v1/datamart_claudia/
/snapshot_date=/0000_part_00.parquet \ --restore-request '{"Days":7,"GlacierJobParameters":{"Tier":"Bulk"}}' \ --profile cloudhumans-export ``` `Days` sets how many days the restored copy stays available for download. Seven days is a comfortable value for a reprocessing job; choose whatever makes sense for you. Three details worth knowing before you automate this: * **`GlacierJobParameters` is optional.** `--restore-request '{"Days":7}'` works and uses the `Standard` tier. The CLI reference marks `Tier` as required, but that only applies *inside* the block: if you omit the whole block, AWS applies the default. * **Repeating the restore of an already-restored object doesn't error** and doesn't restart the queue. You can call it inside a retry without worrying about idempotency. * **Restoring an object that isn't archived errors out**, rather than no-op-ing: `InvalidObjectState: Restore is not allowed for the object's current storage class`. That applies to S3 Standard, S3 Standard-IA, and S3 Glacier Instant Retrieval. In other words, don't restore an entire partition blindly — filter by `StorageClass == GLACIER` first, as in the [restoring many files](/data-export/storage-classes#restoring-many-files) example. ### Choose the tier | Tier | Typical time | | - | - | | `Bulk` | 5 to 12 hours | | `Standard` | 3 to 5 hours | | `Expedited` | 1 to 5 minutes | **Use `Bulk` by default.** Data older than 180 days is almost never urgent, and historical reprocessing is easy to plan a day ahead. `Expedited` exists for the legitimate exception — an incident, an audit, a regulatory deadline. If you need it, use it without hesitation. Now, if fast restores become routine in your workflow, the problem isn't the tier — it's that slice of data being cold when it should be hot. [Contact your Cloud Humans account team](/data-export/troubleshooting#support-and-contract-changes): this gets resolved on our side, and that's a lot better than living with a restore step in every pipeline. ### Track progress ```bash theme={null} aws s3api head-object \ --bucket \ --key v1/datamart_claudia/
/snapshot_date=/0000_part_00.parquet \ --profile cloudhumans-export ``` In progress: ```json theme={null} { "Restore": "ongoing-request=\"true\"", "StorageClass": "GLACIER" } ``` Completed: ```json theme={null} { "Restore": "ongoing-request=\"false\", expiry-date=\"Thu, 13 Aug 2026 00:00:00 GMT\"", "StorageClass": "GLACIER" } ``` Notice that **`StorageClass` stays `GLACIER`**. That isn't a bug. The restore creates a temporary accessible copy; the archived object stays where it was. After the `expiry-date`, the copy disappears and the download fails again. If you need the data permanently hot, copy it somewhere else while the copy exists. To automate this instead of polling, S3 emits the `s3:ObjectRestore:Completed` event. If you want to receive it, [contact your Cloud Humans account team](/data-export/troubleshooting#support-and-contract-changes) — the notification is configured on the bucket, and that's ours. ### Restoring many files An entire partition, with one restore per object: ```bash theme={null} aws s3api list-objects-v2 \ --bucket \ --prefix v1/datamart_claudia/
/snapshot_date=/ \ --query 'Contents[?StorageClass==`GLACIER`].Key' \ --output text \ --profile cloudhumans-export \ | tr '\t' '\n' \ | while read -r key; do aws s3api restore-object \ --bucket \ --key "$key" \ --restore-request '{"Days":7,"GlacierJobParameters":{"Tier":"Bulk"}}' \ --profile cloudhumans-export done ``` This works fine for up to a few hundred objects. Above that, or if you need whole months of history, [contact your Cloud Humans account team](/data-export/troubleshooting#support-and-contract-changes) first — S3 Batch Operations handles this far more efficiently, and the operation runs on the bucket side. ### Download after the restore This is where the gotcha lives — the one behind most "I restored it and it still won't download" reports. **`cp` and `sync` behave differently**, and we measured both: | Command | Restored object | Needs `--force-glacier-transfer`? | | - | - | - | | `aws s3 cp ` | downloads normally | **no** | | `aws s3 sync ` | **silently skipped** | **yes** | The reason is mechanical: `sync` decides what to download from the prefix listing, and the listing only reports the storage class — it doesn't carry the `Restore` header. From `sync`'s point of view, a restored object is still "an object in Glacier". A `cp` of a specific key, on the other hand, runs a `HeadObject` first, sees the completed restore, and downloads without complaining. So after the restore, use the flag on `sync`: ```bash theme={null} aws s3 sync \ s3:///v1/datamart_claudia/
/snapshot_date=/ \ ./data/
/snapshot_date=/ \ --force-glacier-transfer \ --profile cloudhumans-export ``` If any object in the prefix still **isn't** restored, that `sync` fails on it with `InvalidObjectState` and exits with code 1 — the rest download normally. Restore the entire partition before syncing, or accept the exit code and handle it in your orchestrator. # Troubleshooting Source: https://docs.cloudhumans.com/data-export/troubleshooting Fixes for the errors you'll actually hit: AccessDenied, InvalidObjectState, expired SSO tokens, and more. ## `AccessDenied` (403) on the first `aws s3 ls` In this order, which is the real order of probability: 1. **Your identity-based policy is missing.** The most common cause, by far. Our bucket policy alone isn't enough for cross-account access. See [what you need to configure on your side](/data-export/overview#what-you-need-to-configure-on-your-side). 2. **The principal in use isn't the one you told us.** Confirm with `aws sts get-caller-identity --profile cloudhumans-export` and compare it against the ARN you sent. Assuming a role changes the effective ARN — what matters is what shows up here. 3. **Your organization's SCP is blocking cross-account S3**, or a VPC endpoint policy is restricting the bucket. Common in corporate environments with a landing zone. 4. **Wrong bucket name.** See the next entry. ## `AccessDenied` (403) where you expected `404` You're missing `s3:ListBucket` in your policy. Without that permission, S3 returns 403 instead of 404 for a nonexistent object, deliberately — so it doesn't reveal whether the key exists. A wrong key and a missing permission look identical. Add `s3:ListBucket` and the error becomes honest. ## `ls` works but the download gives `AccessDenied` You're missing `s3:GetObject`. `s3:ListBucket` applies to the bucket ARN; `s3:GetObject` applies to the `bucket/*` ARN. Check that your policy has **both** resources. ## `InvalidObjectState` on download ```text theme={null} An error occurred (InvalidObjectState) when calling the GetObject operation: The operation is not valid for the object's storage class ``` With `aws s3 cp`/`sync`, the same situation looks like this: ```text theme={null} warning: Skipping file s3:///... . Object is of storage class GLACIER. Unable to perform download operations on GLACIER objects. You must restore the object to be able to perform the operation. ``` The object is 180 days old or more and archived. This isn't a permissions problem, and it isn't the network. See [restoring archived data](/data-export/storage-classes#restoring-archived-data). ## `aws s3 sync` exits with code 2 and Glacier warnings Expected behavior when the prefix mixes hot and archived files: the CLI downloads what it can and skips the rest. Two ways out: * `--ignore-glacier-warnings` to proceed with what's available and exit with code 0. * Restore what's missing and run again with `--force-glacier-transfer`. ## Restored the file, but `sync` still skips it Expected, and not a bug on your end. Without `--force-glacier-transfer`, `sync` ignores objects in Glacier even once restored — and in this case **without emitting a warning**, because the listing it relies on has no way to know about the restore. Run it again with the flag. See [download after the restore](/data-export/storage-classes#download-after-the-restore) for the detail and the measurement. ## `InvalidObjectState: Restore is not allowed for the object's current storage class` The opposite of the previous case: you requested a restore for a file that **isn't** archived. Only objects in `GLACIER` accept a restore. Filter by class first, as in [restoring many files](/data-export/storage-classes#restoring-many-files). ## `NoSuchBucket` (404) Wrong name, or the wrong region in your profile. The bucket lives in `us-east-1`. Confirm with `aws s3api get-bucket-location --bucket --profile cloudhumans-export`. ## `The specified sso-session does not exist` The `[sso-session ]` block is missing from `~/.aws/config`, or its name doesn't match the profile's `sso_session`. There are two blocks, and the name needs to be identical in both. See [local profile](/data-export/overview#local-profile). ## `Error loading SSO Token: Token for does not exist` Configuration is correct, session isn't authenticated. `aws sso login --profile cloudhumans-export`. ## `ExpiredToken` / `The security token included in the request is expired` Your SSO session or temporary credential expired — usually in the middle of a 40 GB sync, because the universe has a sense of humor. Run `aws sso login --profile cloudhumans-export` and try again. `sync` is incremental, so it resumes where it left off without re-downloading what already came through. For long-running jobs in an orchestrator, prefer a role with a renewable session over a terminal's temporary credential. ## `SlowDown` (503) or `RequestTimeout` Concurrency is too high. Lower `max_concurrent_requests` and try again. S3 scales, but it likes a ramp. ## Parquet file downloaded with the right size but unreadable Check whether the partition had `_SUCCESS` at the time of the download. Without it, you may have caught a partition mid-write. Download again once the marker appears. ## Support and contract changes * **Layout and partition names** are stable within `v1/`. An incompatible change becomes `v2/`, with advance notice and a coexistence period between the two. You'll never wake up to a different `v1/`. * **New tables** can appear inside `v1/` without notice. Write your pipeline to iterate over what exists, not over a fixed list. * **Need help?** Contact your Cloud Humans account team. To speed things up, send along the exact command, the complete error message, and the output of `aws sts get-caller-identity`. With those three, most cases resolve on the first reply. # Introduction Source: https://docs.cloudhumans.com/index Cloud Humans developer documentation This portal documents the public APIs and data integrations for Cloud Humans products. Use the API Reference tab to browse endpoints, try requests in the playground, and copy ready-to-use code samples. Manage canned responses, the shortcuts your agents expand while replying. Endpoints for the Claudia platform. Access and download Parquet files from your dedicated S3 bucket. Every table delivered to your bucket, what one row means, and what each column holds. Beta Connect Claude, Cursor, or any MCP client to your Cloud Humans data — no code required. # ClaudIA API Source: https://docs.cloudhumans.com/integrations/claudia-api Build integrations with the ClaudIA API. Use the ClaudIA API to integrate with projects, agents, knowledge bases, and content. Browse the available endpoints and open them in the API playground. # Cloud Chat API Source: https://docs.cloudhumans.com/integrations/cloud-chat-api Build integrations with the Cloud Chat API. Use the Cloud Chat API to manage resources and workflows from your application. Read the getting-started guides, configure authentication, and browse the available endpoints. # MCP Server Source: https://docs.cloudhumans.com/mcp/overview Connect Claude, Cursor, or any MCP client to your Cloud Humans data — no API keys, no glue code. The MCP server is in **beta**. It is safe to use — it goes through the same permissions as everything else — but tools may change between releases as we refine the surface. The Cloud Humans MCP server exposes the same curated surface as the [public APIs](/api-reference/cloudchat/overview) as tools an AI assistant can call. Connect it once and your assistant can work with your Cloud Humans data in plain language — everything in the [API reference](/api-reference/cloudchat/overview) is available as a tool, and the set grows as the API does. ```text theme={null} https://mcp.cloudhumans.com/openapi/cloudhumans/mcp ``` It speaks the standard [Model Context Protocol](https://modelcontextprotocol.io) over Streamable HTTP, so anything that can act as an MCP client can use it: Claude (web and desktop), Claude Code, Cursor, or your own agent. ## Connecting There are no API keys to create. The server authenticates with **OAuth**: the first time your client connects, a browser window opens and you sign in with your regular Cloud Humans credentials — the same email and password you use in the app. Everything the assistant can see and do is bounded by what *your user* can see and do. Go to **Settings → Connectors → Add custom connector** and paste the server URL: ```text theme={null} https://mcp.cloudhumans.com/openapi/cloudhumans/mcp ``` Claude opens the sign-in page. After you approve, the tools appear in every new conversation. ```bash theme={null} claude mcp add --transport http cloudhumans https://mcp.cloudhumans.com/openapi/cloudhumans/mcp ``` Then run `/mcp` inside Claude Code to trigger the sign-in flow. Add the server to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json theme={null} { "mcpServers": { "cloudhumans": { "url": "https://mcp.cloudhumans.com/openapi/cloudhumans/mcp" } } } ``` Cursor detects the OAuth requirement and walks you through the login. Any client that supports **Streamable HTTP** transport and **OAuth 2.1 with dynamic client registration** works out of the box. Point it at the URL above; the server advertises its authorization endpoints and the client handles the rest. ## Accounts and projects One login covers the whole platform, and the assistant can only see and do what your user can. If your login has access to more than one account or project, just name the one you mean in conversation ("on the Acme account…") — or ask the assistant which ones you have, and it will list them. ## Good to know * If a tool reports something missing that you know exists, the assistant probably targeted the wrong account or project — say which one you mean and try again. * Sessions expire like any login. If tools start failing after a long idle period, reconnect — most clients re-run the sign-in automatically. ## MCP or REST? Same data, same permissions, two doors: * **MCP** when a *person with an assistant* is doing the work: exploring, reviewing, making judgment calls. Zero code. * **[REST API](/api-reference/cloudchat/overview)** when a *system* is doing the work: scheduled syncs, integrations, anything that runs unattended and needs deterministic behavior. They compose: teams typically prototype a workflow over MCP in conversation, then harden the parts worth automating into REST calls.