Documentation overview

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:

text
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).

bash
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

FieldNotes
channelemail, sms, whatsapp, push, or inbox
toThe recipient – email address or E.164 phone, per channel
subjectEmail only; optional when a library_item_id carries one
html / textInline email content (either or both)
library_item_idSend an email built in the platform instead of inline content – see below
external_idYour own customer id, used for contact matching and suppression
categoryreceipt, order, delivery, payout, security, account, support, legal, other
client_idWhich business profile sends – required when the key isn't bound to one and the account has several
idempotency_keyExactly-once: the same key replays the original result, never a second send
allow_unknowntrue 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:

bash
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:

bash
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 subject in 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 pass client_id explicitly. A key bound to one brand can never send another brand's asset.
  • Pass either library_item_id or inline html/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".

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

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

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

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

text
# 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

StatusMeaning
401Missing or invalid key
402Insufficient credits (tools) or insufficient points (loyalty)
403Key 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)
404Resource not found or not yours
409Idempotency conflict – same key, different payload
410Identity erased on request (GDPR) – cannot be messaged or enrolled
422No verified sending identity, or the referenced library item has no sendable content
429Rate limited – honor Retry-After

Error bodies are JSON with a stable error code and a human message.