Skip to content

Messaging API (AMF/1)

One message format for every rich messaging channel.


The Anychat Messaging API lets agents send and receive rich messages on RCS, Apple Messages for Business, SMS, web chat, and other channels — without formatting messages differently per channel, or even knowing which channel the user is on.

The wire format is AMF (spec: "amf/1"). Its core promise:

Every valid message renders on every channel — using the richest native affordance the channel offers, degrading deterministically down to plain SMS text, and never silently dropping content.

There are two ways to produce messages:

  • AML (Anychat Markup Language) — plain text with lightweight [[...]] markers, designed for LLM agents. See the AML reference. If you're building an LLM-backed agent, start here: return text/anychat-aml from your webhook and you're done.
  • AMF JSON — the structured format documented on this page, for full programmatic control.

Set up

You need an Anychat account and an agent. Two secrets matter: an access token (authenticates your API calls) and a signing secret (verifies webhooks came from Anychat). Both live in Agent settings → Webhook in the console.

Warning

Keep them secret, keep them safe! Access tokens and signing secrets are sensitive credentials.

Authentication

The access token is per-agent: every /v4 request authenticates as one agent and operates only on that agent's conversations. Send it as a bearer token:

Authorization: bearer <access token>

An access_token query parameter is also accepted (and takes precedence) for clients that can't set headers. A missing or unrecognized token gets a 401. There are no scopes or roles on this surface — the token is the agent.

The message model

An outbound message is an ordered list of parts, plus optional message-level chips (quick replies):

{
  "spec": "amf/1",
  "parts": [
    { "type": "text", "text": "Happy to set that up." },
    {
      "type": "ask.time",
      "intentId": "booking",
      "prompt": "When works best?",
      "slots": [
        { "id": "tue-345", "start": { "iso": "2026-06-10T15:45:00-07:00", "tz": "America/Los_Angeles" } },
        { "id": "wed-10", "start": { "iso": "2026-06-11T10:00:00-07:00", "tz": "America/Los_Angeles" } }
      ]
    }
  ]
}

Parts come in three layers:

Intents (preferred)

Intent parts state what you're trying to accomplish. The gateway picks the best rendering per channel and guarantees a degrade chain down to SMS text:

Part What it does Rich rendering SMS floor
ask.choice Ask the user to pick an option List picker (AMB) / chips (RCS) Numbered list, "Reply 1 or 2 — or just type your answer"
ask.time Offer appointment slots Time picker (AMB) / slot chips Numbered list
confirm Yes/no confirmation Receipt card + chips "Reply YES to confirm"
showcase Present products/options Carousel with Select chips (single items render as a plain card, no Select) Numbered text digest
ask.location Request the user's location Share-location chip "Reply with your address"
share.event Send a calendar event Add-to-calendar chip Calendar link
share.place Send a location Map chip Maps link
share.contact Send contact details Contact text Contact text

A single option never renders as a picker. Native sheets and lists require something to choose between — an intent with exactly one option renders as a reply chip on chip-capable channels, and on the SMS floor becomes a sentence ("Tue 3:45 PM — reply YES to take it, or just tell me what works"), where YES accepts the lone option.

Intents are round-trip. Whether the user taps an Apple time picker, taps an RCS chip, or texts "2" on SMS, your webhook receives the same typed result event:

{
  "type": "result",
  "result": {
    "intent": "ask.time",
    "intentId": "booking",
    "optionIds": ["wed-10"],
    "labels": ["Wed, Jun 10 at 10:00 AM PDT"],
    "via": "text",
    "text": "2"
  }
}

Free-text answers are matched deterministically, in confidence order: exact ordinal, option id, or label; YES/NO for confirm (YES also accepts the option of a single-option intent); then unambiguous approximate matches — an option's label contained in the reply ("Wed 10 AM works for me"), or every word of the reply appearing in exactly one option's label ("10 am wed"). A reply that could mean more than one option, or doesn't clearly match any, is delivered to you as a normal text message — Anychat never eats user input it isn't sure about.

Presentation parts

For explicit control: text (a CommonMark subset — bold, italic, strikethrough, links, lists — transcoded or stripped per channel), media, card, and carousel. Cards and carousels carry per-card chips.

{ "type": "card", "card": {
    "title": "Our clinic", "description": "Open 9–5",
    "mediaUrl": "https://example.com/clinic.jpg",
    "chips": [ { "label": "Directions", "kind": "url", "url": "https://maps.example.com" } ]
} }

Native escape hatch

{ "type": "native", "channel": "appleMessages", "payload": { ... }, "fallback": [ ...parts ] } passes a channel-specific payload through verbatim on its channel. fallback is required — a native part renders everywhere.

Chips

Chips are one-tap affordances: reply (default), url, dial, calendar, viewLocation, shareLocation. A tapped reply chip produces a result event carrying the chip's value. On channels without tap affordances, reply chips become a numbered text list (still answerable, still a result event) and action chips degrade to links/text. Label limits are per-channel; Anychat truncates rather than rejects.

Typed values

Dates, places, events, and money are structured — { "iso": "...", "tz": "America/Los_Angeles" }, { "amount": "12.00", "currency": "USD" } — and Anychat formats them per channel and locale. Your agent never formats a datetime for SMS.

Receiving events

Anychat delivers one event per webhook request:

{
  "spec": "amf/1",
  "instanceId": "<your agent instance>",
  "userId": "<conversation user id>",
  "event": {
    "spec": "amf/1",
    "eventId": "...",
    "timestamp": "2026-06-10T15:45:12Z",
    "conversation": { "userId": "...", "sessionId": "..." },
    "context": {
      "channel": "rcs",
      "provider": "googleRbm",
      "capabilities": { ... },
      "window": { "state": "open" }
    },
    "event": { "type": "message", "message": { "text": "Hi!" } }
  },
  "mode": "managed"
}

Event types:

type meaning
message Free-form user content: text, media, or a shared place
result A resolved answer to one of your intents (or a reply-chip tap)
action A non-answer tap: the user opened a url, started a dial call, saved a calendar event
status Delivery lifecycle of a message you sent: queued / sent / delivered / read / failed — always referencing your messageId
system Conversation lifecycle: conversationStarted, optIn, optOut, subscribed, unsubscribed, typing, windowChanged

context.capabilities describes what the user's channel can render. Most agents should ignore it — that's the point of the format — but it's there for the few that want to branch, and for tooling.

Opt-out is enforced by the platform. STOP/UNSUBSCRIBE on carrier channels becomes a system optOut event, and subsequent agent-originated sends are refused with a typed optedOut error. Your agent does not implement carrier compliance.

Replying

Reply in the webhook HTTP response, in one of three content types:

application/json{ "messages": [ ...outbound items... ] }

application/x-ndjson — stream one { "message": { ... } } per line; the user receives each message as you emit it.

text/anychat-aml — raw AML text; Anychat compiles it. The simplest possible rich-messaging webhook is literally:

Happy to set that up!
[[ask.time #booking | When works best?
tue-345 = 2026-06-10T15:45:00-07:00 ~ Tue 3:45 PM
wed-10 = 2026-06-11T10:00:00-07:00 ~ Wed 10:00 AM]]

You can also send asynchronously (outreach, delayed replies):

POST https://gateway.anychat.ai/v4/messages
Authorization: bearer <access token>

{ "userId": "<user id>", "messages": [ { "spec": "amf/1", "parts": [ ... ] } ] }

The response carries per-message results: { "results": [ { "messageId": "...", "accepted": true } ] }. Supply your own id on a message for idempotency (Anychat dedupes for 24 hours). Rejections carry a typed reason: optedOut, windowClosed, mediaUnreachable, unsupportedPart, invalidMessage, unknownRecipient, ...

Signals (typing indicators, read receipts) are sent the same way:

{ "spec": "amf/1", "signal": "typing" }

Delivery policy

A message may carry a delivery block:

{
  "spec": "amf/1",
  "parts": [ ... ],
  "delivery": {
    "channels": ["rcs", "sms"],
    "fallbackOn": ["sendFailed"],
    "outsideWindow": "reject"
  }
}

channels is the preference order; fallbackOn lists the triggers that move delivery to the next channel. status events carry an attempt field ({ "channel": "sms", "provider": "twilio" }) so fallback is observable. (TTL-based undelivered fallback is specified but not yet active; sendFailed failover requires the user to be reachable on a sibling binding.)

Compile & capabilities endpoints

POST https://gateway.anychat.ai/v4/compile
{ "aml": "Hello!\n[[chips: Hi | Tell me more]]", "validateOnly": false }
→ { "message": { "spec": "amf/1", ... }, "warnings": [], "problems": [] }

GET https://gateway.anychat.ai/v4/capabilities?userId=<user id>
→ the CapabilityDoc for that user's channel

compile takes { "aml": string, "validateOnly"?: boolean } (aml is required — its absence is a 400). A clean compile returns 200 with the compiled message plus any non-fatal warnings; a compile with problems returns 422 and no message. With validateOnly: true the message is omitted from the response even on success.

capabilities requires the userId query parameter (400 without it) and returns 404 for a user id the agent has never seen.

Use /v4/compile in CI to validate AML your agent might emit, or to adopt AML from any agent framework without running Anychat's runtime.

Media

Media URLs must be publicly-accessible HTTPS. Anychat pulls, caches (keyed by full URL including query string), and re-hosts media on a whitelisted domain during send. Generating dynamic images? Make each URL unique. Media is retained for at least 30 days.

Webhook configuration

Configure your webhook URL in the console, or programmatically:

Method Path Effect
GET /v4/webhook Read the current { webhookUrl, instanceId }
POST /v4/webhook Set the webhook (same as PATCH)
PATCH /v4/webhook Set the webhook
DELETE /v4/webhook Remove the webhook binding
PATCH https://gateway.anychat.ai/v4/webhook
Authorization: bearer <my-access-token>
{ "webhookUrl": "https://www.example.com/webhook", "instanceId": "prod-7" }

webhookUrl is required on POST/PATCH (400 without it). The optional instanceId is an opaque identifier you choose; Anychat echoes it on every event envelope delivered to your webhook, so one endpoint can serve several agents or deployments. All four endpoints respond with the resulting { webhookUrl, instanceId } (DELETE returns a confirmation message).

Webhook requests are signed: verify X-Anychat-Signature (HMAC-SHA256 of v0:<timestamp>:<raw body> with your signing secret, hex-encoded, prefixed v0=) and reject stale X-Anychat-Request-Timestamp values.

Errors

Send failures return 4xx/5xx with { "message": "...", ... } plus typed per-message reason codes in batch results. A 2xx response means a message was accepted for delivery; track actual delivery via status events.