Agents & channels
Four read-only endpoints that tell your backend what this workspace has — which agents exist, which senders are connected, what your key can do, and the machine-readable contract.
Four endpoints on this API do not do anything — they tell you about the workspace you are pointed at. They exist because of a gap the console cannot fill.
Everything else on this API takes an identifier you have to have got from somewhere. POST /messages takes a sender_id. GET /conversations takes an agent_id filter. POST /calls takes an agent_id to assert against. In the console those are things you click. In a deployment script running at 3 a.m. against a customer's workspace, there is no console open and nobody to copy a value out of one.
| Endpoint | Needs | Tells you |
|---|---|---|
GET /api/v1/agents |
agents:read |
Which agents exist, and which are live on web chat |
GET /api/v1/channels |
Any valid key | Which channels this deployment carries, and which senders are connected |
GET /api/v1/ping |
Any valid key | Which workspace, key, environment, and scopes you are holding |
GET /api/v1/openapi.json |
Nothing | The machine-readable contract for every route |
None of them has a side effect. All four are safe to call on every process start.
Listing agents #
/api/v1/agentsRequires the agents:read scope, which the Create an API key dialog ticks by default. No query parameters.
{"items": [{"agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH5","name": "Veloria Support","status": "live","channels": ["web"]},{"agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH4","name": "AMC Renewals","status": "draft","channels": []}],"next_cursor": null}
| Field | Type | Notes |
|---|---|---|
agent_id |
string | The handle you pass everywhere else. Treat it as an opaque string — copy it from here, do not construct it and do not parse it. |
name |
string | What the agent is called in the console. Falls back to Assistant when unset. |
status |
string | live or draft. Read the warning below: this is about web chat, not about whether the agent answers anywhere. |
channels |
string[] | ["web"] when status is live, otherwise []. |
Archived agents are left out. The newest agent comes first.
Nothing else is returned. There is no persona, no model configuration, no knowledge base list, no version history, and no deployment detail — those live in the console and have no public REST equivalent. This endpoint answers "which agents exist and what are they called", and that is its whole job.
`status` and `channels` describe web chat only
status is live while the agent has a live web chat deployment, and draft otherwise. channels is derived from it: ["web"] or [].
So an agent that answers WhatsApp and takes phone calls, but has no live web chat widget, reports draft and []. Never read draft as "this agent is not answering customers". Saved agent edits are served immediately on every channel the agent is deployed to, whatever this field says.
For what the workspace can send on, call GET /channels. For where a specific agent is deployed, the agent's Deployments tab in the console is the authority. See Deployments.
The envelope, and why it never paginates #
The response uses the same {items, next_cursor} shape as conversations, but next_cursor is always null and there is no limit or cursor parameter. You get every agent in the workspace in one response.
That is deliberate — a workspace has agents in the tens, not the thousands — and it means a generic pagination helper works here without a special case: one page, a null cursor, done. Pagination has that helper written out.
What you use agent_id for #
Two places, and they behave very differently:
GET /conversations?agent_id=…filters the list to one agent's conversations. A straightforward filter.POST /callswithagent_idis an assertion, not a selection. The agent that speaks on an outbound call is resolved from the caller-ID number, not from this field. Sending anagent_idthat is not the one deployed to that number returns412 agent_number_mismatch, anddetails.deployed_agent_idnames the agent that actually is. See Calls.
Resolve names to ids once, at startup
If your configuration names agents by their human name — "Support Assistant" — resolve it to an agent_id when your process starts, and cache it. Doing it per request spends a rate-limit slot on a value that changes about once a quarter. Doing it never means someone renames the agent in the console and your integration keeps working, which is the point.
What this deployment can do #
/api/v1/channelsRequires a valid key and no particular scope. No query parameters.
{"message_channels": ["whatsapp"],"call_channels": ["voice"],"senders": [{"channel": "whatsapp","sender_id": "was_9f2c41ab77de1063c8a4b512","display_number": "+918047261935","verified_name": "Veloria Home","is_default": true,"status": "live"}]}
| Field | Type | Notes |
|---|---|---|
message_channels |
string[] | The channels this deployment can send messages on. |
call_channels |
string[] | The channels it can place calls on. |
senders |
object[] | Every connected sender we could enumerate. Empty when nothing is connected. |
senders[].channel |
string | Which channel this sender belongs to. |
senders[].sender_id |
string | The value you pass as sender_id on POST /messages. Opaque — copy it. |
senders[].display_number |
string | The number customers see. |
senders[].verified_name |
string | The business name approved for this sender. |
senders[].is_default |
boolean | true on the workspace's default sender. When you omit sender_id, a default sender is used; with none marked, the first by number. |
senders[].status |
string | configuring, live, or disabled. Plan to send only from a live sender. |
Senders are listed default first, then by number.
Two different questions #
The two arrays and the sender list answer different things, and conflating them is a real source of confusion:
message_channelsandcall_channelsare about the deployment. They tell you which capabilities this service carries at all. Asking for a channel that is not in these lists returns412 failed_preconditionwithdetails.reasonofunsupported_channelanddetails.supportednaming what is available.sendersis about the workspace.whatsappcan be inmessage_channelswhilesendersis empty, which means the capability exists and this workspace has not connected a number to it. A send in that state returns412withchannel_not_connected.
Both are configuration states, not transient ones. Retrying either is pointless — see Errors.
Sender enumeration is best-effort
Building the senders array means asking each channel provider what it has. If one of those lookups fails, the failure is logged and that channel is skipped — the endpoint still returns 200 with whatever else it found.
A sender you expected and did not get is therefore ambiguous: it might not be connected, or its provider might have been unreachable for that one second. Never treat an absent sender as proof of an absent configuration, and never write a startup check that hard-fails on it. The channel arrays are the more stable signal; the sender list is a convenience.
Checking your key #
/api/v1/pingRequires a valid key and no particular scope. No parameters, no body, no side effects.
curl https://services.amomic.in/api/v1/ping \-H "Authorization: Bearer $AMOMIC_API_KEY"
{"ok": true,"workspace_id": "ten_01JQRS4T8XZ0AB1CD2EF3GH4","key_name": "Service desk (production)","environment": "live","scopes": ["agents:read", "conversations:read", "messages:send"]}
| Field | Type | Notes |
|---|---|---|
ok |
boolean | Always true. If the key were bad you would have a 401 instead. |
workspace_id |
string | The workspace this key belongs to. The same value arrives as tenant_id on every webhook. |
key_name |
string | The name the key was given in the console. Useful when three keys look identical in a secret store. |
environment |
string | The key's environment. Keys created in the console are live. |
scopes |
string[] | Sorted. Exactly what this key may do. |
This is the first call every integration should make, and it is worth wiring into more than a one-off:
- On deploy. A
200here proves the secret reached the process, the workspace is in good standing, and the key is not revoked. Three of the most common launch failures are ruled out before any customer-visible request. - When a
403surprises you.scopesis the authoritative list. Comparing it against the scope named indetails.required_scopeturns "permission denied" from a mystery into a one-line diff. See Scopes. - When a
401surprises you.details.reasonsays which check failed:missing_api_key,malformed_api_key,invalid_api_key,revoked_api_key,expired_api_key(a rolled key past its 24 hours) orwrong_environment. See Authentication.
Ping costs a request
It is cheap but not free — it spends one of your 120 per minute, and its response carries X-RateLimit-Limit and X-RateLimit-Remaining like any other success. That makes it a fine way to check your remaining budget, and a bad thing to put in a one-second health-check loop.
The machine-readable contract #
/api/v1/openapi.jsonNo key required. This is the only route on /api/v1 that answers an unauthenticated request, because a contract is not a secret and needing a credential to read one makes tooling harder for no security benefit.
curl https://services.amomic.in/api/v1/openapi.json -o amomic-openapi.json
It returns an OpenAPI 3.1 document containing:
info— titleAmomic-AI API, and a description that repeats the two auth header forms and the no-CORS rule.servers— the host you fetched it from. Every path in the document already starts with/api/v1. Call the host overhttps://whatever scheme this entry shows; the copy published with these docs athttps://docs.amomic.in/openapi.jsonalready sayshttps://services.amomic.in.paths— every/api/v1route and nothing else. Console-only routes are filtered out of the document entirely, so what you see is exactly the public surface.components.schemas— the request and response models. The list endpoints declare theiritemswithout an item schema, so a generated client types them loosely; the item shapes are documented on each endpoint page.securitySchemes—ApiKeyBearer(HTTP bearer) andApiKeyHeader(X-Api-Key), with both offered at the document level.
Two things to do with it:
Generate a client. There are no official SDKs in any language — deliberately, since the API is plain HTTP and JSON. If you want a typed client, generate one from this document rather than hand-rolling models that drift.
Give it to a coding agent. An agent that has the OpenAPI document plus these docs writes a working integration on the first attempt instead of inventing endpoints from a model's memory of "some WhatsApp API". Use with coding agents has the setup for Claude Code, Codex, Cursor, and anything that speaks MCP — including a documentation MCP endpoint and a single-file corpus.
The document describes shapes, not behaviour
An OpenAPI document can tell a generator that POST /messages takes a to and a text. It cannot tell it that a free-form text outside the 24-hour window fails, that Idempotency-Key must be derived rather than generated, or that agent_id on a call is an assertion. Those are the things that break integrations, and they are in these pages. Use both.
A startup check worth having #
Fifteen lines that turn every configuration failure into one clear log line at boot instead of a confusing 412 during a customer's checkout.
const BASE = "https://services.amomic.in/api/v1";const HEADERS = { Authorization: `Bearer ${process.env.AMOMIC_API_KEY}` };async function get(path) {const response = await fetch(`${BASE}${path}`, { headers: HEADERS });const body = await response.json();if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);return body;}export async function preflight({ requiredScopes = ["messages:send"] } = {}) {// 1. Is the credential live, and is it the one we think it is?const ping = await get("/ping");console.info("amomic_ready", {workspace: ping.workspace_id,key: ping.key_name,environment: ping.environment,});const missing = requiredScopes.filter((s) => !ping.scopes.includes(s));if (missing.length) {throw new Error(`API key "${ping.key_name}" is missing scopes: ${missing.join(", ")}`);}// 2. Can this deployment send at all, and is a sender connected?const channels = await get("/channels");if (!channels.message_channels.includes("whatsapp")) {throw new Error("This deployment cannot send WhatsApp messages.");}const sender = channels.senders.find((s) => s.channel === "whatsapp" && s.is_default);if (!sender) {// Enumeration is best-effort — warn, do not crash the process.console.warn("amomic_no_default_sender", { senders: channels.senders.length });} else if (sender.status !== "live") {console.warn("amomic_sender_not_live", { sender_id: sender.sender_id, status: sender.status });}return { workspaceId: ping.workspace_id, senderId: sender?.sender_id ?? null };}
Note what it does and does not treat as fatal. A missing scope is fatal — it will fail every send, loudly, forever. A missing sender is a warning, because enumeration is best-effort and refusing to start over a transient provider lookup is worse than starting.
Failures #
unauthenticatedApplies to `/agents`, `/channels`, and `/ping`. Key missing, malformed, unknown, revoked, expired after a roll, or from the wrong environment — `details.reason` distinguishes all six. `/openapi.json` never returns this.permission_deniedworkspace_suspendedThe workspace is suspended for billing. Every key in it is refused, including on `/ping`, until billing is restored.permission_deniedmissing_scope`/agents` only. The key lacks `agents:read`. `/channels` and `/ping` need no scope beyond a valid key.rate_limitedrate_limitedThese endpoints count against the same 120 per minute as everything else. Cache their answers; they change on the timescale of console edits, not requests.There is no 412 on any of them. GET /channels in particular is designed to be the thing you call instead of discovering a missing capability through a 412 on a real send.
Related #
Messages
POST /messages — where the sender_id from /channels gets used, and what happens when you omit it.
Calls
POST /calls — where agent_id is an assertion rather than a selection, and why that matters.
Scopes
The eleven accepted scopes — what /ping is showing you, and how to narrow a key safely.
Use with coding agents
Point your agent at the contract — the OpenAPI document, the docs MCP endpoint, and the full-corpus file.