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¶
All paths in this document are relative to that root.
Authentication¶
Attach a bearer token in the Authorization header on every request:
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:write⊃agents:read,people:write⊃people:read) — grant only the broadest scope you intend to use. catalogs:readis 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 addwebhooks:manageandorg: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
Long lists are paginated with an opaquedataarray: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
statusCodeand a human-readablemessage: -
Newer endpoints (noted in their sections below) return problem details instead: an
application/problem+jsonbody with the HTTPstatus, a stable machine-readablecodesuch asrequest.invalidorresource.not_found, a human-readabletitle, an optionaldetail, and, for validation failures, anerrorsarray whose entries name the offendingfieldpath:Authorization failures on every endpoint use{ "status": 422, "code": "request.invalid", "title": "Validation failed", "errors": [{ "field": "fields[0].key", "message": "must match ^[a-zA-Z][a-zA-Z0-9_]*$" }] }auth.forbidden(role floor not met) andauth.scope_missing(token lacks the scope). -
A successful
DELETEreturns 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:
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. v2requires the org featureframework-v2(a preview entitlement, sysop-set). Creating an agent withframework: "v2", or switching one to it, without the feature returns403(agent.framework.not_entitled).genericis 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:
The imported personality recordsderivedFrom 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 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._reordersets priority by position; body is the ordered id list: Every id must already exist on the agent; any omitted objectives keep their relative order after the listed ones.- Item-level
PATCH/PUT/DELETEedit a single objective by id. - Import appends a single objective derived from a system
Objective Template:
prioritydefaults 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:
Response: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
v2the 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 nousesgets every ready capability (the lenient default) — lint then suggests a declaration (playbook.uses.suggest) so the Playbook's reach is explicit. - On
genericusesis informative only (the v1 runtime resolves tools from procedures'requiredTools); lint reports it asv1.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:
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:
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:
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
}
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 person — steps
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:
preview returns 404 when no greeting template is configured.
Outreach links¶
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
statusandeventstell 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.
authis 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:
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 |
GETreturns{ "data": Tester[] }: every tester invited to this agent, without magic links (a list never carries one).POST .../testerswith{ "email": string }invites a tester and returns201Tester$Invited. ThemagicLinkis 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.PATCHwith{ "disabled": boolean }disables (or re-enables) a tester and returns theTester. Disabling keeps the record and the tester's conversation history; only their sign-in stops working. Use it to pause access;DELETEto drop the tester for good.POST .../regenerate-linkmints a fresh magic link (lost, or leaked) and returns200Tester$Invited. The previous link stops working immediately.DELETEremoves the tester and returns204. 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:
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" }
]
}
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…" }
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.