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 same building blocks described in the
Agent Framework overview (identity, personality,
objectives, guardrails, skills, playbooks, tools, …) exposed as a set of
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 Settings → 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.
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 |
people:read / people:write |
The per-agent People CRM |
knowledge:read |
Knowledge-base documents |
messages:read |
Messages, conversations, users, logs, activity |
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).
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: -
A successful
DELETEon a tool binding returns 204 No Content. All other writes return the updated resource.
Resources¶
The Agent API mirrors the vocabulary of the Agent Framework. The following sections cover each building block of an agent, the shape of its data, and the endpoints that read and write it.
Most building blocks 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
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 and identity are optional; if id is omitted, Anychat mints a
slug from name. Once set, id is immutable.
PATCH accepts any of { "name", "description", "identity" }.
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¶
Named instruction fragments the agent uses to pursue its objectives. You can author skills directly or import them from the catalog (which also auto-creates empty tool bindings for any catalog tools the skill needs).
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 outbound Playbook
(isOutbound: true, the outreach opener); the runtime
switches between Playbooks mid-conversation via the switch_playbook
tool. 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.
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 }[][];
skillRefs?: string[]; // ids of the agent's skills this Playbook uses
toolRefs?: string[]; // catalog tool ids this Playbook may invoke
messageTemplateRefs?: string[];// ids of message templates this Playbook may deploy
isDefault: boolean; // exactly one true per agent
isOutbound?: boolean; // at most one true — the outreach opener
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.
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. To update a catalog-derived Playbook to the latest template
version, use the updates endpoint with kind: "playbook".
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
}
| 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:
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.
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 |
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
}
Outreach¶
Configures the agent's outreach greeting — the templated first message sent when the agent initiates a conversation. See Outreach in the framework overview.
interface OutreachConfig {
greetingTemplateId?: string; // id of one of the agent's message templates
}
| 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 body is { "greetingTemplateId": "outreach-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. 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.
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
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).
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¶
A per-agent CRM. Anychat keeps one Person record per end-user per agent and updates it as conversations reveal new facts.
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>;
assignedRepresentativeId?: string; // a representative on the agent
journal?: PersonJournalEntry[]; // read-only memory log
// …plus channel + scheduling 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';
representativeName?: string;
representativeTitle?: string;
notes?: string;
history: Array<{
at: string; // ISO-8601
action: string; // created / rescheduled / cancelled
from?: string;
to?: string;
reason?: string;
}>;
createdAt?: 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 |
POST and PATCH accept the same operator-meaningful fields (name,
contact details, company/title, status, source, notes, custom fields,
scheduled-call time/timezone) — on create the person is bound to the
agent in the path. Channel identity and outreach state are runtime-owned
and cannot be set. The journal is read-only — the agent appends to it
as it learns, including a one-line summary each time a conversation ends.
DELETE removes the person (204). appointments lists the person's
scheduled calls (newest first, including cancelled ones) as
{ "data": Appointment[] }.
Apply Agent Template¶
Stand an agent up from an Agent Template. A template is an agent definition with identity omitted (its whole behavior bundle — personality, objectives, guardrails, skills, playbooks, outreach — lives inline), so applying one imports that definition 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": ["personality", "objectives", "skills", "playbooks", "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. Applied content is stamped with derivedFrom
(per-piece) and templateRef (agent-level), so you can later detect a
newer version upstream (see 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.
Updates¶
Importing a catalog piece copies its content and pins your agent's copy, so a later catalog edit never silently changes a live agent. These endpoints surface the opt-in upgrade path.
| Method | Path | Role | Scope |
|---|---|---|---|
GET |
/orgs/{org}/agents/{agent}/updates |
viewer | agents:read |
POST |
/orgs/{org}/agents/{agent}/updates/apply |
editor | agents:write |
GET .../updates reports, per catalog-derived piece (personality,
objective, skill, playbook), whether a newer version is available:
{
"data": [
{
"kind": "skill",
"id": "call-booking",
"name": "Schedule a Call",
"templateId": "@anychat/skill-template.call-booking",
"currentVersion": "1",
"latestVersion": "2",
"updateAvailable": true,
"templateMissing": false
}
],
"updateCount": 1
}
POST .../updates/apply with { "kind": "skill", "id": "call-booking" }
re-copies that one piece's current catalog content and re-stamps its
version, preserving your agent-local state (id, priority, enabled,
isDefault). kind: "personality" needs no id. Pinned-everywhere stays
the default; updates are explicit and per-piece.
Export / Import¶
A round-trippable, operator-meaningful snapshot of an agent. Export projects the agent down to the same buckets the console exposes (identity, personality, objectives, skills, playbooks, tool bindings, guardrails, products, message templates, outreach, …); import applies an export-shaped document back as a patch. This is the supported way to read and write playbooks in bulk.
| Method | Path | Role | Scope |
|---|---|---|---|
GET |
/orgs/{org}/agents/{agent}/export |
viewer | agents:read |
POST |
/orgs/{org}/agents/{agent}/import |
editor | agents:write |
export returns YAML by default (the document reads top-to-bottom in
the console's information-architecture order). import accepts the same
shape and applies only the sections present — unset sections are left
untouched. Validation mirrors the individual resources (e.g. exactly one
default playbook, unique ids).
Catalogs¶
Read-only system catalogs of starter content. Available to any
authenticated caller; org-agnostic (the same content for everyone).
Every kind supports GET /catalogs/{kind} (list) and
GET /catalogs/{kind}/{id} (one item). There is no role floor; the
only requirement is the catalogs:read scope, which every token
holds by default.
| Kind | Returns |
|---|---|
agent-templates |
AgentTemplate[] |
playbook-templates |
PlaybookTemplate[] |
skill-templates |
SkillTemplate[] |
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": [ ... ] }.
interface AgentTemplate {
id: string; // e.g. @anychat/agent-template.sales-development
name: string;
description: string;
category?: string;
identityPreamble?: string;
playbookTemplateIds?: string[]; // imported as the agent's playbooks
personalityTemplateId?: string;
objectiveTemplateIds?: string[];
guardrailTemplateIds?: string[];
skillTemplateIds?: string[];
recommendedToolIds?: string[];
}
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 opener
}
interface SkillTemplate {
id: string; name: string; description: string;
body: string; // prompt fragment for the agent
requiredTools?: string[]; // catalog tool ids
}
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.
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, skills, 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"
# → YAML snapshot: identity, personality, playbooks[], objectives[], skills[], …
# 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.