Skip to content

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.

Open Agents

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 #

GET/api/v1/agents

Requires the agents:read scope, which the Create an API key dialog ticks by default. No query parameters.

200 OK
{
"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 /calls with agent_id is 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 an agent_id that is not the one deployed to that number returns 412 agent_number_mismatch, and details.deployed_agent_id names 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 #

GET/api/v1/channels

Requires a valid key and no particular scope. No query parameters.

200 OK
{
"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_channels and call_channels are about the deployment. They tell you which capabilities this service carries at all. Asking for a channel that is not in these lists returns 412 failed_precondition with details.reason of unsupported_channel and details.supported naming what is available.
  • senders is about the workspace. whatsapp can be in message_channels while senders is empty, which means the capability exists and this workspace has not connected a number to it. A send in that state returns 412 with channel_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 #

GET/api/v1/ping

Requires a valid key and no particular scope. No parameters, no body, no side effects.

Shell
curl https://services.amomic.in/api/v1/ping \
-H "Authorization: Bearer $AMOMIC_API_KEY"
200 OK
{
"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 200 here 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 403 surprises you. scopes is the authoritative list. Comparing it against the scope named in details.required_scope turns "permission denied" from a mystery into a one-line diff. See Scopes.
  • When a 401 surprises you. details.reason says 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) or wrong_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 #

GET/api/v1/openapi.json

No 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.

Shell
curl https://services.amomic.in/api/v1/openapi.json -o amomic-openapi.json

It returns an OpenAPI 3.1 document containing:

  • info — title Amomic-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 over https:// whatever scheme this entry shows; the copy published with these docs at https://docs.amomic.in/openapi.json already says https://services.amomic.in.
  • paths — every /api/v1 route 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 their items without an item schema, so a generated client types them loosely; the item shapes are documented on each endpoint page.
  • securitySchemes — ApiKeyBearer (HTTP bearer) and ApiKeyHeader (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.

Node.js
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 #

200All four endpoints return 200 on success. `/agents` and `/channels` return an empty list rather than a 404 when there is nothing to list.
401unauthenticatedApplies 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.
403permission_deniedworkspace_suspendedThe workspace is suspended for billing. Every key in it is refused, including on `/ping`, until billing is restored.
403permission_deniedmissing_scope`/agents` only. The key lacks `agents:read`. `/channels` and `/ping` need no scope beyond a valid key.
429rate_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.

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.