Skip to content

Agent API

Build, configure, and inspect your Anychat agents programmatically.


The Agent API is the HTTP interface for managing agents on the Anychat platform — the 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

https://api.anychat.ai/v1

All paths in this document are relative to that root.

Authentication

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

Authorization: Bearer <token>

Two kinds of bearer token work:

  • API keys — strings starting sk-any-…, issued from the console's 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:writeagents:read, people:writepeople:read) — grant only the broadest scope you intend to use.
  • catalogs:read is granted to every token by default.
  • User session tokens hold the scope set implied by the user's role: viewer holds the read scopes; editor adds the write scopes and templates:apply; admin and owner add webhooks:manage and org:admin.
  • A request whose token lacks the route's scope receives 403 (auth.scope_missing).

Identifiers

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

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

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

Responses and errors

  • List endpoints return an envelope with a data array:

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

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

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

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

  • A successful DELETE on 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:

{ "name": "Acme Bot", "id": "acme-bot", "identity": { "websiteUrl": "https://acme.com" } }
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:

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

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

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

Objectives

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

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

Skills

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:

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

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

Playbooks

A Playbook is a named mode of operation — the situated plan the agent follows in a given kind of moment (greeting, scheduling, product questions, …). Every agent has exactly one Default Playbook (isDefault: true) and at most one 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:

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

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

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

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

OAuth-runtime tools

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

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

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

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

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

Status reports the current credential state for the binding:

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

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

Guardrails

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

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

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

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

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

Message templates

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

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

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

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

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

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:

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

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

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

Create body:

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

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

Human escalation

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

Channels

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

Two ways to connect a channel:

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

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

interface ChannelBinding {
  id: string;
  agentId: string;
  channel: ChannelName;
  provider?: string;        // unmanaged only — you chose it
  mode: 'managed' | 'unmanaged';
  status: ChannelBindingStatus;
  statusDetail?: string;    // operator-safe status explanation
  events?: Array<{          // lifecycle history, oldest first
    at: string;
    from?: ChannelBindingStatus;
    to: ChannelBindingStatus;
    note?: string;
    actor: 'operator' | 'sysop' | 'system';
  }>;
  routingId?: string;       // provider-side id; display only
  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:

{ "templateId": "@anychat/agent-template.sales-development" }
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…" }
Store it immediately — every subsequent read (list / get / patch) returns only prefix + metadata. Anychat stores a hash, not the key, so a lost key cannot be recovered; mint a new one.

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

Webhooks

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

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

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

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

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

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


A worked example

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

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

# 2. Apply the Sales Development template (brings in playbooks, 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.