Skip to content

Agent API

Build, configure, and inspect your Anychat agents programmatically.


The Agent API is the HTTP interface for managing agents on the Anychat platform — the Definition sections (identity, personality, guardrails, objectives, playbooks, message templates) and capability configuration exposed as REST resources you can drive from your own code or from the anychat-cli.

The Agent API is the management plane: creating agents, editing their behavior, and browsing their conversation history. For the runtime API that delivers messages to and from your agents, see the Messaging API.

Base URL

https://api.anychat.ai/v1

All paths in this document are relative to that root.

Authentication

Attach a bearer token in the Authorization header on every request:

Authorization: Bearer <token>

Two kinds of bearer token work:

  • API keys — strings starting sk-any-…, issued from the console's Workspace · API keys page or via the API keys endpoints below. A key belongs to one org and carries the scopes its issuer chose when minting it.
  • User session tokens — the ID token of a signed-in console user. Scopes are derived from the user's role in the org.

Treat either as a secret.

An optional X-Anychat-Agent header names an automated agent acting on the caller's behalf, for example mcp (the @anychat-ai/mcp server) or copilot (the console's Anychat Copilot). The value is a short token matching [a-z0-9._-]{1,32}; anything else is ignored. It is recorded on the API's access log for attribution and has no effect on authentication or authorization.

Authorization model

Every management endpoint is scoped to one org. A token can only manage the org it was issued for; requests that name a different org in the URL receive a 404 (the existence of foreign orgs is not revealed).

Within your org, every endpoint enforces two orthogonal checks — a role floor and a scope. Both are listed per route in the tables below.

Role floors gate who the calling user is within the org:

  • viewer — read endpoints (GET).
  • editor — write endpoints (POST, PUT, PATCH, DELETE, import, apply).
  • admin — org administration (API keys, webhooks, org settings).

Scopes gate what a token can do:

Scope Covers
agents:read / agents:write Agents and their configuration sub-resources, including lint and flow generation
people:read / people:write / people:export The per-agent People CRM, including outreach sends to a Person; people:export gates bulk export and is implied by people:write
knowledge:read Knowledge documents
messages:read Messages, conversations, users, logs, turn records, and the activity feed
catalogs:read The system catalogs
templates:apply Importing catalog templates onto an agent
webhooks:manage Org webhooks
org:admin Org settings and API-key management

Scope rules:

  • A write scope implies its read scope (agents:writeagents:read, people:writepeople:read) — grant only the broadest scope you intend to use.
  • catalogs:read is granted to every token by default.
  • User session tokens hold the scope set implied by the user's role: viewer holds the read scopes; editor adds the write scopes and templates:apply; admin and owner add webhooks:manage and org:admin.
  • A request whose token lacks the route's scope receives 403 (auth.scope_missing).

Token introspection

GET /me describes the token making the call: who it belongs to, the org it is bound to and the caller's role there, and the effective scope set. It is the first call an SDK, the @anychat-ai/mcp server, or anychat whoami makes to resolve the org for every later request. There is no role floor and no scope requirement; any authenticated token can introspect itself.

Method Path Role Scope
GET /me
interface Me {
  caller: { kind: 'user'; id: string };   // user id, or the API key's id for sk-any-… tokens
  org: {
    id: string;
    name?: string;          // omitted if the org lookup fails
    role: 'viewer' | 'editor' | 'admin' | 'owner';
  };
  isSysop: boolean;
  scopes: string[];         // effective scope set for this token
  rateLimit: null;          // reserved; always null today
}

For an API key, org.role is the role implied by the key's scopes and scopes is the key's expanded scope set (write scopes expanded to include their read scopes). For a user session token, scopes is the set the user's role implies.

{
  "caller": { "kind": "user", "id": "68a1f0c2e4b0a1d2c3e4f567" },
  "org": { "id": "acme", "name": "Acme Health", "role": "editor" },
  "isSysop": false,
  "scopes": ["agents:write", "agents:read", "people:write", "people:read", "people:export", "knowledge:read", "messages:read", "catalogs:read", "templates:apply"],
  "rateLimit": null
}

Identifiers

Every resource carries a stable string id. Several flavors show up in this API:

Kind Format Example
Org id URL-safe slug acme
Agent id URL-safe slug, minted at create time, immutable agent-acme-bot-pj7r2qx4
Catalog template / tool id Qualified @anychat/... id @anychat/personality-template.professional, @anychat/tool.custom-http-webhook
Per-agent skill / objective / playbook / template id Short slug meeting-scheduling, qualify-lead, sales-discovery

Qualified ids contain a / and must be URL-encoded when they appear as a path parameter. For example, @anychat/tool.custom-http-webhook becomes %40anychat%2Ftool.custom-http-webhook.

Responses and errors

  • List endpoints return an envelope with a data array:

    { "data": [ ... ] }
    
    Long lists are paginated with an opaque nextPageToken; pass it back as ?pageToken=... to fetch the next page.

  • Single-item endpoints return the object directly, with no envelope.

  • Errors are JSON with a numeric statusCode and a human-readable message:

    { "statusCode": 400, "message": "`text` (string) is required" }
    

  • Newer endpoints (noted in their sections below) return problem details instead: an application/problem+json body with the HTTP status, a stable machine-readable code such as request.invalid or resource.not_found, a human-readable title, an optional detail, and, for validation failures, an errors array whose entries name the offending field path:

    {
      "status": 422,
      "code": "request.invalid",
      "title": "Validation failed",
      "errors": [{ "field": "fields[0].key", "message": "must match ^[a-zA-Z][a-zA-Z0-9_]*$" }]
    }
    
    Authorization failures on every endpoint use auth.forbidden (role floor not met) and auth.scope_missing (token lacks the scope).

  • A successful DELETE returns 204 No Content unless its section says otherwise. All other writes return the updated resource.


Resources

The Agent API mirrors the vocabulary of The Agent Definition and Capabilities. The following sections cover each resource, the shape of its data, and the endpoints that read and write it. (Wire field names such as toolBindings and customTools predate the capability vocabulary and are stable — they're the API contract, not the operator model.)

Most resources follow the same pattern: a GET/PUT pair on the collection, plus an import endpoint that copies a system template onto the agent (stamping derivedFrom so the console can later show whether your copy has diverged). Playbooks add per-item endpoints and a _setDefault action on top, to uphold the one-default / one-outbound invariants on every write.

Agent

The top-level resource. Every other resource in the API is nested beneath an agent. The Agent joins a name + an identity superset (used to seed channel bindings) with the agent's operational configuration; the operational pieces hang off their own sub-resource URLs.

interface Agent {
  id: string;            // immutable, minted at create time
  orgId: string;
  name: string;
  description?: string;
  identity: AgentIdentity;   // see Identity below
  framework: 'generic' | 'v2';  // runtime the agent runs on; default 'generic'
  createdAt: string;     // ISO-8601
  updatedAt: string;     // ISO-8601
}
Method Path Role Scope
GET /orgs/{org}/agents viewer agents:read
POST /orgs/{org}/agents editor agents:write
GET /orgs/{org}/agents/{agent} viewer agents:read
PATCH /orgs/{org}/agents/{agent} editor agents:write
DELETE /orgs/{org}/agents/{agent} editor agents:write

Create body:

{ "name": "Acme Bot", "id": "acme-bot", "identity": { "websiteUrl": "https://acme.com" } }
id, identity, and framework are optional; if id is omitted, Anychat mints a slug from name. Once set, id is immutable.

PATCH accepts any of { "name", "description", "identity", "framework" }.

Framework (runtime)

framework names the runtime the agent runs on: generic (the runtime described in How a turn is composed) or v2 (the Framework v2 preview). Both read the same Definition; they differ in how a turn is composed (capabilities per playbook via uses, deterministic paths via flow, output-check policies, and a turn record per reply).

  • Default is generic. Omit the field to keep today's behavior.
  • v2 requires the org feature framework-v2 (a preview entitlement, sysop-set). Creating an agent with framework: "v2", or switching one to it, without the feature returns 403 (agent.framework.not_entitled). generic is always allowed.
  • PATCH { "framework": "v2" | "generic" } switches the runtime. It is reversible and takes effect on the agent's next turn. Each runtime keeps its own conversation state, so a conversation in progress resumes in the new runtime's default playbook with the same transcript; switching back restores the previous runtime's state untouched. Run lint first — it reports the v1-only constructs v2 will ignore (v2.ignored.*).
  • Unknown values are rejected with 422 (agent.framework.invalid).

Framework errors are problem details:

Status code When
403 agent.framework.not_entitled framework: "v2" on create or PATCH and the org does not hold the framework-v2 feature
422 agent.framework.invalid framework is not one of generic, v2 (errors[].field = framework)
# Try an existing agent on the v2 preview runtime (reversible)
curl -X PATCH https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4 \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"framework": "v2"}'

Example:

curl https://api.anychat.ai/v1/orgs/acme/agents \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Sales Bot"}'

Identity

The agent's name, branding, and brand contact details — the single source of truth for contact info across the platform (there is no separate "escalation contacts" block). These values also seed new channel bindings: when you bind a channel, Anychat pre-fills the binding's identity from here and prompts only for whatever that channel still needs. After a binding exists, its identity is its own — edits here don't auto-propagate to existing bindings.

interface AgentIdentity {
  smallLogoUrl?: string;
  largeBannerUrl?: string;
  brandColor?: string;            // hex, e.g. #1a73e8
  contactEmail?: string;
  contactPhone?: string;
  websiteUrl?: string;
  privacyPolicyUrl?: string;
  termsOfServiceUrl?: string;
  contactAvailability?: string;  // shown verbatim on escalation, e.g. "Mon–Fri 9–5 PT"
  identityPreamble?: string;     // short agent-level prompt prelude (see below)
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/identity viewer agents:read
PUT /orgs/{org}/agents/{agent}/identity editor agents:write
PATCH /orgs/{org}/agents/{agent}/identity editor agents:write

PUT replaces the identity; PATCH updates only the fields you send.

identityPreamble is the short, agent-level "who's talking" line composed into every system prompt regardless of which Playbook is active (e.g. "You are Acme Corp's sales development agent."). The operational "how the agent behaves" content lives in the agent's Default Playbook (see Playbooks).

Personality

The voice and tone the agent uses, stored as plain text. See Personality in the framework overview.

interface AgentPersonality {
  description?: string;  // human-readable summary
  text: string;          // the prompt fragment the agent uses
  derivedFrom?: { templateId: string; version: string };
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/personality viewer agents:read
GET /orgs/{org}/agents/{agent}/personality/diff viewer agents:read
PUT /orgs/{org}/agents/{agent}/personality editor agents:write
POST /orgs/{org}/agents/{agent}/personality/import editor templates:apply

PUT replaces the personality wholesale (text is required).

Import copies a system Personality Template onto the agent:

{ "templateId": "@anychat/personality-template.professional" }
The imported personality records derivedFrom pointing at the template; edit freely afterward.

diff reports whether the agent's personality has diverged from the template it was imported from, per field:

{ "current": { ... }, "baseline": { ... }, "divergence": { "description": false, "text": true } }
current / baseline / divergence are all null when no personality is set; baseline is null (and every field counts as diverged) when the personality wasn't imported from a template.

Objectives

A ranked list of SMART-style goals the agent works toward. Lower priority wins when objectives compete on a turn.

interface AgentObjective {
  id: string;            // short slug, unique within the agent
  name: string;
  description?: string;  // human-readable
  body: string;          // the prompt fragment the agent uses
  priority: number;      // lower wins ties
  derivedFrom?: { templateId: string; version: string };
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/objectives viewer agents:read
PUT /orgs/{org}/agents/{agent}/objectives editor agents:write
POST /orgs/{org}/agents/{agent}/objectives editor agents:write
POST /orgs/{org}/agents/{agent}/objectives/_reorder editor agents:write
GET /orgs/{org}/agents/{agent}/objectives/{objectiveId} viewer agents:read
PATCH /orgs/{org}/agents/{agent}/objectives/{objectiveId} editor agents:write
PUT /orgs/{org}/agents/{agent}/objectives/{objectiveId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/objectives/{objectiveId} editor agents:write
POST /orgs/{org}/agents/{agent}/objectives/import editor templates:apply
  • PUT (collection) replaces the full list: { "objectives": [ ... ] }.
  • POST (collection) appends one objective.
  • _reorder sets priority by position; body is the ordered id list:
    { "order": ["qualify-lead", "book-call", "answer-product-questions"] }
    
    Every id must already exist on the agent; any omitted objectives keep their relative order after the listed ones.
  • Item-level PATCH/PUT/DELETE edit a single objective by id.
  • Import appends a single objective derived from a system Objective Template:
    { "templateId": "@anychat/objective-template.qualify-lead", "priority": 2 }
    
    priority defaults to end of list + 1. Re-importing the same template replaces the existing objective in place.

Skills (retired resource)

Retired 2026-08-09 by the skills→playbooks collapse: instruction blocks now ride their Playbook as procedures[] (see Playbooks), edited via the playbook resource and carried by Export / Import inside each playbook. The endpoints were removed in the phase-5 cleanup; requests to /agents/{agent}/skills* now return 404.

interface AgentSkill {
  id: string;                  // short slug
  name: string;
  description?: string;
  body: string;
  enabled: boolean;
  requiredTools?: string[];    // tool names the skill needs at runtime
  derivedFrom?: { templateId: string; version: string };
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/skills viewer agents:read
POST /orgs/{org}/agents/{agent}/skills editor agents:write
GET /orgs/{org}/agents/{agent}/skills/{skillId} viewer agents:read
PATCH /orgs/{org}/agents/{agent}/skills/{skillId} editor agents:write
PUT /orgs/{org}/agents/{agent}/skills/{skillId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/skills/{skillId} editor agents:write
POST /orgs/{org}/agents/{agent}/skills/{skillId}/_enable editor agents:write
POST /orgs/{org}/agents/{agent}/skills/{skillId}/_disable editor agents:write
POST /orgs/{org}/agents/{agent}/skills/import editor templates:apply

Each skill carries a source of author or system in responses. System skills are immutable — write attempts return 403 (skill.system_immutable).

A skill's runtime config (used by skills like the scheduling skill) is not writable through this CRUD surface — it's populated only via import / apply-template.

Import body:

{ "templateId": "@anychat/skill-template.meeting-scheduling" }
Response:
{
  "skill":  { ... },
  "addedToolBindings": ["@anychat/tool.google-calendar"]
}

addedToolBindings lists the empty bindings the import created — you typically loop over them and PUT each one's configuration before the skill is usable. An imported skill remains in the agent's configuration even if its required tools aren't configured yet; the runtime simply withholds it for that turn. Re-importing the same template replaces the existing skill in place.

Playbooks

A Playbook is a named mode of operation — the situated plan the agent follows in a given kind of moment (greeting, scheduling, product questions, …). Every agent has exactly one Default Playbook (isDefault: true) and at most one outreach default (isOutbound: true — the Playbook an outreach conversation starts in when the send doesn't pick one); the runtime switches between Playbooks mid-conversation (on generic via the switch_playbook tool; on v2 by a router decision recorded in the turn record). The agent decides when to leave a Playbook from the when-to-use of the others — there is no authored exit field. See Playbooks in the framework overview. On the v2 runtime a Playbook additionally declares the capabilities it draws on (uses) and may carry a deterministic path (flow); both are documented below.

interface AgentPlaybook {
  id: string;                  // short slug
  name: string;
  description?: string;        // operator-facing
  whenToUse?: string;          // LLM-facing activation context (routing)
  body: string;                // LLM-facing instructional body
  examples?: { role: 'user' | 'agent'; content: string }[][];
  procedures?: Array<{           // playbook-owned instruction blocks
    id: string;
    name: string;
    description?: string;
    body: string;
    requiredTools?: string[];    // capability actions the procedure uses
    config?: Record<string, unknown>;
    derivedFrom?: { templateId: string; version: string };
  }>;
  toolRefs?: string[];           // catalog tool ids this Playbook may invoke
  messageTemplateRefs?: string[];// ids of message templates this Playbook may deploy
  uses?: string[];               // capability ids this Playbook draws on (see Uses below)
  flow?: AgentFlow;              // the Playbook's deterministic path (see Flow below)
  isDefault: boolean;            // exactly one true per agent
  isOutbound?: boolean;          // at most one true — the outreach default
  outreach?: {                   // playbook-first outreach binding
    eligible?: boolean;          //   offered in the outreach send dialog
    openingTemplateId?: string;  //   message template sent as its opening
    exclusive?: boolean;         //   outreach-only: never a switch target
  };
  derivedFrom?: { templateId: string; version: string };
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/playbooks viewer agents:read
POST /orgs/{org}/agents/{agent}/playbooks editor agents:write
POST /orgs/{org}/agents/{agent}/playbooks/import editor templates:apply
GET /orgs/{org}/agents/{agent}/playbooks/{playbookId} viewer agents:read
PATCH /orgs/{org}/agents/{agent}/playbooks/{playbookId} editor agents:write
PUT /orgs/{org}/agents/{agent}/playbooks/{playbookId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/playbooks/{playbookId} editor agents:write
POST /orgs/{org}/agents/{agent}/playbooks/{playbookId}/_setDefault editor agents:write

GET (list) returns { "data": AgentPlaybook[] }. POST creates one (slugifies an id from the name when omitted) and returns the created Playbook. PUT replaces a Playbook while preserving its derivedFrom provenance; PATCH updates only the supplied fields. POST .../import copies a catalog playbook template ({ "templateId": "…" }) onto the agent — stamping derivedFrom and mapping the template's skill ids to skillRefs — and returns the full { "data": [...] } list.

POST, PUT, and PATCH all accept procedures[], validated the same way on each. A PUT whose body omits procedures keeps the Playbook's current procedures (as it keeps derivedFrom); send procedures: [] to clear them. PATCH with procedures replaces the list.

The runtime invariants are upheld on every write: the agent always has exactly one Playbook with isDefault: true when any exist (deleting the default promotes the next), and at most one with isOutbound: true. POST .../{playbookId}/_setDefault moves the default atomically. Template updates arrive at the agent level, not per Playbook: see Template updates.

The outreach binding makes a Playbook selectable per outreach send. eligible offers it in the console's send dialog (isOutbound: true implies eligibility, so the outreach default is always offered); openingTemplateId names the message template sent as the conversation's deterministic opening (the send-time greeting precedence is: explicit per-send template, then the selected Playbook's opening, then the agent-level outreach.greetingTemplateId); exclusive makes the Playbook outreach-only, meaning the runtime never offers it as a switch_playbook target — a conversation enters it only when an outreach send selects it. Validation: openingTemplateId must reference one of the agent's message templates, and a Playbook cannot combine isDefault with outreach.exclusive (_setDefault on an outreach-only Playbook is rejected, and invariant enforcement never promotes one to default). On PATCH, outreach: null clears the binding; an object replaces it wholesale.

Playbook writes that fail the framework-v2 checks are problem details:

Status code When
422 agent.playbook.uses_unknown uses names an id outside the agent's capability vocabulary (title lists the unknown ids and the vocabulary; errors[].field = uses)
422 agent.playbook.flow_invalid flow fails a structural flow.* lint rule (errors[] carries each finding's code, path, message)
422 request.invalid uses is not a string array, or flow is not an object with a steps array

Uses (capabilities per Playbook)

uses lists the capability ids the Playbook draws on:

Id Capability
scheduling Scheduling — offer times, book, reschedule, cancel
knowledge Knowledge — documents and the product catalog
people People — the Person record and People fields
notify Notifications — Slack / email alerts to your team
integration:<customToolId> One of the agent's custom tools, by id

POST, PUT, and PATCH validate uses against the agent's vocabulary (the four built-ins plus integration:<id> for each of its custom tools) on both frameworks; an unknown id is rejected with 422 (agent.playbook.uses_unknown — the structural lint rule playbook.uses.unknown, listing the unknown ids and the agent's vocabulary). Duplicates are removed. Omitting uses on PUT keeps the current list; send uses: [] to clear it.

What uses does depends on the agent's framework:

  • On v2 the runtime offers a Playbook only the tools of the capabilities it declares, and a Playbook whose declared capabilities are not ready is not enterable. A Playbook with no uses gets every ready capability (the lenient default) — lint then suggests a declaration (playbook.uses.suggest) so the Playbook's reach is explicit.
  • On generic uses is informative only (the v1 runtime resolves tools from procedures' requiredTools); lint reports it as v1.ignored.uses (info).

The two-way consistency check (playbook.uses.missing — the body talks about a capability it does not declare; playbook.uses.unused — a declared capability the body never draws on) runs on every save; see Lint.

Flow (deterministic path)

flow is the Playbook's deterministic path: a fixed sequence of steps the v2 runtime walks in order, with the model filling in answers and phrasing questions. When a Playbook carries a flow, the flow runs and the body is its brief. The generic runtime ignores it. Semantics — steps, slots, digressions, corrections, sub-flows — are described in Playbooks and Flows.

interface AgentFlow {
  id: string;
  name: string;
  description: string;
  slots: FlowSlot[];
  steps: FlowStep[];
  onComplete?: 'return' | 'end';   // pop back to the caller, or terminate (default 'return')
  allowDigressions?: boolean;      // may hand to another Playbook mid-flow, then resume
  generatedFrom?: {                // provenance of a generated flow
    bodyHash: string;              //   sha256 of the Playbook body it was compiled from
    by?: string; at?: string; model?: string;
  };
}

interface FlowSlot {
  name: string;                    // referenced from steps and {{name}} substitutions
  type: 'string' | 'email' | 'phone' | 'number' | 'date' | 'timeslot' | 'enum' | 'boolean';
  required?: boolean;
  bind?: string;                   // Person field (e.g. 'person.email'); a bound slot already on file is never asked
  options?: unknown[];             // static choices (enum)
  source?: string;                 // dynamic choices, e.g. 'tool:scheduling.getAvailability' (timeslot)
  prompt?: string;                 // pinned elicitation copy; generated when omitted
  validate?: string;               // '/regex/' or a predicate over the slot; a failing value is re-asked
  confirm?: boolean;               // read the value back once before it is used
}

// Exactly one key names the step kind. Every step may carry an `id`
// (the target of branch goto/else).
type FlowStep =
  | { collect: string | string[] }                       // fill one or more slots
  | { say: string | { text: string; suggestions?: string[] } }  // text, 'template:<messageTemplateId>', or text + 1–4 reply chips
  | { action: string; with?: Record<string, unknown>;    // call a capability tool, e.g. 'scheduling.createAppointment'
      confirm?: string; on_error?: 'retry' | 'handoff' | 'branch' }
  | { branch: { if: string; goto: string }[]; else?: string }  // predicates over slot state
  | { call: string }                                     // run another Playbook's flow, then return
  | { handoff: string }                                  // 'human', 'playbook:<id>', or a bare Playbook id
  | { end: { outcome: string; handoff?: string } };      // record the outcome; optionally hand off

POST, PUT, and PATCH accept flow. On every write the candidate document is run through Definition lint; any flow.* error finding under the written Playbook rejects the write with 422 (agent.playbook.flow_invalid) whose errors[] list each finding's code, path, and message. Warnings never block. flow: null on PATCH or PUT clears the path; omitting it on PUT keeps the current one. Editing the body without regenerating leaves the path in place — lint reports flow.body.stale until you regenerate or accept the difference.

Only the flow's own actions must be covered by the Playbook's uses (flow.action.capability.missing); a Playbook that declares no uses gets that finding as a warning instead (the lenient default).

Generating a flow

Draft a flow from the Playbook's text instead of writing one by hand. The runtime reads the brief and the agent's capability catalog (People fields, message templates, products, scheduling tools, Playbook ids), proposes an AgentFlow, validates it against the flow schema, lints every reference, and dry-runs it through the executor with a cooperative person. Nothing is persisted — inspect the result and, if you accept it, save it with PUT/PATCH carrying flow. This is the call behind the console's Deterministic path panel (Generate path / Regenerate) and the Copilot.

Method Path Role Scope
POST /orgs/{org}/agents/{agent}/playbooks/{playbookId}/flow:generate editor agents:write

Request body (all optional):

interface FlowGenerationRequest {
  brief?: string;     // what the flow should accomplish; default: the Playbook's body (+ description, whenToUse)
  uses?: string[];    // capability ids the flow may act through; default: the Playbook's `uses`
  model?: string;     // drafting model; default: the platform's
}

Response:

interface FlowGenerationResult {
  ok: boolean;              // a flow was produced, passes the schema, and has no lint errors
  flow?: AgentFlow;         // present on success — and, as the best attempt, on some failures
  notes?: string;           // the generator's assumptions / open questions
  brief: string;            // the brief that was used
  attempts: number;         // draft + repair rounds
  model: string;
  schema: { ok: boolean; errors: string[] };
  lint:   { errors: string[]; warnings: string[]; advice: string[] };
  dryRun?: {
    reachedEnd: boolean;    // the scripted walk reached an `end` step
    outcome?: string;
    stuckAt?: string;       // step the walk stalled at, when it did not
    turns: { person: string; agent: string[]; trace: string[] }[];
    toolCalls: string[];    // capability.tool names the walk would have called
  };
  usage: { input: number; output: number };   // tokens
  error?: string;           // why generation failed outright, when it did
}

Generation is slow — 10–40 seconds — and returns synchronously. Errors: 404 for an unknown agent or Playbook; 400 for a malformed body; 502 when the runtime is unreachable.

# 1. Draft a path from the booking Playbook's text.
curl -X POST "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/playbooks/schedule-call/flow:generate" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" -H "Content-Type: application/json" -d '{}'
# → { "ok": true, "flow": { ... }, "lint": { "errors": [], ... }, "dryRun": { "reachedEnd": true, "outcome": "call_booked", ... } }

# 2. Accept it.
curl -X PATCH "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/playbooks/schedule-call" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" -H "Content-Type: application/json" \
  -d '{"flow": { ...the returned flow... }}'

Tool bindings

A binding pairs a catalog Tool with the configuration that agent needs to use it (a webhook URL + auth header for the Custom HTTP Webhook tool, a calendar id for Google Calendar, etc.).

interface AgentToolBinding {
  toolId: string;                            // e.g. @anychat/tool.custom-http-webhook
  config: Record<string, unknown>;           // shape defined by the tool's configSchema
  enabled: boolean;                          // default true; false = bound but not offered to the agent
  configStatus: 'complete' | 'incomplete' | 'invalid'; // read-only, computed against configSchema
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/tool-bindings viewer agents:read
GET /orgs/{org}/agents/{agent}/tool-bindings/{toolId} viewer agents:read
PUT /orgs/{org}/agents/{agent}/tool-bindings/{toolId} editor agents:write
PATCH /orgs/{org}/agents/{agent}/tool-bindings/{toolId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/tool-bindings/{toolId} editor agents:write
POST /orgs/{org}/agents/{agent}/tool-bindings/{toolId}/_enable editor agents:write
POST /orgs/{org}/agents/{agent}/tool-bindings/{toolId}/_disable editor agents:write

{toolId} must be URL-encoded. PUT replaces the binding's config; PATCH merges into it:

{ "config": { "webhookUrl": "https://example.com/hook", "authHeader": "..." } }

Both accept a top-level enabled: boolean alongside config; when it is omitted the stored value is kept (new bindings default to true). _enable and _disable flip the same flag without touching config. A disabled binding keeps its configuration and secrets but is not offered to the agent at runtime.

configStatus is computed on every read from the catalog tool's configSchema: complete when every required field is present with the expected type, incomplete when a required field is missing, and invalid when a present field has the wrong type. Tools that publish no configSchema always read complete.

The catalog's configSchema for the tool describes the expected shape of config. Bindings to unknown tools are rejected. DELETE returns 204 No Content (and also clears any stored secrets for the binding).

Secret fields are write-only. Any config field the catalog marks writeOnly: true (API keys, webhook URLs, auth headers) is stored encrypted in a separate secret store, never on the binding document and never in an export. A read returns redacted metadata in its place:

{ "config": { "webhookUrl": { "set": true, "last4": "9af3" } } }
On write, send a non-empty string to set or replace the value, an empty string to clear it, or omit the field to leave the stored secret untouched. A binding's configStatus counts a required write-only field as satisfied once a value is stored.

Custom tools

A custom tool is the agent-facing contract of a Custom integration: what the agent calls the tool, what it does, and the arguments the agent supplies. The connection behind it (URL, method, auth header) is the separate tool binding keyed by the same id, so credentials never enter or leave through this resource. GET .../custom-tools returns the same list as integrations.custom in an export.

interface CustomToolDef {
  id: string;                          // ^[a-zA-Z0-9_-]{1,64}$ — the name the agent calls the tool by
  name?: string;                       // operator-facing label; defaults to id
  description: string;                 // what the tool does and when to use it
  parameters?: Record<string, unknown>;// JSON Schema for the call arguments; defaults to {}
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/custom-tools viewer agents:read
GET /orgs/{org}/agents/{agent}/custom-tools/{toolId} viewer agents:read
PUT /orgs/{org}/agents/{agent}/custom-tools/{toolId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/custom-tools/{toolId} editor agents:write

GET (list) returns { "data": CustomToolDef[] }; GET .../{toolId} returns one CustomToolDef. PUT upserts the contract under the id in the path from { "name"?, "description", "parameters"? } (a body id, if present, must equal the path id) and returns the stored CustomToolDef with 201 when it created the tool or 200 when it replaced one. DELETE returns 204 and leaves the tool binding in place; delete the binding as well when retiring an integration.

Errors are problem details: 404 tool.not_found for an unknown {toolId} on GET or DELETE, and 422 request.invalid for a malformed body (errors[].field names the path, for example customTool.description).

OAuth-runtime tools

Some tools (Google Calendar, Slack-as-CRM, …) require an OAuth grant from the operator before the agent can use them. Three endpoints drive the operator-facing flow.

Method Path Role Scope
POST /orgs/{org}/agents/{agent}/tools/{toolId}/oauth/start editor agents:write
GET /orgs/{org}/agents/{agent}/tools/{toolId}/oauth/status viewer agents:read
POST /orgs/{org}/agents/{agent}/tools/{toolId}/oauth/disconnect editor agents:write

Start returns an authorizationUrl the operator's browser should navigate to. Optionally include a returnTo URL — Anychat redirects the browser back there once the grant completes, with ?oauthStatus=success&toolId=… or ?oauthStatus=error&reason=….

// POST .../oauth/start
{ "returnTo": "https://yourapp.example.com/agents/acme-bot/tools" }
// 200
{ "authorizationUrl": "https://accounts.google.com/o/oauth2/..." }

The grant completes at GET /v1/oauth/callback — a single, unauthenticated endpoint the provider redirects the operator's browser to with ?code=…&state=…. You never call it yourself; the unguessable single-use state token minted by start ties the completion back to the right (org, agent, tool) and is its CSRF defense. On completion the browser is redirected to your returnTo (which must be on an Anychat-allow-listed host — otherwise a minimal success/failure page is shown instead of an open redirect).

Status reports the current credential state for the binding:

type OAuthStatus =
  | { state: 'not-connected' }
  | { state: 'connected'; connectedAt: string }
  | { state: 'expired';   connectedAt: string }
  | { state: 'revoked';   connectedAt: string; lastError?: string }
  | { state: 'error';     connectedAt: string; lastError?: string };

Disconnect revokes the grant at the provider and deletes the stored credential. The binding stays in place so reconnecting is one click; remove the binding with DELETE .../tool-bindings/{toolId} if you also want the tool unbound from the agent.

Guardrails

Four parallel lists of plain-text constraints, plus a back-link list of the templates the agent has imported from.

interface AgentGuardrails {
  topicsToAvoid?: string[];
  claimsNotToMake?: string[];
  requiredDisclaimers?: string[];
  additionalRules?: string;     // free-form hard rules that don't fit the lists
  derivedFromTemplates?: string[];
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/guardrails viewer agents:read
PUT /orgs/{org}/agents/{agent}/guardrails editor agents:write
PATCH /orgs/{org}/agents/{agent}/guardrails editor agents:write
POST /orgs/{org}/agents/{agent}/guardrails/import editor templates:apply

PUT replaces the lists wholesale; PATCH updates only the lists you send. The list categories (topicsToAvoid, claimsNotToMake, requiredDisclaimers) take { add, remove } operations under PATCH; additionalRules is a free-form string you set or clear directly. (Escalation is no longer a guardrail — it's the Escalate to Human Playbook.)

Import merges the template's strings into the agent's existing lists (deduplicated, original order preserved). The template id is appended to derivedFromTemplates:

{ "templateId": "@anychat/guardrail-template.healthcare-disclaimers" }
Guardrails are the only block where import merges into the existing lists instead of replacing them, so importing several guardrail templates accumulates their entries.

Message templates

Operator-authored canned messages with {slot} placeholders, rendered deterministically at send time. Outreach uses one as its greeting; the collection is general-purpose.

interface AgentMessageTemplate {
  id: string;
  name: string;
  description?: string;
  body: string;            // slot syntax, e.g. "Hi {person.firstName | "there"}"
  defaultOnMissingSlot?: 'omit-sentence' | 'omit-message' | 'block-send';
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/message-templates viewer agents:read
POST /orgs/{org}/agents/{agent}/message-templates editor agents:write
GET /orgs/{org}/agents/{agent}/message-templates/{templateId} viewer agents:read
GET /orgs/{org}/agents/{agent}/message-templates/{templateId}/preview viewer agents:read
PATCH /orgs/{org}/agents/{agent}/message-templates/{templateId} editor agents:write
PUT /orgs/{org}/agents/{agent}/message-templates/{templateId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/message-templates/{templateId} editor agents:write

POST creates a template with a server-generated id. PUT is an upsert: it replaces the template stored under {templateId}, or, when none exists, creates one under that client-chosen id and returns 201 (a replace returns 200). PATCH merges into an existing template. name and body are required on POST and PUT.

A template body is AML: plain text plus optional markers, so reviewed copy can carry chips, a rich card, or a carousel — [[chips: Book a demo | Tell me more]], [[card | Title ~ description | media: image:<id>]], one card per line inside [[carousel … ]], and [[next-message]] for a second bubble. Pictures come from the agent's media library and are named by id (media: image:<id>), never by URL. Playbooks send a template with [[template: <id>]] (v1) or templateRef (v2); the platform expands it in place, markers and all.

Slot syntax reads from the Person record and the agent identity: {person.firstName}, {person.customFields.plan}, {agent.companyName}. Per-slot fallbacks use an inline pipe — {person.firstName | "there"} (literal default), or one of omit-sentence / omit-message / block-send. defaultOnMissingSlot sets the policy for slots without an inline one.

preview renders the template against a real Person (?personId=… required) and returns what would be sent:

interface MessageTemplatePreview {
  renderedText: string | null;  // null when block-send fired
  missingSlots: Array<{
    path: string;               // e.g. person.firstName
    action: 'default' | 'omit-sentence' | 'omit-message' | 'block-send';
  }>;
  blocked: boolean;             // true iff a block-send slot was hit
}
{ "renderedText": "Hi Dana — thanks for stopping by Acme.", "missingSlots": [], "blocked": false }

Media

The agent's media library: the images (and, as rich channels allow, videos) it may show in rich cards, carousels and media messages. A menu as a carousel, a product card, a "here's what we do" showcase early in a conversation. The platform hosts each item (the object is created and deleted with the entry). Content refers to an item by id, never by URL: media: image:<id> in any AML marker that takes media: ([[card]], [[carousel]] lines, [[showcase]] items), [[media: image:<id>]] on its own, or card.media / media in a Framework v2 response. A reference is <kind>:<id>; the kind is a readability cue and the id is the key. An id that no longer exists degrades to "no picture" at send time, never a broken link, and lint reports it (media.ref.unknown).

name and usageHint are the human-facing fields: what the item is, and when the agent should reach for it. description is what the agent reads when it decides to show something on its own ("Media you can show: id — description (Use when: hint)" sits next to the product catalog in its brief). It is generated by a vision model when the item is added or its file replaced (what it depicts, visible text quoted) and is editable.

interface AgentMediaItem {
  id: string;                     // stable per-agent slug — the reference key
  kind: 'image' | 'video';        // image today; video as channels allow
  name: string;
  description: string;
  descriptionSource: 'generated' | 'edited' | 'none';
  usageHint?: string;
  url: string;                    // public hosted URL (read-only)
  contentType: string;            // image/png | image/jpeg | image/gif | image/webp
  bytes: number;
  width?: number;
  height?: number;
  sourceUrl?: string;             // when added by URL
  createdAt: string;
  updatedAt: string;
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/media viewer agents:read
POST /orgs/{org}/agents/{agent}/media editor agents:write
GET /orgs/{org}/agents/{agent}/media/{mediaId} viewer agents:read
PATCH /orgs/{org}/agents/{agent}/media/{mediaId} editor agents:write
PUT /orgs/{org}/agents/{agent}/media/{mediaId}/content editor agents:write
POST /orgs/{org}/agents/{agent}/media/{mediaId}:describe editor agents:write
DELETE /orgs/{org}/agents/{agent}/media/{mediaId} editor agents:write

GET …/media returns { data: AgentMediaItem[] }. POST …/media adds an item either as multipart (file field, plus optional name, description, usageHint, id fields) or as JSON:

interface AgentMediaCreateRequest {
  sourceUrl?: string;    // public http(s) URL to fetch — or
  dataBase64?: string;   // raw bytes, base64
  id?: string;           // preferred id (slugified; suffixed when taken); defaults from name
  name?: string;
  description?: string;  // skips generation when supplied
  usageHint?: string;
}

It returns 201 with the entry, or 200 with the existing entry when the same bytes are already in the library. PNG, JPEG, GIF and WebP up to 5 MB; at most 200 items per agent. PATCH takes any of name, description, usageHint (null clears the hint). The id never changes.

PUT …/media/{mediaId}/content replaces the file behind an id, as multipart (file) or JSON ({ sourceUrl } or { dataBase64 }): a new hosted URL, a regenerated description, and the same id, name and usage hint, so every template that references the id shows the new picture. POST …/media/{mediaId}:describe regenerates the description from the stored file. DELETE removes the entry and the hosted object.

Errors: agent.media.not_found (404), agent.media.source_required (422), agent.media.unsupported_type (415), agent.media.too_large (413), agent.media.limit_reached (409), agent.media.fetch_failed (422).

/orgs/{org}/agents/{agent}/images… is a deprecated alias of the same routes.

Export / Import carries the library as capabilities.media[] (id, kind, name, description, usageHint, url); an import adds, by URL, the ids the target agent lacks.

Outreach

Configures the agent's outreach greeting — the templated first message sent when the agent initiates a conversation — plus an optional automatic follow-up (nudge) for people who don't reply. See Outreach in the framework overview.

interface OutreachConfig {
  greetingTemplateId?: string;   // id of one of the agent's message templates
  followUps?: OutreachFollowUps; // a single automatic nudge for non-responders
}

interface OutreachFollowUps {
  enabled: boolean;              // master switch for automatic sending
  steps: OutreachFollowUpStep[]; // exactly one step (the single follow-up)
  quietHours?: OutreachQuietHours;
  recordInHistory?: boolean;     // record sent nudges into the conversation history (default false)
}

interface OutreachFollowUpStep {
  id: string;
  delay: { value: number; unit: "hours" | "days" }; // wait since the outreach
  messageTemplateId: string;     // id of one of the agent's message templates
}

interface OutreachQuietHours {
  startHour: number;             // 0–23, inclusive
  endHour: number;               // 0–23, exclusive; may wrap midnight
  timezone: string;              // IANA zone, e.g. "America/Chicago"
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/outreach viewer agents:read
PUT /orgs/{org}/agents/{agent}/outreach editor agents:write
GET /orgs/{org}/agents/{agent}/outreach/preview viewer agents:read

PUT replaces the whole config. { "greetingTemplateId": "outreach-greeting" } sets the greeting; the id must reference an existing message template on the agent (a dangling id is rejected with 404). PUT {} clears the config. When greetingTemplateId is unset, outreach falls back to an LLM-composed opener.

Follow-ups. Supply followUps to configure the automatic nudge. At most one automatic follow-up is ever sent per personsteps holds exactly one entry (more than one is rejected with 422). The step's messageTemplateId must reference an existing message template (else 404); delay.value must be a positive number and delay.unit one of "hours"/"days"; quietHours hours are integers 0–23 and timezone a non-empty IANA string (otherwise 422). The delay is measured from the initial outreach. The follow-up is cancelled the moment the person replies, and the automatic send is suppressed during quietHours. Set recordInHistory: true to have each sent nudge recorded into the conversation history as an agent turn, so the agent's reply to a lead who answers the nudge is aware of it (default false; opt-in per agent). Nudges are RCS-only today. Follow-ups default off and only enroll people contacted after they're enabled — turning them on never retroactively messages your existing contacts. (Operators can also send additional follow-ups by hand from the console; those manual sends are separate and not capped.)

preview renders the configured greeting for a given Person (?personId=… required) and wraps the message-template preview shape under a greeting key:

{ "greeting": { "renderedText": "Hi Dana — …", "missingSlots": [], "blocked": false } }
preview returns 404 when no greeting template is configured.

RCS doesn't let users start a conversation with a business. An outreach link is a shareable URL to a public phone-entry page: a prospect opens it, enters their phone number, and the agent initiates the RCS conversation. Links are minted against one of the agent's RCS channel bindings and are use-limited and time-limited.

interface OutreachLink {
  id: string;
  code: string;             // URL-safe code; the link is {url}
  url: string;              // shareable page, e.g. https://embed.anychat.ai/o/{code}
  label?: string;           // operator-facing note
  maxUses: number;
  usesRemaining: number;
  redemptionCount: number;
  expiresAt: string;        // ISO-8601
  disabled: boolean;
  createdAt?: string;
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/outreach-links viewer agents:read
POST /orgs/{org}/agents/{agent}/outreach-links editor agents:write
PATCH /orgs/{org}/agents/{agent}/outreach-links/{id} editor agents:write
DELETE /orgs/{org}/agents/{agent}/outreach-links/{id} editor agents:write

Create body:

{ "bindingId": "<rcs channel binding id>", "label": "Trade-show QR",
  "maxUses": 100, "expiresAt": "2026-07-01T00:00:00Z" }
bindingId is required and must be an RCS binding on this agent. maxUses defaults to 25 (1–500). expiresAt defaults to 14 days out and may be at most 30 days out. Validation failures return 400 with a human-readable error.

PATCH takes exactly { "disabled": true | false } — pause or resume a link without losing its remaining uses. DELETE removes the link (204). The redemption audit log is not exposed; redemptionCount summarizes it.

Human escalation

Anychat does not route conversations to a live human in real time. When the user asks for a human, the runtime escalate-to-human skill renders the agent's identity contact fields (contactEmail, contactPhone, websiteUrl) as tappable contact options and shows contactAvailability verbatim if set — the user takes the action. Configure these once on the Identity resource; there is no separate escalation-contacts payload.

Scheduling

The Scheduling capability: the hours the agent may book, the default call length, who is notified, the pre-call reminder, and the representatives the booked calls are with. This resource is the capabilities.scheduling slice of an export. The agent's Google Calendar connection is account-specific state and is not part of it: it never appears on GET, is ignored on PUT, and is preserved across writes (connect a calendar through the console or the OAuth-runtime tools endpoints).

interface SchedulingConfig {
  timezone?: string;                       // IANA; default zone for proposing and holding slots
  availableHours?: SchedulingWindow[];     // weekly bookable windows; empty = nothing restricted
  blackoutDates?: string[];                // YYYY-MM-DD dates with no availability
  disallowSameDayBooking?: boolean;        // never offer slots on the current day (in timezone); default false
  defaultDurationMinutes?: number;         // 1–1440
  notificationRecipients?: string[];       // emails for appointment-lifecycle mail, alongside the representative's
  reminders?: {
    enabled?: boolean;                     // unset behaves as on
    leadMinutes?: number;                  // 1–10080, minutes before the call
    includeContactCard?: boolean;          // attach the representative's contact card (.vcf)
  };
  representatives?: SchedulingRepresentative[];
  defaultRepresentativeIdentity?: string;  // fallback identity, e.g. "a sales representative"
}

interface SchedulingRepresentative {
  id: string;                              // stable per-agent id (Person.assignedRepresentativeId)
  name: string;
  title?: string;
  phone?: string;
  email?: string;                          // lifecycle emails go here
  type?: string;                           // free string: sales, advisor, clinician, support; default sales
  availability?: {                         // empty = use the agent-level availableHours
    timezone?: string;
    workingHours?: SchedulingWindow[];
  };
}

interface SchedulingWindow {
  dayOfWeek: number;                       // 0–6, Sunday–Saturday
  startTime: string;                       // "HH:MM", 24-hour, in the governing timezone
  endTime: string;                         // "HH:MM"
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/scheduling viewer agents:read
PUT /orgs/{org}/agents/{agent}/scheduling editor agents:write

GET returns the SchedulingConfig (representatives is always present, [] when there are none). PUT takes a full SchedulingConfig and replaces the whole configuration: keys the body omits are cleared (representatives becomes [], defaultRepresentativeIdentity is unset), so read, modify, and write back the whole object. Both return the stored SchedulingConfig. Validation failures are 422 request.invalid problem details whose errors[].field names the offending path, for example scheduling.availableHours[0].startTime. Representative ids must be unique within the agent.

Products

The agent's product catalog, part of the Knowledge capability: the offerings the agent can present as cards and reconcile a Person's productInterest against. This resource is the capabilities.knowledge.products slice of an export.

interface AgentProduct {
  id: string;                          // stable per-agent id, unique within the agent
  name: string;
  description?: string;
  imageUrl?: string;
  productPageUrl?: string;
  metadata?: Record<string, string>;   // free-form attributes (price, category, …)
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/products viewer agents:read
PUT /orgs/{org}/agents/{agent}/products editor agents:write

GET returns { "data": AgentProduct[] }. PUT takes { "products": AgentProduct[] } and replaces the catalog; both return { "data": AgentProduct[] }. A body without a products array, a product missing id or name, a duplicate id, or a non-string metadata value is a 422 request.invalid problem detail (errors[].field, for example products[1].id).

Knowledge documents

The documents in the agent's knowledge base, the other half of the Knowledge capability. This surface is read-only and returns document metadata: the file name, type, title, and processing status. Neither the extracted text nor the stored file is returned. Documents are uploaded and removed in the console, under the agent's Knowledge area (see Knowledge & Products).

interface KnowledgeDocument {
  id: string;
  agentId: string;
  fileName: string;
  mimeType: string;
  title?: string;                              // extracted or operator-set document title
  status: 'processing' | 'ready' | 'failed';   // ready = indexed and available to the agent
  uploadedAt: string;                          // ISO-8601
  createdAt?: string;                          // ISO-8601
  updatedAt?: string;                          // ISO-8601
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/knowledge/documents viewer knowledge:read
GET /orgs/{org}/agents/{agent}/knowledge/documents/{documentId} viewer knowledge:read

The list returns { "data": KnowledgeDocument[] } and is not paginated. An unknown {documentId} on this agent is a 404 resource.not_found problem detail.

Channels

Wires an agent to the messaging channels its users live on — RCS, Apple Messages, WhatsApp, SMS, webchat, and the long tail. Operators think in channels; the provider serving a channel is an implementation detail. One live binding per channel per agent.

Two ways to connect a channel:

  • Managed (recommended) — name the channel, supply the business inputs its setup form asks for (brand name, banner, 10DLC registration info, …), and Anychat provisions it: provider choice, credentials, and the carrier/Apple/Meta workflow are all handled for you. Provisioning for rich channels takes days to weeks; the binding's status and events tell the story.
  • Unmanaged — bring your own provider account (your Webex Connect tenancy, Twilio account, Google RBM agent, or WhatsApp Business Account) and supply its credentials. auth is write-only: it is accepted on create and rotate, and never returned by any read.
type ChannelName =
  | 'rcs' | 'appleMessages' | 'whatsapp' | 'sms' | 'webchat'
  | 'viber' | 'instagram' | 'slack' | 'poe';

type ChannelBindingStatus =
  | 'Requested'        // managed request filed; Anychat is on it
  | 'Provisioning'     // carrier/Apple/Meta workflow in flight
  | 'ActionRequired'   // blocked on YOU — statusDetail says what
  | 'Active'
  | 'Failed'           // statusDetail explains
  | 'Disabled';

interface ChannelBinding {
  id: string;
  agentId: string;
  channel: ChannelName;
  provider?: string;        // unmanaged only — you chose it
  mode: 'managed' | 'unmanaged';
  status: ChannelBindingStatus;
  statusDetail?: string;    // operator-safe status explanation
  events?: Array<{          // lifecycle history, oldest first
    at: string;
    from?: ChannelBindingStatus;
    to: ChannelBindingStatus;
    note?: string;
    actor: 'operator' | 'sysop' | 'system';
  }>;
  routingId?: string;       // provider-side id; display only
  serviceId?: string;       // provider-assigned service id (some RCS providers); display only
  inputs?: Record<string, unknown>; // your submitted business inputs
  delivery?: {              // delivery tuning (any mode)
    minMessageDelayMs?: number;     // 250–3000; unset = no pacing
  };
  compliance?: {            // carrier compliance (rcs/sms only)
    enabled: boolean;               // off by default
    disclaimerText?: string;        // override; max 1000 chars
  };
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/channels viewer agents:read
GET /orgs/{org}/agents/{agent}/channels/{bindingId} viewer agents:read
POST /orgs/{org}/agents/{agent}/channels editor agents:write
PATCH /orgs/{org}/agents/{agent}/channels/{bindingId} editor agents:write
DELETE /orgs/{org}/agents/{agent}/channels/{bindingId} editor agents:write
POST /orgs/{org}/agents/{agent}/channels/{bindingId}/_disable editor agents:write
POST /orgs/{org}/agents/{agent}/channels/{bindingId}/_enable editor agents:write

POST bodies:

{ "channel": "rcs", "mode": "managed",
  "inputs": { "brandName": "Acme Health", "bannerUrl": "https://…", "...": "…" } }
{ "channel": "sms", "mode": "unmanaged", "provider": "twilio",
  "auth": { "accountSid": "AC…", "authToken": "…",
            "messagingServiceSid": "MG…", "phoneNumber": "+1555…" } }

A managed request returns 201 with status: "Requested" (or "Active" for instantly-provisioned channels like webchat); poll the binding or list to follow progress. A second create for an already-bound channel returns 409. PATCH { "auth": { … } } rotates unmanaged credentials. DELETE removes the binding — cancelling a pending request, or releasing a live one (managed RCS is archived rather than deleted, so re-enabling restores the same carrier-verified agent) — and returns the removed ChannelBinding with 200 rather than 204.

PATCH { "delivery": { "minMessageDelayMs": 1000 } } sets message pacing: a minimum gap (250–3000 ms) between consecutive agent messages to one user on this channel. Use it to make multi-message replies land one after another, and to keep messages in order on providers that don't guarantee delivery ordering. Works on managed and unmanaged bindings alike; "delivery": null clears it. Out-of-range values return 400.

PATCH { "compliance": { "enabled": true } } turns on carrier compliance for an RCS or SMS binding (off by default). When enabled, Anychat sends a one-time disclaimer at the start of each messaging relationship — the stock operator text, or your disclaimerText override — and handles the CTIA keyword set deterministically: STOP/UNSUBSCRIBE/CANCEL/END/QUIT get a compliant opt-out confirmation and halt all further messages to that user; HELP gets a brand-contact reply built from the agent's identity (answered even when opted out); START re-subscribes with a confirmation. Leave it off when your provider already auto-handles opt-out keywords (many do — e.g. Twilio Advanced Opt-Out), or users will receive two confirmations. "compliance": null clears the block; non-carrier channels return 400.

The per-channel setup forms (which fields, which are seeded from identity, expected provisioning times) come from the channels catalog. Webchat is created automatically with every agent.

People

The built-in CRM. Anychat keeps one Person record for each person, per agent, and updates it as conversations reveal new facts. See the People guide for the console side.

interface Person {
  id: string;
  fullName?: string;
  firstName?: string;
  lastName?: string;
  email?: string;
  phone?: string;
  company?: string;
  title?: string;
  status: 'new' | 'contacted' | 'qualified' | 'converted' | 'lost';
  notes?: string;
  customFields?: Record<string, string>;
  tags?: string[];
  externalId?: string;               // correlation id for an external system
  lockedFields?: string[];           // field paths the agent may never write
  // Read-only: who set each field's current value, keyed by field path
  fieldMeta?: Record<string, { by: 'operator'|'agent'|'import'|'intake'|'system'; at: string }>;
  // Read-only: flagged possible-duplicate pairs
  duplicateCandidates?: Array<{
    personId: string;
    matchedOn: 'externalId' | 'email' | 'phone';
    at: string;
    dismissedAt?: string;
  }>;
  assignedRepresentativeId?: string; // a representative on the agent
  journal?: PersonJournalEntry[];   // read-only memory log
  channelUserIds?: string[];        // read-only channel identities (ASIDs)
  // Outreach state (read-only; runtime-owned) —
  outreachInitiatedAt?: string;      // ISO-8601; set once outreach is delivered
  outreachReachability?: 'rcs' | 'unreachable'; // result of the last attempt's capability check
  lastOutreachAttemptAt?: string;    // ISO-8601; last attempt, success or failure
  // Follow-up state (read-only; runtime-owned) —
  lastInboundAt?: string;            // ISO-8601; most recent inbound reply
  nextFollowUpAt?: string;           // ISO-8601; when the next nudge is due, if enrolled
  followUpsSentCount?: number;       // nudges sent in the current sequence
  followUpsPaused?: boolean;         // operator paused automatic nudges
  // Scheduling mirrors —
  scheduledCallAt?: string;          // ISO-8601; next active booking (cleared once it elapses)
  scheduledCallTimezone?: string;    // IANA
  lastCallAt?: string;               // ISO-8601; most recent completed call (read-only; persists)
  lastCallTimezone?: string;         // IANA (read-only)
  // …plus channel metadata
}

interface PersonJournalEntry {
  at: string;                        // ISO-8601
  author: 'agent' | 'operator';
  text: string;
}

interface Appointment {
  id: string;
  startUtc: string;                  // ISO-8601 (UTC)
  durationMinutes: number;
  prospectTimezone: string;          // IANA, e.g. America/Los_Angeles
  status: 'scheduled' | 'rescheduled' | 'cancelled' | 'completed';
  representativeName?: string;
  representativeTitle?: string;
  notes?: string;
  history: Array<{
    at: string;                      // ISO-8601
    action: string;                  // created / rescheduled / cancelled / completed
    from?: string;
    to?: string;
    reason?: string;
  }>;
  createdAt?: string;
  // Present on the agent-level listing only —
  personId?: string;                 // linked person, when resolvable
  personName?: string;
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/people viewer people:read
POST /orgs/{org}/agents/{agent}/people editor people:write
GET /orgs/{org}/agents/{agent}/people/{personId} viewer people:read
PATCH /orgs/{org}/agents/{agent}/people/{personId} editor people:write
DELETE /orgs/{org}/agents/{agent}/people/{personId} editor people:write
GET /orgs/{org}/agents/{agent}/people/{personId}/appointments viewer people:read
GET /orgs/{org}/agents/{agent}/appointments viewer people:read
GET /orgs/{org}/agents/{agent}/people/export editor people:export
POST /orgs/{org}/agents/{agent}/people/import editor people:write
POST /orgs/{org}/agents/{agent}/people/{personId}/merge editor people:write
POST /orgs/{org}/agents/{agent}/people/{personId}/restore editor people:write
POST /orgs/{org}/agents/{agent}/people/{personId}/dismissDuplicate editor people:write

GET /people accepts limit (max 200), startingAfter (the last person id of the previous page), and the filter parameters below. With any of these parameters the response is { "data": Person[], "hasMore": boolean }, newest first. A bare GET /people still returns the full { "data": Person[] } array; treat that form as deprecated and pass limit in new integrations. Single-resource reads and writes return the bare Person object.

Filtering people

The list and export endpoints share one filter vocabulary. Filters combine with AND: a person matches only when it satisfies every parameter supplied.

Parameter Matches
status Exact funnel status
q Case-insensitive substring of name, email, phone, or company
tags People carrying any of the listed tags (comma-separated)
source Exact source value
lastImportBatchId People created or updated by one import call (the importBatchId in an import result)
hasPhone true: a phone number is on file; false: missing or empty
outreachReachability rcs or unreachable, the result of the most recent outreach attempt's capability check
createdAfter / createdBefore Record created on or after / on or before the ISO-8601 instant
lastInboundAfter / lastInboundBefore Most recent inbound message on or after / on or before the ISO-8601 instant
cf.<key> Exact value of the custom field <key>, e.g. cf.targetMarket=medspa
segmentId The filter saved in a segment

A cf. key must be a declared People field; an undeclared key is rejected with 400 naming the key. segmentId stands alone: combining it with any individual filter parameter is a 400, and an unknown segment id is a 404.

GET .../people/export takes the same parameters alongside its format, so a filtered export contains exactly the people the equivalent filtered list returns.

Writable fields

POST and PATCH accept exactly these fields; on create, the person is bound to the agent in the path. A PATCH with none of them is a 400.

Field Type Limit
fullName string 200
firstName / lastName string 100 each
email string 300
phone string 50; normalized to E.164 on write
company string 200
title string 200
productInterest string 200; reconciled against the product catalog at use time
source string 200
status enum new, contacted, qualified, converted, lost
notes string 5,000
tags string[] Free-form labels
externalId string 200; exact-matched by import
lockedFields string[] Field paths the agent may never write (e.g. email, customFields.budget)
customFields object String values, 1,000 each; keys must be declared People fields, and values are validated against each field's declared type
assignedRepresentativeId string Must reference one of the agent's representatives
scheduledCallAt / scheduledCallTimezone ISO-8601 / IANA Normally maintained by the scheduling feature; writable as an operator override

productInterest, status, assignedRepresentativeId, and the scheduled-call fields serve the product-catalog, sales-funnel, and call-scheduling features. Applications without a catalog, funnel, or representatives can leave them untouched; status stays new.

Everything else on Person is runtime-owned and read-only: channel identity, outreach state, follow-up state, and the journal, which the agent appends to as it learns (including a one-line summary each time a conversation ends; entries are capped at 2,000 characters).

DELETE removes the person (204). Deletion is soft for 30 days: the record disappears from every list, lookup, and export, and POST .../{personId}/restore undoes it. Records consumed by a merge cannot be restored (409). After 30 days the record is purged.

GET .../people/export streams the CRM, narrowed by any filter parameters present: format=csv (default) produces one row per person with columns matching the import template, and format=ndjson produces one full JSON record per line including the journal and fieldMeta.

POST .../people/import takes { "rows": [...], "mode": "fill" } where each row is a header-to-value object. Rows matching an existing person by externalId or email update that person: mode: "fill" (default) writes only empty fields, mode: "update" overwrites with non-empty incoming values. Locked fields are never overwritten. The result reports imported, updated, skipped, duplicatesFlagged, unmatchedColumns, per-row rowErrors, and an importBatchId that the lastImportBatchId filter accepts to list exactly the people this call touched.

POST .../people/{personId}/merge with { "sourceId": "...", "fieldChoices": { "email": "source" } } merges the source record into the target and retires the source. Defaults are target-wins with source filling empty fields; fieldChoices overrides per field. For status, new counts as empty, so a new target adopts a contacted/qualified source's stage. Outreach and follow-up state (outreachInitiatedAt, lastOutreachTouchAt, nextFollowUpAt, …) moves to the target when the target has never been reached out to; lastInboundAt and lastCallAt take the later value and createdAt the earlier. Conflicting externalIds return 409 unless a choice is supplied. dismissDuplicate with { "personId": "<other>" } marks a flagged pair as distinct people; the pair is never re-flagged.

.../people/{personId}/appointments lists the person's scheduled calls (newest first, including cancelled ones) as { "data": Appointment[] }.

.../agents/{agent}/appointments lists every appointment across the agent's people, newest start first, each annotated with personId and personName when the linked person can be resolved. Optional query parameters: status (scheduled, completed, cancelled) and limit (default 200, max 500). A completed status means the call's scheduled time has elapsed; the person's lastCallAt mirrors the most recent such call.

Sending outreach

Send outreach to one Person: the Agent API form of the People page's Send and Follow up actions. Both routes go through the same checks and produce the same outcomes as the console. See Outreach for the operator side and Outreach above for the agent-level configuration.

Method Path Role Scope
POST /orgs/{org}/agents/{agent}/people/{personId}/outreach editor people:write
POST /orgs/{org}/agents/{agent}/people/{personId}/outreach/follow-up editor people:write

POST .../outreach initiates the outreach conversation. The body is optional:

Field Meaning
greetingTemplateId Per-send override of the agent's outreach.greetingTemplateId. An id the agent does not have falls back to the configured default; the send never goes out empty.
playbookId Per-send override of the outreach default Playbook. An unknown id is rejected (400) rather than silently falling back, since it would misroute every reply.

Unknown body keys are ignored. On success the response is 200:

{ "success": true, "channelUserId": "v2-…", "rcsCapable": true, "carrier": "T-Mobile" }

channelUserId is the channel identity (ASID) the conversation was opened with, rcsCapable is true when the phone was verified RCS-capable before sending, and carrier is the carrier the capability check reported; each is present when known.

POST .../outreach/follow-up sends one follow-up (nudge) now, independent of the automatic cadence. The body is { "messageTemplateId"?: string }, naming the message template to send; omit it to send the next cadence step's template. On success the response is 200 { "success": true }.

Errors are problem details:

Status code When
409 agent.outreach.already_initiated Outreach was already initiated for this Person (detail carries the agent's message)
400 agent.outreach.send_failed The send could not be made: no E.164 phone on file, the phone is not RCS-capable, the agent has no active RCS channel, or the dispatch failed (title carries the reason, the same text the console shows)
400 agent.outreach.follow_up_failed The follow-up could not be sent: no outreach conversation yet, an unknown or unconfigured template, or a render or delivery failure (title carries the reason)
400 request.invalid The agent rejected the request, for example an unknown playbookId (title names the id)
422 request.invalid A supplied override is not a non-empty string (errors[].field names it)
404 resource.not_found The Person is not in the caller's org or belongs to another agent

Examples

# List an agent's people
curl -s https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people \
  -H "Authorization: Bearer $ANYCHAT_TOKEN"

# List tagged people with a phone number on file
curl -s "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people?tags=webinar-2026-06&hasPhone=true&limit=50" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN"

# Create a person
curl -s https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "Jane Smith",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "productInterest": "CoolSculpting",
    "source": "webinar-2026-06",
    "customFields": { "targetMarket": "medspa" }
  }'

# Move a person along the funnel
curl -s -X PATCH \
  https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people/665f2a... \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "qualified"}'

# Export the whole CRM as CSV
curl -s -OJ https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people/export \
  -H "Authorization: Bearer $ANYCHAT_TOKEN"

# Import rows, updating matched records
curl -s https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/people/import \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "update",
    "rows": [
      { "email": "jane@example.com", "company": "Example Health", "external id": "crm-1042" }
    ]
  }'

People segments

A segment is a named, saved People filter stored on the agent. Pass its id as segmentId on the People list or export to apply it. A segment stores the filter, not its results: it matches whichever people satisfy the filter at the moment it is used.

interface PeopleSegment {
  id: string;            // per-agent slug, derived from name at create time
  name: string;          // ≤ 100 characters
  description?: string;  // ≤ 500 characters
  filter: PeopleSearchFilter;
}

filter is the query-parameter vocabulary of the People list as a JSON object: scalar parameters by name, tags as a string array, and custom-field conditions nested under cf:

{
  "name": "Webinar leads, reachable",
  "filter": {
    "tags": ["webinar-2026-06"],
    "hasPhone": true,
    "outreachReachability": "rcs",
    "cf": { "targetMarket": "medspa" }
  }
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/people/segments viewer people:read
POST /orgs/{org}/agents/{agent}/people/segments editor people:write
GET /orgs/{org}/agents/{agent}/people/segments/{segmentId} viewer people:read
PUT /orgs/{org}/agents/{agent}/people/segments/{segmentId} editor people:write
DELETE /orgs/{org}/agents/{agent}/people/segments/{segmentId} editor people:write

id is slugified from name on create and is stable through renames. POST also accepts an explicit id in the body; a value already in use on the agent is rejected with 422 (agent.people_segment.id_conflict). PUT is an upsert: it replaces the segment under the id in the path, or, when none exists, creates one under that id and returns 201 (a replace returns 200). An agent holds at most 50 segments. A body that fails validation (a missing name, a filter key outside the vocabulary, an undeclared cf key) is rejected with a 422 problem detail.

People fields

The declared People fields: the structured facts the agent tracks about each Person under customFields[key], and the keys the People list's cf. filters, POST / PATCH customFields, and import matching accept. See Settings · People fields in the People guide. This resource is the capabilities.people.fields slice of an export.

interface PersonFieldDef {
  key: string;                   // ^[a-zA-Z][a-zA-Z0-9_]*$, unique within the agent
  label?: string;                // operator- and LLM-facing label
  description?: string;          // what the field means and when to fill it
  type?: 'string' | 'number' | 'date' | 'boolean' | 'enum'; // default string
  enum?: string[];               // allowed values when type is enum
  examples?: string[];           // sample values that help the agent recognise the field
  required?: boolean;            // advisory: expected on an intake form; never blocks the agent
  intake?: { aliases?: string[] }; // source columns matching by key, label, or alias (case-insensitive)
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/people-fields viewer agents:read
PUT /orgs/{org}/agents/{agent}/people-fields editor agents:write

GET returns { "data": PersonFieldDef[] }. PUT takes { "fields": PersonFieldDef[] } and replaces the whole list; both return { "data": PersonFieldDef[] }. A body without a fields array, a field missing key, a key that fails the pattern or repeats, or a type outside the enumeration is a 422 request.invalid problem detail (errors[].field, for example fields[0].key).

Testers

The people you invite to try an agent in the tester app (test.anychat.ai) before it launches. A tester has no console access and no Member role: they sign in with a magic link the operator shares with them, fill in the same intake form a real user would (see the tester form), and talk to the agent through the channel emulator. Their conversations are test traffic. This is not the RCS test devices list on a Google-served RCS binding (see the Channels guide); that one lives with the carrier.

interface Tester {
  id: string;
  email: string;
  agentId: string;      // the path {agent}
  active: boolean;      // true once the magic link has been used at least once
  disabled: boolean;    // disabled testers keep their record; their link stops working
  invitedAt?: string;   // ISO 8601
  lastSeenAt?: string;  // ISO 8601, most recent sign-in
  updatedAt?: string;
}

interface Tester$Invited extends Tester {
  magicLink: string;    // one-time: only on invite and regenerate-link responses
}
Method Path Role Scope
GET /orgs/{org}/agents/{agent}/testers viewer agents:read
POST /orgs/{org}/agents/{agent}/testers editor agents:write
PATCH /orgs/{org}/agents/{agent}/testers/{testerId} editor agents:write
POST /orgs/{org}/agents/{agent}/testers/{testerId}/regenerate-link editor agents:write
DELETE /orgs/{org}/agents/{agent}/testers/{testerId} editor agents:write
  • GET returns { "data": Tester[] }: every tester invited to this agent, without magic links (a list never carries one).
  • POST .../testers with { "email": string } invites a tester and returns 201 Tester$Invited. The magicLink is returned exactly once; there is no email delivery today, so copy it into your own message to the tester. One tester per email per agent.
  • PATCH with { "disabled": boolean } disables (or re-enables) a tester and returns the Tester. Disabling keeps the record and the tester's conversation history; only their sign-in stops working. Use it to pause access; DELETE to drop the tester for good.
  • POST .../regenerate-link mints a fresh magic link (lost, or leaked) and returns 200 Tester$Invited. The previous link stops working immediately.
  • DELETE removes the tester and returns 204. Re-inviting the same email later creates a new tester with a new link.

Never returned by any of these: the login-token hash, the tester's private conversation identity, or the intake payload they submitted.

Errors are problem details:

Status code When
422 agent.tester.email_invalid email missing or malformed (errors[].field = email)
409 agent.tester.email_conflict that email is already a tester of this agent
422 agent.tester.disabled_required PATCH without a boolean disabled
404 agent.not_found the agent is not in the org
404 agent.tester.not_found no such tester on this agent (a tester id that belongs to another agent is a 404, not a 403)

Apply Agent Template

Stand an agent up from an Agent Template. A template is an agent document with identity omitted — the same two-layer shape as an export (a definition layer with personality, objectives, guardrails, skills, playbooks, message templates; a capabilities layer with people and outreach configuration) — so applying one imports that document through the same path as Import.

Method Path Role Scope
POST /orgs/{org}/agents/{agent}/apply-template editor templates:apply

Body:

{ "templateId": "@anychat/agent-template.sales-development" }
Response:
{
  "agent": { ... },
  "appliedFrom": { "templateId": "...", "templateName": "Sales Development Agent" },
  "applied": [
    "definition.personality",
    "definition.objectives",
    "definition.playbooks",
    "capabilities.outreach"
  ],
  "skipped": []
}

Semantics are wholesale section-replace (import semantics): every section the definition carries replaces that section on the agent; sections it omits are left untouched. The agent is stamped with templateRef (the template id and version it was applied from), which later drives Template updates. The result is validated to have exactly one default playbook. Configuration (tool bindings, secrets) is not part of a definition, so it's untouched — you wire up tools afterward.

Template updates

Applying a template copies its content onto your agent, so a later catalog edit never silently changes a live agent. When the catalog publishes a newer version of the template you started from, these endpoints surface it and let you apply it as a reviewed merge. The server compares three documents — the template version your agent came from (the base), the latest template version, and your agent's current document — and reports every difference as an item you resolve: take the template's change or keep yours.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/template-update viewer agents:read
GET /orgs/{org}/agents/{agent}/template-update/preview viewer agents:read
POST /orgs/{org}/agents/{agent}/template-update/apply editor agents:write

GET .../template-update reports whether a newer version exists:

{
  "templateRef": { "templateId": "@anychat/agent-template.sales-development", "version": "24" },
  "latestVersion": "25",
  "latestName": "Sales Development Agent",
  "updateAvailable": true,
  "baseAvailable": true
}
templateRef is null for agents not created from a template. baseAvailable: false means the original version is no longer on record; the preview then reports every difference as a conflict, since edits you made can't be told apart from template changes.

GET .../template-update/preview returns the merge items. Each has a path (the document section, e.g. definition.playbooks), a key for items inside array sections, a label, the three compared values (base, theirs, mine), and a status:

Status Meaning Default resolution
incoming the template changed this; you didn't take template
added new in the template take template
removed the template dropped this; you didn't change it take template
local you changed this; the template didn't keep yours
conflict both changed you must choose

Items identical on both sides are omitted. Content you added that never came from the template is yours alone and never appears.

POST .../template-update/apply takes the target version and your choices:

{
  "toVersion": "25",
  "resolutions": [
    { "path": "definition.playbooks", "key": "sales-discovery", "choice": "template" },
    { "path": "definition.personality", "choice": "mine" }
  ]
}
Unlisted non-conflict items get their default resolution; an unresolved conflict is a 422. A 409 means the catalog moved past toVersion while you were reviewing (fetch the preview again). On success the agent's templateRef is re-stamped to the new version and the updated agent is returned.

The retired per-piece endpoints GET .../updates and POST .../updates/apply remain routable for compatibility: the list is always empty, and apply responds 410 Gone.

Export / Import

A round-trippable, operator-meaningful snapshot of an agent. Export projects the agent into a two-layer document: a header (name, description, template lineage, llm, genericConfig), a definition layer — the authored document (identity, personality, guardrails, objectives, playbooks with their procedures, message templates) — and a capabilities layer — capability configuration (scheduling including representatives and defaultRepresentativeIdentity, people with fields and the update permission, knowledge with products, outreach, and integrations.custom — custom integration contracts). Import applies the same document back as a patch. This is the supported way to read and write playbooks in bulk, and the mechanism behind moving an agent between accounts.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/export viewer agents:read
POST /orgs/{org}/agents/{agent}/import editor agents:write

Framework v2 fields round-trip in the same document: definition.playbooks[].uses and definition.playbooks[].flow (see Playbooks), and definition.policies[] — the agent's output-check policies (see Rules and safety):

interface AgentOutputPolicy {
  id: string;                     // named in the turn record as output.<id>:rewrite | :block
  name?: string;
  kind: 'output.blocklist';       // any pattern present in a reply trips the policy
  patterns: string[];             // plain words/phrases (case-insensitive, word-bounded) or '/regex/flags'
  action?: 'rewrite' | 'drop';    // rewrite (default): ask the model once, then fall back; drop: no rewrite
  fallbackText?: string;          // sent instead of a reply that still trips; omitted → the reply is dropped
  enabled?: boolean;              // default true
}

The header carries the agent's framework, which is informational on import (the target agent's runtime is not changed — switch it with PATCH on the agent); a document from an agent on the other framework still applies its shared sections and the response notes say so.

export returns YAML by default (the document reads top-to-bottom in the console's information-architecture order); pass ?format=json for JSON. The document never contains secrets or account-specific state: tool bindings (their config can carry credentials), channel bindings, OAuth connections (including scheduling.googleCalendar), knowledge documents, and runtime state are all excluded.

import accepts the same shape (as a JSON body) and applies only the sections present — unset sections are left untouched; a section present with an empty value clears it. Server-owned keys (id, framework, secrets, timestamps) are ignored. Documents in the retired flat format (pre-August 2026, section keys at the top level) are rejected with a 422 asking for a re-export. Validation mirrors the individual resources (e.g. exactly one default playbook, unique ids, known uses ids), and imported flows are linted the same way a Playbook write is — a flow.* error in the document is a 422 (agent.playbook.flow_invalid) listing the findings. The target agent's own Google Calendar connection, if any, is preserved across an import. Responds with the updated agent plus what happened to each document path:

{
  "agent": { ... },
  "applied": ["name", "definition.playbooks", "capabilities.scheduling"],
  "skipped": ["id"],
  "notes": []
}

Lint

Definition lint is a pure check over the agent's document (plus its capability configuration and framework) that returns findings: is the document internally consistent? It is the static counterpart of readiness ("is the capability set up?") and shares its finding shape, so the console's Readiness card shows both in one list. The same rules run on both frameworks; the structural rules also run inline on every document write (an error there rejects the write with 422), on import, and when a template is applied.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/lint viewer agents:read
interface AgentLintResponse { findings: LintFinding[] }

interface LintFinding {
  severity: 'error' | 'warning' | 'info';
  code: string;        // stable rule id, e.g. 'playbook.uses.unknown'
  path: string;        // JSON pointer into the agent document, e.g. '/playbooks/0/uses/1'
  message: string;     // operator-facing
  fix?: {
    label: string;                 // e.g. 'Declare the capability'
    route?: string;                // console path, relative to the agent, that fixes it
    apply?: {                      // a one-click patch, when the fix is mechanical
      kind: 'playbook.uses';
      playbookId: string;
      uses: string[];              // the list to save as the Playbook's `uses`
    };
  };
}

Rule families and codes:

Family Codes Severity
Structural (rejects the write) playbook.uses.unknown, playbook.messageTemplateRef.unknown, playbook.outreach.openingTemplate.unknown, playbooks.default.missing, playbooks.default.multiple, playbook.id.duplicate, messageTemplate.id.duplicate error
Consistency (the two-way uses check) playbook.uses.missing — the Playbook declares uses but its body talks about a capability not in the list (declared but incomplete); playbook.uses.suggest — a v2 Playbook with no uses at all, with the list to declare in fix.apply; playbook.uses.unused — a declared capability the body never draws on warning / info / info
Content messageTemplate.slot.unknown, playbook.whenToUse.empty, media.ref.unknown (an image:<id> reference to an item not in the media library — the message still sends, without the picture) warning
Framework-compat v2.ignored.skills — v1-only constructs a v2 agent ignores (a playbook's toolRefs / procedure requiredTools / derivedFrom are not reported separately: they fold into that playbook's playbook.uses.suggest finding); v1.ignored.uses — v2 fields on a generic agent warning / info
Flow (structural) flow.action.capability.missing, flow.action.tool.unknown, flow.template.unknown, flow.bind.unknown, flow.slot.unknown, flow.branch.target.unknown, flow.handoff.unknown, flow.call.unknown, flow.call.noflow, flow.enum.options.missing, flow.timeslot.source.missing, flow.end.missing, flow.predicate.invalid, flow.predicate.option.unknown, flow.say.suggestions.invalid, flow.step.unknown error
Flow (advisory) flow.bind.notes (a slot bound to person.notes — a destructive sink; declare a People field instead), flow.slot.prompt.missing, flow.step.unreachable, flow.outcome.style, flow.body.stale (the Playbook body changed since the path was generated) warning

The prose-to-uses consistency rule is keyword-based; on generic agents it runs only for Playbooks that already declare uses, so live v1 agents are not nagged about a field their runtime ignores. Findings are computed on each read; the response has no page token.

{
  "findings": [
    {
      "severity": "info",
      "code": "playbook.uses.suggest",
      "path": "/playbooks/2/uses",
      "message": "Schedule a call draws on scheduling and people but declares no capabilities; it currently gets all of them.",
      "fix": {
        "label": "Declare scheduling, people",
        "apply": { "kind": "playbook.uses", "playbookId": "schedule-call", "uses": ["scheduling", "people"] }
      }
    }
  ]
}

Catalogs

Read-only system catalogs of starter content. Available to any authenticated caller; org-agnostic (the same content for everyone). Every kind supports a list and a single-item read. There is no role floor; the only requirement is the catalogs:read scope, which every token holds by default.

Method Path Role Scope
GET /catalogs/{kind} catalogs:read
GET /catalogs/{kind}/{id} catalogs:read

{kind} is one of the values below; any other path is a 404. {id} is the item's qualified id (URL-encoded, see Identifiers) or, for the static manifests, its short key such as rcs; an unknown id is a 404 resource.not_found problem detail.

Kind Returns
agent-templates AgentTemplate[]
playbook-templates PlaybookTemplate[]
skill-templates SkillTemplate[]
flows FlowTemplate[]: catalog Flows, deterministic conversation paths with typed slots and steps
objective-templates ObjectiveTemplate[]
personality-templates PersonalityTemplate[]
guardrail-templates GuardrailTemplate[]
tools Tool[]
channels channel manifest: per-channel setup forms for bindings — managed business-input fields, unmanaged provider credential-form schemas, provisioning copy
log-events log-event manifest (static)
webhook-events webhook-event manifest (static)

List responses are enveloped as { "kind": "...", "data": [ ... ] } and are not paginated. Single-item reads return the item directly.

interface AgentTemplate {
  id: string;                                  // e.g. @anychat/agent-template.sales-development
  name: string;
  description: string;
  category?: string;
  framework?: string;
  version?: string;                            // integer string; the catalog serves the latest version
  // The template's whole document, inlined in the two-layer export
  // shape: a `definition` layer (personality, guardrails, objectives,
  // playbooks with their procedures, message templates) and a
  // `capabilities` layer (people, outreach, …), with identity omitted.
  // apply-template is a plain import of this document onto the agent.
  definition: Record<string, unknown>;
}

interface PlaybookTemplate {
  id: string; name: string; description: string;
  whenToUse?: string;                          // activation context
  body: string;                                // prompt fragment
  examples?: { role: 'user' | 'agent'; content: string }[][];
  derivedSkillTemplateIds?: string[];          // skills this Playbook draws on
  isDefault?: boolean;
  isOutbound?: boolean;                        // the outreach default
}

interface SkillTemplate {
  id: string; name: string; description: string;
  body: string;                                // prompt fragment for the agent
  requiredTools?: string[];                    // catalog tool ids
}

interface FlowTemplate {
  id: string; name: string; description: string;
  version?: string;
  whenToUse?: string;                          // activation context
  guard?: string;
  slots: unknown[];                            // typed inputs the flow collects
  steps: unknown[];                            // ordered steps; shape varies by step kind
  onComplete?: 'return' | 'end';
  allowDigressions?: boolean;
  isDefault?: boolean;
}

interface ObjectiveTemplate {
  id: string; name: string; description: string;
  body: string;                                // prompt fragment for the agent
}

interface PersonalityTemplate {
  id: string; name: string; description: string;
  promptFragment: string;
}

interface GuardrailTemplate {
  id: string; name: string; description: string;
  topicsToAvoid?: string[];
  claimsNotToMake?: string[];
  requiredDisclaimers?: string[];
}

interface Tool {
  id: string; name: string; description: string;
  category: string;
  parameters: Record<string, unknown>;         // describes how the agent calls the tool
  configSchema?: Record<string, unknown>;      // shape PUT into AgentToolBinding.config
}

Every template carries a human-readable description distinct from its body / promptFragment / list contents. UIs typically show the description by default and reveal the prompt body behind an advanced toggle — the underlying text is rarely something an operator needs to edit.

Messages, conversations, users, and logs

A read-only window into a single agent's traffic and runtime events.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/messages viewer messages:read
GET /orgs/{org}/agents/{agent}/conversations viewer messages:read
GET /orgs/{org}/agents/{agent}/users viewer messages:read
GET /orgs/{org}/agents/{agent}/logs viewer messages:read

Shared query parameters (all optional):

Param Default Applies to
start, end last 24 hours all
limit 200 (max 1000) all
userId messages, conversations, logs
sessionId messages, logs
origin messages (user / agent)
type messages (content / status / action)
level logs (debug / info / warn / error)
event logs (e.g. framework.dispatched, llm.call)
pageToken messages, logs
includeTest false messages, conversations, users

start and end accept ISO-8601 timestamps or epoch-millis strings.

Test traffic. Conversations conducted through the channel emulator or by an identified tester are marked as test traffic and excluded by default from messages, conversations, and users results. Pass includeTest=true to include them; included entries carry test: true and, for emulator conversations, emulatedChannel — the channel the emulator was rendering (rcs, amb, whatsapp, sms) — which is the channel you should display (the transport channel is the synthetic webchat surface).

Returned shapes:

interface AgentMessageEntry {
  timestamp: string;       // ISO-8601
  agentId: string;
  userId: string;          // channel-side user id
  sessionId: string;       // 30-min conversation bucket
  origin: 'user' | 'agent';
  type:   'content' | 'status' | 'action';
  subtype?: string;        // text / image / card / receipt / dial / ...
  channel: string;         // slack / viber / instagram / webchat / ...
  text?: string;           // best-effort plain-text extraction
  messageId: string;       // Anychat-assigned id
  channelMessageId?: string;
  channelConversationId?: string;
  error?: string;          // delivery failure (outbound only)
  raw: unknown;            // full underlying payload
  test?: boolean;          // test traffic (emulator / tester)
  emulatedChannel?: string; // channel the emulator rendered (rcs / amb / ...)
}

interface AgentConversationSummary {
  sessionId: string;
  userId: string;
  channel: string;
  startedAt: string;       // ISO-8601
  endedAt: string;         // ISO-8601
  messageCount: number;
  firstMessagePreview?: string;
  test?: boolean;          // test traffic (emulator / tester)
  emulatedChannel?: string;
}

interface AgentUserSummary {
  userId: string;
  channel: string;
  lastSeenAt: string;      // ISO-8601
  messageCount: number;
  test?: boolean;          // test traffic (emulator / tester)
  emulatedChannel?: string;
}

interface AgentLogEntry {
  timestamp: string;
  agentId: string;
  level: 'debug' | 'info' | 'warn' | 'error';
  event: string;           // dot-namespaced, e.g. framework.dispatched
  framework?: string;
  userId?: string;
  sessionId?: string;
  channel?: string;
  data?: unknown;
  error?: { message: string; stack?: string };
  raw: unknown;
}

The raw field on each entry exposes the full underlying payload — useful for power UIs and debugging, optional for typical clients.

Turns

The turn record of a Framework v2 agent: one row per runtime turn (inbound event → reply) with a trace of what the runtime did — which Playbook was chosen and why, whether the model was called, which tools were offered and called, which policies fired, the flow steps that ran, latency and usage. It is the read behind the console's Inspect turns view on a conversation and answers "why did it say that?" (see How a turn works). Rows exist only for agents on framework: "v2" — a generic agent returns an empty page — and are kept for 90 days.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/turns viewer messages:read
Param Default Notes
userId One person, by gateway ASID (v2-…)
since, until ISO-8601 timestamps or epoch-millis strings; inclusive
limit 100 1–500; the newest N matching turns, still returned ascending
includeHistory false Include each turn's historyEntries (the conversation-history delta it appended)

Returns { "data": AgentTurnRecord[] }, ascending by createdAt; there is no page token. Malformed since, until, or limit values are a 400 (request.invalid).

interface AgentTurnRecord {
  id: string;
  agentApplicationId: string;
  userId: string;                  // gateway ASID (v2-…); never the channel-native address
  conversationId?: string;         // absent on an outreach opening turn (traced before the conversation opens)
  turnIndex: number;               // history length at turn start; a restart re-uses low indexes — order by createdAt
  framework: 'v2';
  kind: 'text' | 'action' | 'conversationStarted' | 'restart' | 'result';
  channel?: string;                // rcs / sms / webchat / …
  provider?: string;               // rbm / twilio / webexConnect / …
  createdAt: string;               // ISO-8601
  trace: AgentTurnTrace;
  historyEntries?: unknown[];      // only with includeHistory=true; opaque runtime history format
}

// Every member is optional — a turn a policy short-circuited never
// reached the model. Treat unknown members additively.
interface AgentTurnTrace {
  router?: {
    activePlaybook: string;
    decidedBy: 'default' | 'model' | 'sticky' | 'unavailable';  // how the Playbook was chosen
    reason?: string;
  };
  model?: { provider: string; model: string; iterations: number; terminatedBy?: string };
  toolsOffered?: string[];
  toolsCalled?: string[];          // a '!denied' suffix marks a call a policy refused
  policiesFired?: string[];        // e.g. 'scheduling.cancel.explicit-intent:deny', 'output.no-diagnosis:rewrite'
  resources?: { id: string; tokens?: number }[];   // context assembled into the prompt
  response?: unknown;              // the raw structured reply before channel rendering
  warnings?: string[];
  flow?: string[];                 // flow-executor decisions (flow-backed Playbooks only)
  latencyMs?: number;
  usage?: { inputTokens?: number; outputTokens?: number; costUsd?: number };
}

kind is the inbound event that started the turn: text (a message), action (a tap or channel-side intent), conversationStarted (the channel opened a conversation with no user text — a webchat first open or an outreach opening), restart, or result (a typed result round-trip such as a picker answer). Turn records reference gateway message ids rather than duplicating the transcript; join to messages by userId and time.

# The last 20 turns of one person's conversation, with the history each turn appended
curl "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/turns?userId=v2-8f3a…&limit=20&includeHistory=true" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN"

Activity

The agent's activity feed: operator-facing events with a pre-rendered one-line summary, newest first. Today the feed carries the scheduling lifecycle (appointment.created, appointment.rescheduled, appointment.cancelled); further event types will join the same shape.

Method Path Role Scope
GET /orgs/{org}/agents/{agent}/activity viewer messages:read
Param Default Notes
limit 50 1–200; out-of-range values are clamped, non-numeric values fall back to the default

Returns { "data": AgentActivityEvent[] }; there is no page token.

interface AgentActivityEvent {
  id: string;                     // stable per-agent event id
  type: 'appointment.created' | 'appointment.rescheduled' | 'appointment.cancelled';
  at: string;                     // ISO-8601, when the event occurred
  summary: string;                // operator-facing one-liner
  appointmentId: string;
  personId?: string;
  personName?: string;
  when?: string;                  // ISO-8601 start of the call
  prospectTimezone?: string;      // IANA
  representativeName?: string;
  fromTime?: string;              // rescheduled: the prior start time, ISO-8601
  reason?: string;                // cancelled: optional reason
}

Org

A curated read of the org's own metadata, plus a small preferences patch. Operational and billing configuration is not exposed here.

interface Org {
  id: string;
  name: string;
  description?: string;
  timeZone?: string;       // IANA
  features: string[];
  products: string[];
  createdAt?: string;      // ISO-8601
  updatedAt?: string;      // ISO-8601
}
Method Path Role Scope
GET /orgs/{org} viewer
PATCH /orgs/{org} admin org:admin

GET is deliberately scope-ungated so any token can read the metadata of its own org. PATCH accepts any of { "name", "description", "timeZone" } and rejects everything else; a body with no editable fields returns 400.

API keys

Mint and manage the org's API keys. All five endpoints require the admin role and the org:admin scope.

interface ApiKey {
  id: string;
  name?: string;
  description?: string;
  prefix?: string;         // display prefix, e.g. sk-any-api01-Ab12Cd
  scopes: string[];        // issuer-chosen scope set
  expiresAt?: string;      // ISO-8601, optional automatic expiry
  lastUsedAt?: string;     // ISO-8601, best-effort
  createdAt?: string;      // ISO-8601
  updatedAt?: string;      // ISO-8601
}
Method Path Role Scope
GET /orgs/{org}/api-keys admin org:admin
POST /orgs/{org}/api-keys admin org:admin
GET /orgs/{org}/api-keys/{keyId} admin org:admin
PATCH /orgs/{org}/api-keys/{keyId} admin org:admin
DELETE /orgs/{org}/api-keys/{keyId} admin org:admin

Create body (name is required):

{ "name": "CI deploys", "description": "Used by the deploy pipeline",
  "scopes": ["agents:write", "people:read"], "expiresAt": "2027-01-01T00:00:00Z" }

The 201 create response is the only place the raw key value ever appears:

{ "id": "…", "name": "CI deploys", "prefix": "sk-any-api01-Ab12Cd",
  "scopes": ["agents:write", "people:read"],
  "key": "sk-any-api01-…full secret, shown once…" }
Store it immediately — every subsequent read (list / get / patch) returns only prefix + metadata. Anychat stores a hash, not the key, so a lost key cannot be recovered; mint a new one.

PATCH accepts any of { "name", "description", "scopes", "expiresAt" }. DELETE removes the key outright (204) and immediately revokes it.

Webhooks

Register org-level webhook endpoints for control-plane events. All endpoints require the admin role and the webhooks:manage scope. The available event names are listed by the webhook-events catalog.

interface Webhook {
  id: string;
  url: string;
  events: string[];        // subscribed event names; [] = none
  active: boolean;
  description?: string;
  signingSecretPrefix?: string;
  lastDeliveryAt?: string;             // ISO-8601
  lastDeliveryStatus?: 'success' | 'failure';
  createdAt?: string;      // ISO-8601
  updatedAt?: string;      // ISO-8601
}
Method Path Role Scope
GET /orgs/{org}/webhooks admin webhooks:manage
POST /orgs/{org}/webhooks admin webhooks:manage
GET /orgs/{org}/webhooks/{webhookId} admin webhooks:manage
PATCH /orgs/{org}/webhooks/{webhookId} admin webhooks:manage
DELETE /orgs/{org}/webhooks/{webhookId} admin webhooks:manage
POST /orgs/{org}/webhooks/{webhookId}/_rotate admin webhooks:manage

Create body (url is required; active defaults to true):

{ "url": "https://example.com/anychat-events",
  "events": ["agent.updated"], "description": "Ops bus" }

The create and _rotate responses carry the raw signingSecret — the only places it ever appears; reads show signingSecretPrefix only. Use the secret to verify that deliveries to your URL came from Anychat; _rotate invalidates the old secret and issues a fresh one.

PATCH accepts any of { "url", "events", "description", "active" } (set active: false to pause deliveries without losing the registration). DELETE removes the registration (204).


A worked example

Spin up an agent from scratch using the Sales Development Agent template:

# 1. Create the agent.
curl -s https://api.anychat.ai/v1/orgs/acme/agents \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme SDR"}'
# → { "id": "agent-acme-sdr-pj7r2qx4", ... }

# 2. Apply the Sales Development template (brings in playbooks with their procedures, objectives, …).
curl -s https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/apply-template \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "@anychat/agent-template.sales-development"}'
# → { "agent": {...}, "addedToolBindings": ["@anychat/tool.google-calendar"] }

# 3. Inspect what got applied — including playbooks — via export.
curl -s "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/export" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN"
# → two-layer YAML snapshot: definition (identity, personality, playbooks[], …) + capabilities

# 4. Configure the Google Calendar tool binding the template added.
curl -s -X PUT \
  "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/tool-bindings/%40anychat%2Ftool.google-calendar" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"config": {"calendarId": "sdr-team@acme.com"}}'

# 5. Kick off the OAuth grant for the calendar.
curl -s "https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/tools/%40anychat%2Ftool.google-calendar/oauth/start" \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"returnTo": "https://yourapp.example.com/agents/acme-sdr/tools"}'
# → { "authorizationUrl": "https://accounts.google.com/o/oauth2/..." }
# Open that URL in the operator's browser to complete the grant.

# 6. Add a custom guardrail.
curl -s -X PUT https://api.anychat.ai/v1/orgs/acme/agents/agent-acme-sdr-pj7r2qx4/guardrails \
  -H "Authorization: Bearer $ANYCHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"topicsToAvoid": ["competitor pricing"], "claimsNotToMake": ["any guarantees about renewal terms"]}'

Once the OAuth grant completes, the agent is ready to accept traffic on whichever messaging channels are bound to it.