Skip to content

API overview

The base URL, the shape of every request, and the complete route table for the Amomic-AI REST API.

Open Developers

The Amomic-AI API lets your own systems send messages, place AI calls, read conversations, open and update tickets, read and upsert contacts, run WhatsApp and call campaigns, and drive the Duesly payments app. It is a deliberately flat REST surface with one error envelope and one authentication mechanism.

If you have not made a request yet, start with Your first API call. This page is the map you come back to.

Base URL #

HTTPS://SERVICES.AMOMIC.IN/API/V1

Every path on this page is relative to that. There is no other host, no /api/v2, no Accept-Version header, and no content negotiation — see API versioning for how changes are handled instead.

Your key carries an environment. A live key (amk_live_…) is the one that works against this host. A test key used here fails with 401 and details.reason of wrong_environment — a distinct, loud error rather than a silent no-op. Authentication covers the split.

What the API is for #

The API is the operational surface. It drives agents you have already built:

  • Send a WhatsApp message — free-form text inside the 24-hour window, or an approved template outside it.
  • Call a number and have an agent hold the conversation, then read the outcome.
  • Read conversations and their transcripts, whichever channel they happened on.
  • Open a support ticket from your own system, read it back, and move its status, priority, category, or tags.
  • Read and upsert contacts so a person your CRM knows about is the same person our inbox knows about.
  • Run campaigns: create a WhatsApp broadcast or a call batch as a draft, check who it would reach, launch, pause, resume, cancel, and read per-recipient results and call outcomes.
  • Drive Duesly: put customers on recurring plans, list dues, and record payments collected outside the gateway.

It is not the agent-building surface. Agents, versions, knowledge bases, tools, deployments, phone numbers, and templates are configured in the console and have no public REST equivalent. That boundary is intentional: the things you change once a quarter live where humans can review them, and the things you call ten thousand times a day live here.

How Amomic-AI fits togetherYOUR CUSTOMERSWhatsApptheir phonePhone callyour numberWeb chatyour websiteYour agentone persona · one knowledge base · one set of toolsdeployed separately to each channelKnowledge basewhat it knowsToolswhat it can doYOUR SYSTEMSYour backendorders, CRM, helpdeskRESTPublic API/api/v1 · API keywebhooksYour endpointsigned eventsEverything a customer says lands in one inbox, one contact record, and one set of analytics —no matter which channel it arrived on.
Your backend calls /api/v1; events come back to you over webhooks. Everything an agent is made of is configured in the console.

Server-side only #

/api/v1 sends no CORS headers at all. Not a restrictive Access-Control-Allow-Origin — none. Preflight requests are not answered. A browser cannot successfully call this API, and that is the design, not a gap.

The reason is the key itself. API keys are workspace-wide and carry every scope you granted them; there is no per-user, per-agent, or per-origin key. A key placed in front-end JavaScript is readable by anyone who opens your bundle, and the blast radius of that leak is your entire workspace. Removing CORS removes the temptation.

If you need something in the page

Use the hosted web chat widget or build your own interface. Both use browser-safe, origin-bound visitor sessions — no API key involved.

The shape of every request #

Four things make up a request. Only the first is always required.

Authorizationheaderrequired

Bearer amk_live_…. Alternatively send X-Api-Key: amk_live_… — either is accepted, and X-Api-Key wins if you send both.

Content-Typeheaderoptional

application/json on every request with a body. Every POST takes a JSON body (the campaign action routes need one too, for expected_version); none take form encoding or multipart.

Idempotency-Keyheaderoptional

Honoured on five creates: POST /messages, POST /calls, POST /tickets, POST /campaigns, and POST /apps/duesly/subscribers. Makes a retry safe. See Idempotency.

X-Correlation-Idheaderoptional

Your own trace id. Echoed back on the response and stored in your workspace request log, so one id joins your logs to ours. Generated for you if you omit it.

A minimal request looks like this:

Shell
curl -X POST https://services.amomic.in/api/v1/messages \
-H "Authorization: Bearer $AMOMIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821-shipped" \
-H "X-Correlation-Id: trace-9f2c1d" \
-d '{"to": "+919876543210", "text": "Your order AM-4821 has shipped."}'

What comes back #

Header When Meaning
X-Correlation-Id every response Your X-Correlation-Id echoed, or one we generated.
X-RateLimit-Limit successful responses only Requests allowed per minute for this key.
X-RateLimit-Remaining successful responses only Requests left in the current minute.

There is no X-Request-Id response header. The request id appears inside error envelopes as error.request_id, and nowhere else. There is no Retry-After and no X-RateLimit-Reset — Rate limits explains how to back off without them.

Successful responses are 200, except the six creates, which are 201: POST /messages, /calls, /contacts, /tickets, /campaigns, and /apps/duesly/subscribers.

Every non-2xx response uses one envelope:

403 Forbidden
{
"error": {
"code": "permission_denied",
"message": "This API key does not have the `messages:send` scope.",
"details": { "reason": "missing_scope", "required_scope": "messages:send" },
"request_id": "req_01JQRS4T8XZ0AB1CD2EF3GH4",
"correlation_id": "cor_01JQRS4T8XZ0AB1CD2EF3GH4"
}
}

Nine code values exist and no others can. Errors has the full map and the complete details.reason catalogue.

Ids #

Every id we mint is a prefix, an underscore, and a body. The prefix tells you what you are holding even when it is out of context in a log line.

Prefix What it identifies Where you see it
con_ A conversation conversation_id
msg_ A chat message message_id on transcript rows
call_ A call call_id
tkt_ A ticket ticket_id
cmp_ A campaign campaign_id
sub_ A Duesly subscriber subscriber_id
AM-XXXXXXXX A ticket's human reference public_ref — the one you quote to a customer
req_ One API request error.request_id
cor_ One trace error.correlation_id, X-Correlation-Id
evt_ One webhook event. It keeps the same id across retries and replays, so it is your dedupe key The webhook envelope id and Amomic-Event-Id
whd_ One delivery attempt of an event The console's delivery log
amk_ An API key token Shown once, at creation
whsec_ A webhook signing secret Shown once, at endpoint creation
wamid. A WhatsApp message message_id from POST /messages — WhatsApp's format, not ours

Most of these bodies are ULIDs: 26 characters of Crockford base32, time-ordered, so sorting the strings sorts by creation time. That property is load-bearing — the message cursor in Pagination is just a message_id.

Treat every id as an opaque string. Do not parse the body, and do not assume a length: tkt_ and evt_ ids are hex, not ULIDs, and wamid. belongs to WhatsApp.

Timestamps #

Two shapes exist, and confusing them is a real source of bugs.

Where Shape Example
REST resources on /api/v1 ISO 8601 in UTC. Most carry microseconds and +00:00; some resources end in Z instead 2026-10-14T05:50:12.418903+00:00
The webhook envelope's created_at ISO 8601 to the second with a literal Z 2026-10-14T05:50:12Z

All of them are UTC. All parse with any conforming ISO 8601 parser, but a hand-rolled regex or a strptime format string written against one shape will fail on another.

Every timestamp field is nullable. A value that cannot be formatted comes back as null rather than raising, so created_at: null means "not recorded", not "epoch".

The route table #

Thirty-three operations, and that is the entire public surface. Thirty-two sit behind an API key; the OpenAPI document needs nothing.

Method Path Scope Idempotent Success
GET /openapi.json none (unauthenticated) – 200
GET /ping any valid key – 200
GET /channels any valid key – 200
POST /messages messages:send yes 201
POST /calls calls:write yes 201
GET /calls/{call_id} calls:write – 200
POST /calls/{call_id}/hangup calls:write – 200
GET /conversations conversations:read – 200
GET /conversations/{conversation_id} conversations:read – 200
GET /conversations/{conversation_id}/messages conversations:read – 200
GET /agents agents:read – 200
GET /contacts contacts:read – 200
GET /contacts/lookup contacts:read – 200
GET /contacts/{contact_id} contacts:read – 200
POST /contacts contacts:write converges 201
POST /tickets tickets:write yes 201
GET /tickets/{ticket_id} tickets:write – 200
PATCH /tickets/{ticket_id} tickets:write version guard 200
POST /campaigns campaigns:write yes 201
GET /campaigns campaigns:read – 200
GET /campaigns/{campaign_id} campaigns:read – 200
POST /campaigns/{campaign_id}/evaluate campaigns:read – 200
POST /campaigns/{campaign_id}/launch campaigns:write version guard 200
POST /campaigns/{campaign_id}/pause campaigns:write version guard 200
POST /campaigns/{campaign_id}/resume campaigns:write version guard 200
POST /campaigns/{campaign_id}/cancel campaigns:write version guard 200
GET /campaigns/{campaign_id}/recipients campaigns:read – 200
GET /campaigns/{campaign_id}/outcomes campaigns:read – 200
GET /campaigns/{campaign_id}/outcomes.csv campaigns:write – 200 (CSV)
GET /apps/duesly/subscribers campaigns:read – 200
POST /apps/duesly/subscribers campaigns:write yes 201
GET /apps/duesly/dues campaigns:read – 200
POST /apps/duesly/dues/{due_id}/paid campaigns:write state guard 200

Grouped by what you are doing:

Area Page
/ping, /channels, /agents Agents & channels
/messages Messages
/calls… Calls and Call outcomes
/conversations… Conversations
/contacts… Contacts
/tickets… Tickets
/campaigns… Campaigns; the two outcome routes are on Call outcomes
/apps/duesly/… Duesly (payments)

Three rows deserve a second look:

  • GET /tickets/{ticket_id} requires tickets:write. There is no tickets:read scope. Granting a key the ability to read a ticket necessarily grants it the ability to open one. Scopes explains the reasoning and what to do about it.
  • POST /contacts does not honour Idempotency-Key. It is naturally convergent: it matches on email, then phone, so a retry resolves to the same contact. It is the one create where the header is ignored.
  • GET /campaigns/{campaign_id}/outcomes.csv requires campaigns:write, although it is a read. The file carries every column of the uploaded list, which the per-recipient read does not, so a reporting key reads /outcomes and /recipients instead.

The machine-readable version #

GET/api/v1/openapi.json

An OpenAPI 3.1 document describing the thirty-two keyed operations, fetchable with no key at all. List endpoints are typed as a page of items without an item schema, so use these pages for the item fields. Feed it to a client generator, or hand it to a coding agent alongside these docs.

What this API deliberately does not do #

This is scope, not a roadmap. Building against an assumption in this list will not work today and is not waiting on a release.

  • It is not callable from a browser. No CORS headers, no preflight. Use the widget for in-page work.
  • POST /messages is still one recipient. For a managed WhatsApp broadcast or call batch, use the Campaigns API, which owns audience resolution, pacing, guardrails, scheduling, and per-recipient outcomes.
  • Individual messages and calls have no scheduling parameter. There is no send_at on /messages or /calls; hold that request until you want it sent. Campaigns have their own schedule.
  • There is no media upload on messages. POST /messages accepts text and approved templates. There is no attachment field and no upload endpoint to put a file behind one.
  • There are no SDKs. No official client library in any language. The API is plain HTTP and JSON on purpose — fetch in Node or requests in Python is the whole dependency list, and GET /openapi.json generates a client if you want one.
  • There is no key or webhook management API. Keys and webhook endpoints are created, rolled, and revoked in the console by an admin. There is no public route that mints a credential.
  • There is no read side for messages. No GET /messages or message list; read message history through conversations. Contacts do have list, lookup, and get routes.
  • Updates are deliberately narrow. PATCH /tickets/{ticket_id} can change status, priority, category, and tags with optimistic version checking. Other public resources have no general update or delete routes.
  • There is no sandbox mode. The only split is live versus test, and it follows the environment a key was issued in. You cannot obtain a test key that works against production.

Next #

Authentication

How keys work: the format, the one-time reveal, rolling, revoking, and the distinct causes of a 401 or 403.

Scopes

What a key can do: eleven scopes, the read-only default, and the endpoint matrix.

Errors

One envelope, nine codes, and the details.reason catalogue to branch on.

Send something

POST /messages: the endpoint most integrations exist for.