API & Integrations
Everything programmatic lives behind one kind of credential: an API key
from Settings → API keys (/settings/api), scoped to exactly what it
may do. Keys are server-to-server credentials – never ship one in browser
code, a mobile app, or anything a customer can inspect.
Authentication
Every request carries the key in a header – either works:
Authorization: Bearer theaicmo_...
X-Api-Key: theaicmo_...
Keys are shown once at creation. Each key carries scopes (ticked when you
create it) and per-minute/per-day rate limits; 429 responses include a
Retry-After header.
Send a service message
Scope: send:transactional. Receipts, password resets, notifications –
delivered on your transactional identity and never blocked by marketing
opt-outs (a receipt is not a promotion).
curl -X POST https://theaicmo.com/api/send \
-H "X-Api-Key: theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"to": "customer@example.com",
"subject": "Your order shipped",
"html": "<p>It is on the way.</p>",
"idempotency_key": "order-1042-shipped"
}'
Parameters
| Field | Notes |
|---|---|
channel | email, sms, whatsapp, push, or inbox |
to | The recipient – email address or E.164 phone, per channel |
subject | Email only; optional when a library_item_id carries one |
html / text | Inline email content (either or both) |
library_item_id | Send an email built in the platform instead of inline content – see below |
external_id | Your own customer id, used for contact matching and suppression |
category | receipt, order, delivery, payout, security, account, support, legal, other |
client_id | Which business profile sends – required when the key isn't bound to one and the account has several |
idempotency_key | Exactly-once: the same key replays the original result, never a second send |
allow_unknown | true sends to a recipient who is not a contact yet – see below |
Sends go to known customers by default. The recipient must match a
contact on the business profile (by email, phone, or external_id);
anyone else answers 403 unknown_recipient. For flows where the send
legitimately precedes the contact record – a brand-new customer's first
receipt or OTP – pass "allow_unknown": true to override for that one
request:
curl -X POST https://theaicmo.com/api/send \
-H "X-Api-Key: theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"to": "brand-new-customer@example.com",
"subject": "Welcome – your account is ready",
"html": "<p>Your login is ready.</p>",
"allow_unknown": true,
"idempotency_key": "signup-8812-welcome"
}'
Blocked lists, hard suppressions and GDPR erasures apply either way: an
identity erased on request answers 410 identity_erased on every
channel, override or not.
Send an email from your library
Emails you built in the platform – the email designer, the campaign generator, journey drafts – live in your library and can be sent by reference instead of pasting HTML:
curl -X POST https://theaicmo.com/api/send \
-H "X-Api-Key: theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"to": "customer@example.com",
"library_item_id": "<the library item id>",
"idempotency_key": "welcome-cust-8812"
}'
- The item's own subject line is used when it carries one; a
subjectin the request overrides it. Provide one or the other. - Personalization renders for the recipient exactly as a journey send
would render it –
{{p.*}}tokens and survey links resolve to this person before delivery. - Brand-locked. The item must belong to the same business profile
the key resolves to; anything else is refused with
403 wrong_profile. Accounts with several profiles passclient_idexplicitly. A key bound to one brand can never send another brand's asset. - Pass either
library_item_idor inlinehtml/text– not both. - This endpoint stays service-only. An item sent by reference must still be a service message; the content check flags marketing copy riding a service send, exactly as it does for inline content.
The item id is shown on the item's detail panel in the library ("API id" – click to copy).
Trigger a journey
Scope: journeys:trigger. Start a published journey for one person from
your own systems – "order shipped", "deposit made", "ticket resolved".
curl -X POST https://theaicmo.com/api/journeys/trigger \
-H "X-Api-Key: theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"journey_id": "<the journey id>",
"email": "customer@example.com",
"event": { "order_number": "1042", "amount": 89.90 },
"idempotency_key": "order-1042-shipped"
}'
Event properties (up to 20) reach your message copy as personalization. Every entry door still applies: suppression, contact status, and the journey's own re-entry rules. The journey's id and a ready-made curl live on the journey's trigger settings in the builder.
Loyalty: balance, rewards, redemption
Scope: loyalty. Render points and tier inside your product, list the
rewards shop, and redeem – full walkthrough in the
Loyalty Program guide.
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/loyalty/balance?identity=customer@example.com"
Redemptions are idempotent (same idempotency_key returns the original
receipt) and can never overdraw a balance.
Run a tool
Scope: tools:execute. Any generation tool, programmatically – same
tools, same credits as in the product.
curl -X POST https://theaicmo.com/api/execute-tool-api \
-H "Authorization: Bearer theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"tool_id": "social-post-generator",
"parameters": {
"productName": "Aurora Coffee",
"platform": "linkedin",
"variations": 2
}
}'
The response carries the generated output plus credits_used and
credits_remaining. Tool ids and per-tool credit prices are listed on
each tool's page in the product.
Ask in plain English
Scope: agent:run (or tools:execute). One call, plain English, and the
same agent that runs the chat in the product picks the tools, runs them in
order, and hands back what they produced. This is the endpoint most
integrations want – the per-tool call below is for when you know exactly
which tool you need.
curl -X POST https://theaicmo.com/api/agent-api \
-H "Authorization: Bearer theaicmo_..." \
-H "Content-Type: application/json" \
-d '{
"message": "write three LinkedIn posts about our summer sale and a Google ad to match",
"client_id": "<brand-id>"
}'
The reply carries reply, the tools it ran, a results entry per tool
with its own credits_used, any media produced, and the total
credits_used. Pass the conversation_id you get back on the next call to
continue the same conversation. A tool that returns customer or analytics
data also needs data:read on the key – without it that tool is refused
and the rest of the turn still runs.
Read your customers back out
Scope: data:read. Your customer base is yours to read: profiles, the
membership of a saved segment, and everything changed since a timestamp so
another system can stay in step. JSON by default, CSV with format=csv.
# one page of profiles for a brand
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/customers/read?client_id=<brand-id>&limit=100"
# one person, by your own id or by e-mail
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/customers/read?identifier=player-48213"
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/customers/read?identifier=customer@example.com"
# a saved segment, page by page, as a file
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/customers/read?segment_id=<segment-id>&format=csv"
# a sync loop: everything changed since your last run
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/customers/read?updated_since=2026-08-01T00:00:00Z"
Pages are 100 by default, 1000 at most. When has_more is true, pass
next_cursor back as cursor for the next page – a segment of any size
comes out in full that way. Keep passing back whatever next_cursor gave
you rather than building a cursor yourself: on a segment read it is a
member id, and on every other read it is a timestamp. On a segment read a
page can also come back shorter than the limit when a filter removes
someone from it, and that is not the end of the segment – has_more is. Contacts marked as test
data are left out unless you pass include_test=true. People erased under a
GDPR request are never returned – an erasure is not undone by an export.
Every read is scoped to your own account, and a client_id that is not one
of your business profiles is refused rather than ignored.
Errors
| Status | Meaning |
|---|---|
| 401 | Missing or invalid key |
| 402 | Insufficient credits (tools) or insufficient points (loyalty) |
| 403 | Key lacks the required scope, the resource belongs to a different business profile (wrong_profile), or the recipient is not a known contact (unknown_recipient – see allow_unknown) |
| 404 | Resource not found or not yours |
| 409 | Idempotency conflict – same key, different payload |
| 410 | Identity erased on request (GDPR) – cannot be messaged or enrolled |
| 422 | No verified sending identity, or the referenced library item has no sendable content |
| 429 | Rate limited – honor Retry-After |
Error bodies are JSON with a stable error code and a human message.