---
name: theaicmo
description: Work with The AI CMO, the system a brand runs its marketing on – acquisition and retention planned, produced, sent and measured on the brand's own customer data. Use when the user asks you to create marketing for a brand they run on The AI CMO (ads, social posts, e-mails, articles, images, video, strategies, keyword plans), read their customers or a segment, send a service message, start a journey for one customer, or check and redeem loyalty points. Covers getting an API key, every endpoint, and the rules each one enforces.
metadata:
  author: The AI CMO
  version: 2.0.0
  homepage: https://theaicmo.com
  api_base: https://theaicmo.com/api
  docs: https://theaicmo.com/docs/api
---

# The AI CMO

The AI CMO runs a brand's marketing as one operation. It plans the strategy, produces the assets, sends e-mail, SMS and WhatsApp from the brand's own domain and accounts, and measures what worked on the brand's own customer data. An account holds one or more brands (business profiles), and each brand has its own profile, customers and library.

This file tells an agent how to work with it through the API. The canonical copy is https://theaicmo.com/skill.md. It is a Markdown instruction file, not a shell command: read it and carry out the steps that apply.

## 1. Get access

- **Account.** Existing users sign in at https://theaicmo.com/login. New users choose a plan at https://theaicmo.com/pricing. Let the person finish sign-up, payment and verification in the browser.
- **API key.** The person creates it at **Settings → API keys** (https://theaicmo.com/settings/api). It is shown once. Ask them to put it in an environment variable, `THEAICMO_API_KEY`, rather than paste it into the conversation.
- **Server-side only.** A key never goes into browser code, a mobile app, or anything a customer can inspect.
- **Header.** `Authorization: Bearer $THEAICMO_API_KEY` or `X-Api-Key: $THEAICMO_API_KEY`. Both work on every endpoint below.
- **Scopes.** Every key can ask the agent and run tools. The rest are ticked when the key is created; ask the person to tick only what the task needs.

| Scope | Allows |
| --- | --- |
| `tools:execute` | The plain-English endpoint and single tools. On every key. |
| `data:read` | Read customers and segments. Also lets a plain-English turn use the tools that return customer or analytics data. |
| `send:transactional` | Send service messages. |
| `journeys:trigger` | Start a published journey for one person. |
| `loyalty` | Loyalty balances, the rewards shop and redemption. |

- **Brands.** `client_id` is the brand a call acts for. The brand ids are listed under **Brand ids** on Settings → API keys. An account with one brand can leave `client_id` out. On an account with several, the plain-English and tool endpoints answer `400 client_id_required` with a `brands` list to choose from, and the send endpoint answers `400` too; a journey trigger takes the brand from the journey.
- **Rate limits.** A key made in Settings allows 60 calls a minute and 1,000 a day. A `429` carries `Retry-After`.

**Check the key without spending credits:**

```bash
curl -X POST https://theaicmo.com/api/execute-tool-api \
  -H "Authorization: Bearer $THEAICMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool_id": "list"}'
```

A `404` carrying `availableTools` means the key works. A `401` means it is wrong, revoked or expired.

## 2. Ask in plain English – start here

`POST /api/agent-api` hands one request, in plain English, to the same agent that runs Chat in the product. It acts as the brand – its name, industry, audience, selling points, language and currency, the apps the account has connected and, when memory is on, what the account has taught it – picks the tools, runs them in order, and returns what they produced. What it makes is filed in the brand's Library.

Through a key the agent creates and reads. It never publishes, sends, or changes a live ad account or connected app; a person does that in the product.

```bash
curl -X POST https://theaicmo.com/api/agent-api \
  -H "Authorization: Bearer $THEAICMO_API_KEY" \
  -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>"
  }'
```

| Field | Notes |
| --- | --- |
| `message` | Required. Up to 8,000 characters. |
| `client_id` | The brand to act as. See Brands above. |
| `conversation_id` | Continues an earlier conversation, which keeps its brand. Every reply returns one. |
| `images` | Up to 5 `{ "url": "..." }` for the agent to look at. |

```json
{
  "success": true,
  "conversation_id": "…",
  "client_id": "…",
  "reply": "Here are three posts and a matching search ad…",
  "tools": ["social-post-generator", "ppc-ad-generator"],
  "results": [
    { "tool_id": "social-post-generator", "credits_used": 8, "result": { } },
    { "tool_id": "ppc-ad-generator", "credits_used": 10, "result": { } }
  ],
  "media": [],
  "credits_used": 18
}
```

- `credits_used` counts the tools the turn ran. The agent's own reading and writing is metered separately, on what it uses, and shows under Settings → Usage.
- A turn stops at the first tool it may not run – one that publishes or changes a live account, or one that reads customer data without `data:read` – or at a tool that fails. The reply then carries `success: false`, `stopped` (`scope_denied` or `tool_failed`), `error` and a `message`, with whatever ran before it.
- A turn can take several minutes when it makes images or video. Allow at least 300 seconds before timing out.
- `402` means the account is out of credits. The body still carries `conversation_id`.

## 3. Run one tool

`POST /api/execute-tool-api` runs one named tool, the same way it runs in the product: keyword research, backlinks and the Google Ads campaign builder read live search data; images carry the brand's look; the result is filed in the brand's Library (`library_item_id`).

```bash
curl -X POST https://theaicmo.com/api/execute-tool-api \
  -H "Authorization: Bearer $THEAICMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_id": "social-post-generator",
    "client_id": "<brand id>",
    "idempotency_key": "summer-sale-linkedin-1",
    "parameters": {
      "prompt": "Launch of our cold brew subscription, first month half price",
      "platform": "linkedin",
      "variations": 2
    }
  }'
```

| Field | Notes |
| --- | --- |
| `tool_id` | Required. One of the tools below. |
| `parameters` | The tool's fields. Common spellings are accepted (`target_audience` for `targetAudience`). |
| `client_id` | The brand. See Brands above. |
| `idempotency_key` | A retry with the same key returns the first run's result and charges nothing (`replayed: true`); `409 run_in_progress` while that run is still going. |

The brand fills the fields in the table's last column when you leave them out – its name, audience, voice and the like – and the tool writes in the brand's language and currency. If a required field is still missing, the call is refused with `400 invalid_parameters`, listing `missing`, before anything runs or is charged.

```json
{
  "success": true,
  "tool_id": "social-post-generator",
  "client_id": "…",
  "credits_used": 8,
  "credits_remaining": 4192,
  "execution_time_ms": 6100,
  "result": {
    "variations": [{ "content": "…" }, { "content": "…" }],
    "rawOutput": "…"
  },
  "library_item_id": "…"
}
```

- Text tools return `result.variations` and the full text in `result.rawOutput`. An image tool returns `result.mediaUrl` (several images: `result.images`).
- A tool that ran and reported a failure answers `422 tool_failed` with its `message`, and its credits are returned. A run that failed on our side answers `502`, and any credits it took are returned. If the answer never arrives, retry with the same `idempotency_key`: a run that did finish is returned, not run again.
- Other refusals: `402 insufficient_credits` or `client_budget_exceeded`, `403 no_active_plan`, `brand_locked` or `read_only`, `404 unknown_tool`.
- Allow up to 300 seconds.

### Tools

**Required** fields are refused when missing. **The brand fills** fields come from the brand's profile when left out; send them to override it.

**Strategy and retention**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `strategy-creator` | A marketing strategy | – | businessName, industry, targetAudience, productDescription, uniqueSellingPoints, competitors |
| `customer-journey-mapper` | A customer journey map | – | businessType, product, targetAudience |
| `loyalty-strategy-creator` | A loyalty programme strategy | – | businessName, industry, targetAudience, productDescription |
| `referral-program-creator` | A referral programme | – | businessName, industry, targetAudience, productDescription |
| `customer-winback-campaign` | A win-back campaign | – | businessName, industry, targetAudience, productDescription |
| `winback-campaign-creator` | Win-back e-mails and offers | – | businessName, industry, targetAudience, productDescription |

Also useful: `goals`, `channels`, `budget`; win-back takes `churnReasons` and `inactivePeriod`.

**Advertising and search**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `ppc-ad-generator` | Search and social ad copy | prompt | productName, targetAudience, tone |
| `ppc-quick` | Quick search ad copy | prompt | productName, targetAudience, tone |
| `ppc-scaffolder` | A Google Ads campaign built on live keyword data | prompt | productName, products, website, industry, targetAudience, uniqueSellingPoints |
| `ad-compliance-checker` | A policy check of ad copy | adCopy | – |
| `keyword-research` | Keywords with live search volume, CPC and difficulty | – | productName, website |
| `backlinks-analyzer` | A domain's live backlink profile | – | target (the brand's website) |

`keyword-research` takes `keywords` (a list) or a `website`, plus `location` and `language`.

**Social**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `social-post-generator` | Social posts | prompt, platform | productName, tone (and the one brand audience the prompt names, unless you pass targetAudience) |
| `social-post` | Social posts | prompt, platform | productName, tone (and the one brand audience the prompt names, unless you pass targetAudience) |
| `social-thread-generator` | A thread for X, as ordered posts | topic | productName, tone (and the one brand audience the prompt names, unless you pass targetAudience) |

**E-mail**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `email-campaign-generator` | A full e-mail campaign | prompt | productName, targetAudience, tone |
| `email-campaigns` | An e-mail campaign on a template | prompt | productName, targetAudience, tone |
| `email-copy` | One marketing e-mail | prompt | productName, targetAudience, tone |
| `email-subject-line-tester` | A subject line checked against inbox and spam-filter rules, with five alternatives (no score) | subjectLine | industry, productName, targetAudience |

**Content**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `seo-blog` | An SEO blog post | topic | productName, targetAudience, tone |
| `blog-post-generator` | A long-form article | topic | productName, targetAudience, tone |
| `seo-content-generator` | Content for a target keyword | prompt | productName, targetAudience, tone |
| `seo-content` | A page's title, description, social card and structured data, read from the page (and its Search Console searches) and measured the way Google cuts them | – | productName |
| `app-store-optimization` | App Store and Google Play listings for any market the stores cover: the current listing, store search positions, the apps above it and reviews read from the stores, then three options per store and market measured against each store's limits and checked against the brand's rules (demand only with its named source: Apple Ads search popularity or the brand's Play Console search terms when connected, an estimate of store searches for the United States in English, or Google web searches for the listing's country and language, which are interest on the web and not store searches; otherwise none) | – | productName |
| `product-description-generator` | Product descriptions | prompt | productName, targetAudience, tone |
| `product-descriptions` | Short product copy | prompt | productName, targetAudience, tone |
| `pr-release` | A press release | prompt | productName, industry, targetAudience |
| `landing-pages` | Landing page copy | prompt | productName |
| `bulk-content` | Many similar pieces in one call | – | productName |
| `style-based-content` | Content in the style of a sample – put the brief and the sample in `prompt` | prompt | productName, targetAudience, tone |

`app-store-optimization` takes `apps` (App Store links or ids, Google Play links or package names), `stores`, `locales` (`en-GB` style, any market the stores cover; at most 4 listings, stores × markets, per call) and `terms` (what the app should be found for). Searches are per language: with markets in more than one language, pass `termsByLanguage` (`{"en": [...], "de": [...]}`) and each listing uses its own language's list. Left out, `apps` is the app saved for the brand. `result.rawOutput` is the result as JSON: `demand.rows` carry each figure's `store`, `country`, `language`, `scale` and `source`, and `demand.sources` the label to show with it; each option in `listings[].options` has `guardrails`, naming any brand rule it breaks (`advisory: true` for an industry advisory). The result is copy: nothing is changed in either store.

**Sales and reporting**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `sales-script` | A sales demo script | prompt | productName, targetAudience, tone |
| `webinar-content` | A webinar script, slides and promotion | prompt | productName, targetAudience, tone |
| `client-proposal` | A marketing proposal | prompt | productName, targetAudience, tone |
| `review-response` | A reply to a customer review | reviewText | productName, tone |
| `campaign-report` | An analysis of the campaign figures you send | campaignName, campaignData | productName |

**Images and video**

| Tool id | Makes | Required | The brand fills |
| --- | --- | --- | --- |
| `image-generator` | A marketing image | prompt | productName, targetAudience, tone |
| `ugc-images` | A UGC-style product or lifestyle image | prompt | productName, targetAudience, tone |
| `image-banner-generator` | A finished banner with the headline and call to action in it | prompt | – |
| `video-studio` | A film from a brief | prompt, and `client_id` | product, audience |
| `ugc-ad-generator` | The older name of `video-studio`; it runs Video Studio | prompt, and `client_id` | product, audience |

Images take `aspectRatio` (`1:1` by default; also `4:5`, `9:16`, `16:9` and others). `video-studio` takes `format`: `prompt` (the default – describe the video), `brief_ad` (research, five scripts, the best one rendered), `actor_batch` (one script per actor), `article` (an article or URL becomes a short; send `sourceUrl` or `sourceText`) or `repurpose` (shorts cut from a long video; send `sourceVideoUrl`). It returns at once with the project in status `processing`; the finished film lands in the brand's Library. Credits are reserved scene by scene and returned for any scene that never renders, so `credits_used` in this reply reads 0.

## 4. Read customers

Scope `data:read`. `GET /api/customers/read` returns profiles, a saved segment's members, or everyone changed since a timestamp. JSON by default, CSV with `format=csv`.

```bash
# a page of one brand's customers
curl -H "Authorization: Bearer $THEAICMO_API_KEY" \
  "https://theaicmo.com/api/customers/read?client_id=<brand id>&limit=100"

# one person, by e-mail or by the brand's own customer id
curl -H "Authorization: Bearer $THEAICMO_API_KEY" \
  "https://theaicmo.com/api/customers/read?identifier=player-48213"

# a page of a saved segment's members
curl -H "Authorization: Bearer $THEAICMO_API_KEY" \
  "https://theaicmo.com/api/customers/read?segment_id=<segment id>"

# everyone changed since the last sync
curl -H "Authorization: Bearer $THEAICMO_API_KEY" \
  "https://theaicmo.com/api/customers/read?updated_since=2026-09-01T00:00:00Z"
```

The JSON reply is `{ customers, count, next_cursor, has_more }`, plus `segment` on a segment read.

- With `client_id`: that brand's contacts – whoever on the team added them – plus the account's contacts that carry no brand. Any role on the brand may read it.
- Without `client_id`: the whole account – every brand and the contacts with no brand – for the account owner or an organization owner, admin or manager. A key restricted to one brand reads that brand. Anyone else gets `400 client_id_required` with the `brands` they can read.
- Pages hold 100 by default and 1,000 at most. While `has_more` is true, pass `next_cursor` back as `cursor`. The cursor is opaque: never build one.
- A page can come back shorter than the limit, or empty, and still not be the last one. Only `has_more` says the read is finished.
- CSV pages the same way through the `X-Has-More` and `X-Next-Cursor` response headers. Its columns: id, email, external_id, first_name, last_name, company, phone, status, source, is_test, created_at, updated_at, client_id.
- Contacts marked as test data are left out unless you pass `include_test=true`.
- People erased under a GDPR request are never returned. If that cannot be checked, the read answers `503 erasure_check_unavailable`; retry.

## 5. Send a service message

Scope `send:transactional`. `POST /api/send` sends receipts, password resets, order and account notices. It is for service messages only; marketing goes out through journeys and campaigns in the product. The content is checked, and marketing copy riding a service send comes back as `warnings` (the message is still delivered).

```bash
curl -X POST https://theaicmo.com/api/send \
  -H "Authorization: Bearer $THEAICMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "email",
    "to": "customer@example.com",
    "subject": "Your order shipped",
    "html": "<p>It is on the way.</p>",
    "category": "delivery",
    "idempotency_key": "order-1042-shipped"
  }'
```

| Field | Notes |
| --- | --- |
| `channel` | `email`, `sms`, `whatsapp`, `push` or `inbox` |
| `to` | The e-mail address (email), E.164 phone number (sms, whatsapp), or the person's e-mail (push, inbox) |
| `external_id` | The brand's own customer id. On push and inbox it can address the person alone. |
| `category` | `receipt`, `order`, `delivery`, `payout`, `security`, `account`, `support`, `legal` or `other` |
| `client_id` | Required when the account has several brands. The key may send for any brand its owner may change. |
| `idempotency_key` | Or the `Idempotency-Key` header. The same key replays the first result, a `202 outcome_unknown` included; the same key with a different message is `409 idempotency_conflict`. |
| `allow_unknown` | `true` sends to someone who is not a contact yet, such as a first receipt or a one-time code |

Per channel:

- **email** – `subject` and `html` and/or `text`, or `library_item_id` to send an e-mail built in the product (not both). Delivered from the brand's service sending identity.
- **sms** – `text`.
- **whatsapp** – `template` (an approved service template), with `variables` and `language` when it has them.
- **push** – `text`, with `title`, `url` and `routes` (`all`, `app` or `web`).
- **inbox** – `headline` and `text`, with `cta_label`, `cta_url`, `days` (1–365, default 30) and `site_id` when the brand has several sites. It shows the next time the person loads the brand's site.

SMS and WhatsApp use the brand's service sender when it has one, and its marketing sender otherwise.

**Known recipients only, by default.** The address the message goes to must belong to a contact of the brand – whoever on the team added it – or to a contact with no brand, otherwise `403 unknown_recipient`. An `external_id` sent with it must be the same contact, otherwise `403 recipient_mismatch`. `allow_unknown: true` lifts that one check. Blocked lists – the account owner's included – hard suppressions (`403 suppressed`) and GDPR erasures (`410 identity_erased`) apply either way; if erasures cannot be checked, the send answers `503 erasure_check_unavailable` and nothing goes out. If blocked lists or opt-outs cannot be checked, the send answers `503 suppression_unavailable` and nothing goes out; retry with the same `idempotency_key`. SMS answers the same (it used to answer `502`).

A successful send answers `200` with `channel`, `status` and, where there is one, `provider_message_id`. Errors are `{ "error": { "code", "message" } }`.

**Result unknown.** Every email, SMS, WhatsApp and push message goes out once. When the provider gives no clear answer (no answer in time, a connection that broke after the message was handed over, or a server error), the message may have been delivered: the send answers `202` with `"status": "unconfirmed"`, `"code": "outcome_unknown"`, a `message` and the provider's words in `provider_message`. It was not sent again, and a retry with the same `idempotency_key` gets the same `202` (`"replayed": true`) and never sends. Do not retry it as a failure; send a new message under a new key only when a second copy is acceptable, such as a fresh one-time code. A send that never left, or that the provider refused or asked to wait (`429`, `503`), answers `502 provider_error`: nothing was delivered, so retry it with the same key. A push answers `422 no_device` only when no device is registered for the person, which a retry cannot change.

## 6. Start a journey for one person

Scope `journeys:trigger`. `POST /api/journeys/trigger` starts a published journey from the brand's own systems – "order shipped", "deposit made", "ticket resolved".

```bash
curl -X POST https://theaicmo.com/api/journeys/trigger \
  -H "Authorization: Bearer $THEAICMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "journey_id": "<journey id>",
    "email": "customer@example.com",
    "event": { "order_number": "1042", "amount": 89.90 },
    "idempotency_key": "order-1042-shipped"
  }'
```

- Identify the person by `email`, `external_id`, or both. Both must describe one contact, otherwise `409 identity_conflict`. Someone who is not a contact yet is created.
- A person this call creates starts with no marketing consent: a marketing journey sends them no e-mail, a service journey still does. Send `"consent": { "email": true }` when your own system holds their e-mail marketing consent; it is recorded on their consent history. For someone already on record it counts only when that "Not granted" is all that stops marketing to them (never over an unsubscribe or a withdrawal); it is never read as a withdrawal. Anything but an object is `400 invalid_consent`.
- `client_id` is optional: the journey's own brand is used. The key may start any journey on a brand its owner may change, whoever built it.
- `event` carries up to 20 properties – text, numbers or true/false; text is cut at 500 characters. The journey's messages read them as `{{ event.order_number }}`.
- The reply is `202 { enrollment_id, status: "queued" }`, or `200 { enrollment_id, deduped: true }` when the person is already in the journey or the key was seen before. The key identifies the run: reusing it for someone else returns the first run.
- Refusals are `409` with a code: `journey_paused`, `contact_bounced`, `contact_not_active`, `already_ran` or `reentry_too_soon`; `409 trigger_in_progress` while a call with the same key is still being answered (retry after `Retry-After`). Suppression and GDPR erasure apply, and if erasure cannot be checked the call answers `503 erasure_check_unavailable` with nothing enrolled.
- The journey's id and a ready-made call are on its trigger settings in the product.

## 7. Loyalty

Scope `loyalty`.

- `GET /api/loyalty/balance?identity=<e-mail or customer id>` – points, tier, and the programme's tiers and terms.
- `GET /api/loyalty/rewards` – up to 100 active rewards with a points price: the brand's own and account-wide ones. Add the customer to ask for one person: `?identity=P-10234` (your own customer id), `?identity=anna@example.com`, or both as `?identity=P-10234&email=anna@example.com`. Use the identifier your loyalty data is keyed on as `identity` and pass the other as `email` or `external_id`, so both are checked; `phone` is checked too. Someone who cannot redeem gets `403 not_eligible` and no rewards.
- `POST /api/loyalty/redeem` with `{ identity, promo_id, points?, client_id?, idempotency_key?, note?, email?, external_id?, phone? }` – spends points and grants the reward in one step. It answers `{ ok: true, grantId, promoName, spent, balance, tier }`. The same `idempotency_key` returns `{ ok: true, duplicate: true, grantId, balance, tier }` and never spends twice. A redemption larger than the balance is refused with `402 insufficient_points`, carrying `balance` and `required`; a balance can never go below zero this way. `email`, `external_id` and `phone` are other identifiers of the same customer, checked against the brand's blacklists; the points are spent from `identity`.
- A customer on a blacklist that blocks everything cannot redeem: `403 not_eligible`, also on a retry of an earlier key, and the answer never says which list. If that check cannot run, the answer is `503 suppression_unavailable` with `Retry-After` and nothing is redeemed; retry with the same `idempotency_key`.

Full walkthrough: https://theaicmo.com/docs/loyalty-program

## Errors

Every error is JSON. On `/api/send` it is `{ "error": { "code", "message" } }`; everywhere else `error` is the code, usually with a `message` beside it.

| Status | Meaning |
| --- | --- |
| `400` | A field is missing or invalid, or `client_id` is needed |
| `401` | Missing, wrong, revoked or expired key |
| `402` | Out of credits (tools, agent) or points (loyalty), or the brand's credit budget is used up |
| `403` | The key lacks the scope, the brand is not one the key may act for (`read_only` for a view-only role), the call comes from an address a pinned key does not allow (`ip_not_permitted`), the recipient is not allowed, or the customer cannot redeem rewards (`not_eligible`) |
| `404` | Not found, not this account's, or an unknown `tool_id` (the reply lists `availableTools`) |
| `409` | An idempotency or identity conflict, a run still in progress, or a journey rule |
| `410` | The person was erased on request and cannot be messaged or enrolled |
| `422` | No verified sending identity, nothing to send, or a tool reported a failure |
| `429` | Rate limited. Wait `Retry-After` seconds, then retry |
| `502`, `503` | Could not finish, or could not check. Retry – with the same `idempotency_key` where the endpoint takes one (on `/api/send`, nothing was sent) |

`202 outcome_unknown` on `/api/send` is not an error: the message may have been delivered and is never sent again.

## Working rules

1. Prefer `/api/agent-api`. Use `/api/execute-tool-api` when you know the exact tool.
2. Tell the person what you spent. Every reply carries `credits_used`.
3. Send an `idempotency_key` on every tool run, send, trigger and redemption, and reuse the same key when you retry.
4. Never use `/api/send` for marketing.
5. Never ask for the key in the conversation, and never put it in code that runs in a browser.
6. When the account has several brands, confirm which one before you act.

Documentation: https://theaicmo.com/docs/api
