Resource reference
Every path below is relative to https://api.mxaura.ai/v1. {id} is a UUID.
The scope column is what the credential must hold; read for GET, write
for everything else unless noted. Request and response schemas are in the
OpenAPI document at /v1/openapi.json; this page is the map.
Ids of objects that do not belong to your account behave as if they do not
exist (404), never as 403. The API does not confirm what it will not show.
Me
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /me |
any | The credential: key id, account, scopes, limit, status |
Components (components.*)
Six component types share one shape: messages, headers, summaries,
signatures, senders, templates.
| Method | Path | Notes |
|---|---|---|
| GET | /{type} |
List (paginated) |
| POST | /{type} |
Create |
| GET | /{type}/{id} |
One |
| PUT | /{type}/{id} |
Update |
| DELETE | /{type} |
Bulk delete; body { "ids": [...] } |
| GET | /{type}/{id}/dependencies |
What uses this component (pools, messages). Check before deleting |
A header holds one value per field; the API refuses multi-value fields
(400). A template is the HubSpot email template the message renders
into; hs_template_id ties it to the portal.
Composition model (components.*, pools.*)
Templates are imported from HubSpot marketing emails with a manifest: one
field per mxa_* token, with a label, a type (text, html, url,
image_url), whether it is multivalue, whether it is required, and whether
Branding fills it. A message types a value into each field. A pool holds an ordered sequence of
messages; the same message may sit in several pools.
| Method | Path | Scope | Notes |
|---|---|---|---|
| POST | /templates/import |
components.write | { hs_email_id }. Infers field types from where each token sits. Re-imports an email already imported |
| POST | /templates/{id}/reimport |
components.write | Human labels and declared types survive; removed tokens become orphaned |
| GET / PUT | /templates/{id}/manifest |
components.read / write | Fields, sections, unresolved, ready. A type set by PUT is declared |
| GET | /pools/{id}/sequence |
pools.read | Rows with membership_id, fill counts, status, shared |
| POST | /pools/{id}/sequence |
pools.write | { message_id } or { compose: { name, template_id, copy_from } }, position |
| PATCH | /pools/{id}/sequence/reorder |
pools.write | { order: [membership_id] } |
| DELETE | /pools/{id}/sequence/{membershipId} |
pools.write | Removes from this pool only |
| POST | /pools/{id}/sequence/{membershipId}/detach |
pools.write | Private copy of a shared message |
| GET / PUT | /messages/{id}/bindings |
components.read / write | Per-field validation; errors returned per field, valid ones save; status flips draft/ready |
| GET | /messages/{id}/usage |
components.read | Pools this message sits in |
| POST | /advisor/rows |
ai.write | Drafts for the next rows; nothing saved. Metered |
| POST | /advisor/variants |
ai.write | Alternatives for one text/html field. Metered |
| POST | /preview/url |
campaigns.read | Signed 10-minute link rendering the message for a contact (next or last_sent) |
Dispatch runs a pool on this model as soon as it has a sequence row whose
message is ready; pools without one keep the legacy path.
Brandings (brandings.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /brandings |
List / create |
| GET / PUT | /brandings/{id} |
One / update |
| DELETE | /brandings |
Bulk; { "ids": [...] } |
/brandings/provision and /deprovision (HubSpot theme provisioning) are app-only.
Pools (pools.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /pools |
List / create |
| GET / PUT | /pools/{id} |
One / update |
| GET | /pools/{id}/sequence |
The ordered messages in the pool, each with its membership_id, template and fill summary |
| POST | /pools/{id}/sequence |
Add a message: { "message_id" }, or compose a new one { "compose": { name, template_id, copy_from? } }; optional position |
| PATCH | /pools/{id}/sequence/reorder |
{ "order": [membership_id, ...] } |
| DELETE | /pools/{id}/sequence/{membershipId} |
Remove from this pool; the message stays |
Writes to /pools/{id}/messages (POST, PUT, DELETE) are retired and answer 410; its GET still answers in an older shape. Use /pools/{id}/sequence.
| GET | /pools/{id}/dependencies | Campaigns and flows that draw from this pool |
| PUT | /pools/{id}/metadata | Name, description, pool type, nurture mode |
| POST | /pools/{id}/summary-preview | Render a summary variant (read-only) |
/pools/{id}/test-deploy is app-only.
Campaigns (campaigns.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /campaigns |
List / create |
| GET / PUT | /campaigns/{id} |
One / update (enabled is the switch HubSpot respects) |
| DELETE | /campaigns |
Bulk |
| GET / POST | /campaigns/{id}/pools |
Pools assigned to the campaign / assign one (allow_duplicate: true assigns a pool again for another audience) |
| PUT / DELETE | /campaigns/{id}/pools/{cpId} |
Update the row / unassign. PUT takes property_filters, priority, pool_order, nurture_mode, pool_type, sender_id, sender_type, branding_id, schedule_start, schedule_end, expiry_date, priority_order. Fields left out, null or "" are kept; "__clear__" clears one. priority: true needs a limited nurture row (send nurture_mode: "limited" with it); 400 otherwise |
| PUT | /campaigns/{id}/pools/reorder |
{ "updates": [{ "id": cpId, "pool_order": 0, "priority_order": 0 }, …] } |
| GET / PUT | /campaigns/{id}/pools/{cpId}/variants |
Block variants on a row, in order (first match wins; no match gets the row's default blocks, and a section a variant leaves out comes from the default). PUT replaces the whole list: { "variants": [{ id?, name, property_filters: { logic, filters }, bindings: [{ kind_id, source: set | block | rotation | rotate_all, set_id?, block_id?, rotation_id?, mode? }] }] }. Keep each id from the GET to keep its send history. GET also returns row_filter, kinds, max and warnings. Overlap between variants is allowed. 422 when a variant can never apply: its filter contradicts the row's, a variant above it has no filter, or one above it has the same filter; nothing is written |
| GET / PUT | /campaigns/{id}/pools/{cpId}/bindings |
Blocks bound on this assignment. PUT patches: { "bindings": [{ kind_id, source: inherit | block | rotation | rotate_all, block_id?, rotation_id?, mode: sequential | locked | shuffle_per_rotation }] } |
| PUT | /campaigns/{id}/pools/{cpId}/pins |
{ "pins": [{ pool_message_id, kind_id, block_id | null }] }: one message always gets one block here |
| GET | /campaigns/{id}/clients |
Agency: which client accounts this campaign is assigned to |
| GET | /campaigns/{id}/dependencies |
Flows and workflows referencing it |
| POST | /campaigns/{id}/preview |
Render a message as a given contact would receive it (read-only) |
| POST | /preview/simulate-campaign |
The full cascade journey for a contact. See below |
| POST | /dispatch/preview |
The message a contact would get next, writing nothing. See The dispatcher |
Simulate a campaign
POST /preview/simulate-campaign runs the same engine dispatch uses and
returns the ordered steps a contact would receive. Read-only.
{ "campaignId": "…", "contactId": "124303957916", "maxSteps": 5, "maxDays": 30 }contactId (or hs_contact_id) is optional. With it, the contact's
properties and lists are read from HubSpot as dispatch reads them, and rows
whose filters they fail are skipped. Without it the simulation shows every
row's full sequence. The response lists each step's pool,
message, position, resolved sender/branding and the reason it was chosen
(or skipped: filtered, exhausted, window closed).
The dispatcher
POST /dispatch/preview runs the production cascade for one HubSpot
contact: the same property read (every property a row, a block variant or a
list filters on), the rows inside their schedule window, the filters, the
contact's pool progress, block variants, pins and rotations. It answers with
exactly what that contact would get next.
{ "campaign_id": "…", "hs_contact_id": "124303957916", "commit": false }| Field | Notes |
|---|---|
commit |
false (default) writes nothing: no progress, no rotation, no dispatch log, no stats, no HubSpot property. Its links and open pixel carry a test tracking id that records nothing, so a test email cannot move a number. Needs campaigns.read. true is a real dispatch (below) and needs dispatch.write |
now |
Preview only. Evaluate schedule windows on this date |
portal_id |
An agency names its client's portal; otherwise the account's own |
write_properties |
With commit: true: also write the message to the HubSpot contact, as the workflow action does |
include_html |
Return the rendered email |
The answer, in data:
status:message,no_message(nothing left, or nothing in window),filtered(every row in window failed its filter),blocked(the message cannot be filled:reason_code,reason),list_unavailable.- With a message:
campaign_pool_id,row_order,pool_id,pool_name,message_id,message_name,position_in_pool(from 1),pool_size,variant,template_name,sender { name, email },subject,preview_text,blocks [{ kind, set, entry }],content(every field value),tracking_id,htmlwhen asked. progress_key:contact+pool. Progress belongs to the pool: a pool on two rows continues where the contact left it, whichever row admits them.properties_read: what was read from HubSpot.rows_evaluated: every row in order witheligibleandreason, e.g.opportunity_type is_equal_to [Partner] (got "Client").would_send: falsewithwould_not_send_becausewhen the campaign is off or stopped by the Monitor: the preview still shows the message.- A campaign has no send spacing (v3.1.941): it answers whenever it is called. Timing lives in the flow or the HubSpot workflow.
too_soonis no longer returned. priority: truewhen a priority row serves the message; that row's reason readsmatched; priority.
Committing and sending (dispatch.write)
commit: true on /dispatch/preview, or POST /dispatch/send, is a
production dispatch: the contact advances, rotations move, the audit row and
a dispatch log row are written (source: api_commit or api_send), pool,
campaign and message stats count the send, and dispatch.sent fires. It
sends nothing itself: send the returned message (content, properties,
html), or pass write_properties: true and let the HubSpot email that
follows render it. Refused with 409 when the campaign is off or stopped by
the Monitor, 503 when HubSpot would not say which lists the contact is in.
Send an Idempotency-Key: a retry returns the first answer and advances
nothing.
Flows (flows.*) and enrollments (enrollments.write)
A flow governs a contact's journey through campaigns. Enrolling a contact causes real dispatches, meaning HubSpot property writes and real email. It is a separate scope on purpose and it is capped per hour.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET / POST | /flows |
flows | List / create |
| GET / PUT | /flows/{id} |
flows | One / update |
| PUT | /flows/{id}/status |
flows.write | active / paused / archived |
| GET | /flows/{id}/enrollments |
flows.read | Contacts in the flow, with position and next send |
| GET | /flows/{id}/stats |
flows.read | Daily enrolled / sent / opened / clicked / completed |
| POST | /flows/{id}/preview-next |
flows.read | What a given contact would get next (read-only) |
| GET | /flows/by-campaign/{campaignId} |
flows.read | Flows that use a campaign |
| POST | /flows/{id}/enroll |
enrollments.write | { "hs_contact_id": "…" }. Sends email and counts toward the hourly cap |
| POST | /flows/{id}/unenroll |
enrollments.write | { "hs_contact_id": "…" } |
Flow blueprints (agencies)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /flow-blueprints |
An agency's reusable flow definitions |
| GET / PUT | /flow-blueprints/{id} |
|
| GET | /flow-blueprints/{id}/assignments |
Which client accounts have it |
| POST | /flow-blueprints/{id}/assign · /unassign |
{ "client_account_id": "…" } |
/flows/scan/run is app-only.
Dispatch & activity (dispatch.read)
What was sent, to whom, with what outcome. A row a third party committed carries source (api_commit, api_send).
| Method | Path | Notes |
|---|---|---|
| GET | /activity/dispatches |
The dispatch log: campaign, pool, message, contact, status (success / no_message / filtered / blocked / throttled / write_failed / error), latency. Filters: ?campaign_id= ?pool_id= ?dispatch_status= ?days= or ?date_from=&date_to=, ?recipient= |
| GET | /activity/feed |
Human-readable account activity (who did what in the app). Filters: ?category= ?event_type= ?days= |
A blocked dispatch carries a reason_code: no_sender, no_branding,
reauth_required, campaign_disabled. These are the ones a customer must
act on; the dispatch.blocked webhook fires for them.
Hubs (hubs.*)
A hub is an embeddable library of articles, served to sites through an embed key. The family is split into four scopes so a key can hold the content library without the ability to hand out embed keys or spend model budget.
Hubs themselves (hubs.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /hubs |
List / create. Each hub carries default_key, preview_url, embed_url, feed_url |
| GET / PUT | /hubs/{id} |
One (settings, layouts, subscriptions, site path) / update. Gallery settings in settings_json include featured_view (header, full or row: where featured articles show), featured_order (article ids, the order of the features), pinned (article ids that lead the card list in this order, before the gallery's sort) and featured_max |
| GET | /hubs/{id}/stats |
Views by period: `?window=1h |
| GET | /hubs/stats/views |
Views per article and per hub for a period, for lists: `?window=all |
| GET / PUT | /hubs/{id}/fields |
The hub's field configuration |
| GET / POST | /hubs/{id}/layouts |
Layouts (gallery / entry HTML+CSS). A save is refused (422) when the layout would break the page: unclosed sections, no article loop or link, no body, an HTML field in two braces |
| PUT / DELETE | /hubs/{id}/layouts/{layoutId} |
Same validation on update; warnings returned with 200 |
| POST | /hubs/{id}/layouts/validate |
{ kind, template_html, css_text } returns { ok, errors, warnings } without saving |
| POST | /hubs/{id}/layouts/copy |
{ from_hub_id } copies another hub's layouts and style in as new, non-default layouts |
| POST | /hubs/{id}/layouts/preset |
Apply a named preset |
| GET | /hubs/{id}/library |
Articles available to place |
| GET / POST | /hubs/types · /hubs/types/{id} · /hubs/types/{id}/fields |
Content types and their fields. Roles: title, summary, body (richtext, textarea or markdown), image, category, date, author, and seo_title, meta_description, og_image (the served page's <title>, meta description and share image, server-rendered, with fallbacks to title, answer layer / summary and image), social_long, social_short (where social_posts jobs write) |
| GET | /hubs/types/{id}/dependencies |
Articles and live hubs holding the type (delete is refused while any exist) |
| POST | /hubs/types/clone |
{ ids } copies types with every field, their own slug, no default category |
| GET | /hubs/subscribed · /hubs/subscribed/{id} |
Hubs this account embeds from other accounts |
DELETE /hubs/{id} is app-only (it cascades into every client site).
/hubs/scheduler/run is app-only.
Articles & authors (articles.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /hubs/articles |
List (?state=draft|live|paused; every article carries state) / create. A slug you send is kept exactly: if another article of the type has it, 409 { code: "slug_conflict", existing_id } and nothing is created (no "-2"). Leave it out and one is made from the title. An article created untitled gets a placeholder slug (untitled, untitled-3); the first update that carries a real title replaces it with the slug that title makes, unless the update sends a slug or the article has ever been live. Every update answers with the current slug. Importing: display_date (YYYY-MM-DD or ISO) is the date readers see and galleries sort by; created_at and published_at set the record's own stamps so the library and the editor show the original dates (created_at is never cleared; published_at: null clears it; neither publishes anything). With created_at and no display_date the display date follows it. Left out, the stamps never move |
| DELETE | /hubs/articles |
Delete permanently { ids }, scope articles.delete. Refused with 409 has_placements while an article is in a hub; use /hubs/articles/{id}/pause (and /live to restore) for a reversible take-down. In the app, Delete removes the article from its hubs itself; either way each hub it was live in keeps a 301 from its address to the hub's home page |
| GET | /hubs/articles/publish-targets?ids= |
Every hub with how many of these articles it holds and shows, and its widgets (assignable: false for a widget that shows every article). Scope articles.read |
| POST | /hubs/articles/publish |
{ article_ids, hub_ids, surface_ids?, action?: "publish" | "schedule" | "add", publish_at?, unpublish_at?, make_live? }: place, publish (required fields checked) and add to widgets in one call. An article that is not live is skipped unless make_live. Scope publishing.write |
| GET / DELETE | /hubs/{id}/redirects |
Addresses of deleted articles, each a 301 to the hub's home page; DELETE { ids } removes them. A live article with the same slug wins |
| POST | /hubs/articles/{id}/ai/seo |
Suggest SEO: { fields?, write?: "suggest" | "live" }. Tags 3 to 8 matched to tags in use; category only from existing ones; live writes exactly the fields named; response carries provenance (packs used, model), never pack text |
| GET / PUT | /hubs/articles/{id}/faqs |
The answer layer: answer_block and FAQs with approved. PUT replaces the FAQ list: read it, then send every FAQ back with approved: true on the ones to make public |
| POST | /hubs/articles/{id}/ai/apply · /hubs/articles/{id}/ai/discard |
The AI review slot. apply { fields?, job_id? } copies the draft in data.ai_review into the live article; discard clears it |
| GET / PUT | /hubs/articles/{id} |
One / update. Includes placements, SEO fields, stats |
| GET | /hubs/articles/{id}/stats |
Views by day |
| GET / POST | /hubs/authors · /hubs/authors/{id} |
Authors (name, bio, avatar) |
Publishing (publishing.*)
Anything that changes what the public can see.
| Method | Path | Notes |
|---|---|---|
| GET | /hubs/{id}/entries |
Articles placed in the hub with placement status |
| POST | /hubs/{id}/entries |
Place an existing article: { "article_id", "status"?: "unpublished" (default) | "published" | "scheduled", "publish_at"?, "featured"?, "display_date"? }. display_date (YYYY-MM-DD or ISO) is the date readers see and the gallery sorts by; it keeps an imported article's original date and schedules nothing. Never creates one: a title, data or slug without article_id is refused with 400 use_compose_endpoint |
| POST | /hubs/{id}/compose |
Make a new article of the hub's type and place it in one step. A slug you send is kept exactly or refused with 409 slug_conflict |
| POST | /hubs/{id}/entries/{articleId}/publish · unpublish · schedule · feature · unfeature |
schedule takes { "publish_at": "…", "unpublish_at": "…" } |
| POST | /hubs/{id}/entries/bulk |
One action for up to 200 articles: { "article_ids": [...] or "items": [{ "article_id", "display_date" }], "action": "publish" | "schedule" | "unpublish" | "remove", "publish_at"?, "unpublish_at"?, "display_date"? }. An invalid date is refused before anything changes. schedule needs publish_at. Returns { done, errors: [{ id, error }] }; each article is done on its own |
| POST | /hubs/{id}/entries/reorder |
{ article_ids } sets the gallery's manual order (the hub's gallery_order setting must be manual) |
| POST | /hubs/articles/{id}/live · pause |
Article state. draft: never set live, cannot be placed in a hub. live: hubs place and publish it. paused: hidden everywhere, kept in its hubs and listed there as Paused. Older names: publish, activate = live; unpublish, deactivate, retire = pause |
| POST | /hubs/{id}/preview |
{ "minutes": 60 } opens a preview window: drafts become visible on the live key for that long, with noindex and no caching. { "minutes": 0 } closes it. |
Sites (subscribers.*)
Where a hub is published. A subscription is one site: its embed key, allowed origins, path, brand and layout pins. (The scope keeps its older name.)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /hubs/{id}/subscriptions |
Subscriptions = embed keys for this hub (own site or a client's) |
| PUT / DELETE | /hubs/{id}/subscriptions/{subId} |
allowed_origins (first is canonical; www twins are added), mount_path override, overrides_json.branding_id (a Brand Kit id, or client for the client account's default brand), layout pins, status |
| POST | /hubs/{id}/subscriptions/{subId}/check |
Looks at the site: is the embed on the page, which address style it uses, and whether the server sends article addresses to the gallery page (clean_works) |
| GET / POST | /hubs/site-keys · /hubs/site-keys/{id} |
Site keys: one key a client site uses for every hub assigned to it |
| GET / POST / DELETE | /hubs/assignments · /hubs/assignments/{id} |
Assign a hub to a client account |
POST /hubs/site-keys/{id}/rotate is app-only (revokes every live embed).
Public rendering (no credential)
https://hubs.mxaura.ai/e/{embed_key}/ renders a hub; …/feed.json is its
feed; …/{slug}/ an entry. ?published=1 forces published-only even when a
preview window is open. Embed with:
<script src="https://hubs.mxaura.ai/embed.js" data-mxa-hub="EMBED_KEY"></script>or, for a client site, data-mxa-site="SITE_KEY".
Article content for your own page
The gallery is a frame. A crawler that does not run scripts (every AI crawler) or does not credit framed text to the page that frames it gets only what your server sends at the article's address. …/{slug}/content.json is the article for your server to put in that page.
| Status | Body |
|---|---|
| 200 | { slug, url, title, headline, description, published, modified, author, image, noindex, answer, faqs: [{ question, answer }], html, jsonld } |
| 301 | { moved_to: { slug, url, content } } and a Location header: the slug was renamed. Redirect your reader to url. For a deleted article slug and content are null, reason is deleted and url is the hub's home page. |
| 404 | { error: "not_found" }. Never a redirect to the index: answer 404 on your site too. |
htmlis one<article class="mxa-article" data-mxa-static>:<h1>, the summary (.mxa-dek), the byline (.mxa-bylinewith a<time>), the lead answer (.mxa-answer), the body (.mxa-body) and the approved FAQs (.mxa-faq,<h3>question and<p>answer). A text snapshot: scripts, styles, frames, forms and inline handlers are removed. A link written as a path is absolute on your site.jsonldis the graph for a<script type="application/ld+json">: Article, with FAQPage when there are approved FAQs. Put it on the page only together withhtml, since FAQ schema needs the questions visible on the same page.- Put
htmlinside the mount element.embed.jskeeps it on screen until the gallery has loaded, then hides it (it stays in the document). If the gallery never loads, the article stays readable. ETagandIf-None-Matchare honoured (304). Cached at the edge for 60 seconds. Keep your last good copy and serve it if the hub does not answer.noindexis true for a preview key; do not index those.
Or let mxAURA build the page (one rewrite line)
Instead of placing html yourself, proxy your article addresses to …/_site/. mxAURA fetches your own gallery page, swaps its head for the article's (title, description, canonical, share tags, JSON-LD), puts the article HTML inside the mount element, and returns the finished page. No code on your side.
| Address under your mount | Returned |
|---|---|
/insights/<slug>/ |
200, your page with the article in it |
/insights/<old-slug>/ |
301 to the article's current address on your site |
/insights/<no-such>/ |
404, your gallery page marked noindex |
/insights/page/2/ |
200, your gallery page unchanged |
# Netlify, _redirects. No "!" so /insights/ itself stays your own page.
/insights/* https://hubs.mxaura.ai/e/EMBED_KEY/_site/:splat 200
# Vercel, vercel.json
{ "rewrites": [{ "source": "/insights/:path+", "destination": "https://hubs.mxaura.ai/e/EMBED_KEY/_site/:path+" }] }
# nginx
location ~ ^/insights/(.+)$ { proxy_pass https://hubs.mxaura.ai/e/EMBED_KEY/_site/$1; proxy_set_header Host hubs.mxaura.ai; proxy_ssl_server_name on; }- Your gallery page at the mount path must be served by your own site (do not proxy it), over https. It is fetched with the header
x-mxa-shell: 1and kept for five minutes; the last good copy is used for a day if your index is briefly unreachable. - Your page's own title, description, canonical, share tags and JSON-LD are replaced on article addresses. Everything else in your head and body stays.
- Cloudflare Pages cannot proxy to another host from
_redirects; usecontent.jsonin a Pages function there. - Remove any function of your own that was adding share tags on these paths; the page now arrives with them.
AI (ai.write)
Calls that spend the account's AI allowance (or a paid web search). Metered per account and
limited to 30 a minute per account, shared with the app. When the allowance refuses: 402
budget_exhausted with error.reason (exhausted, would_exceed, disabled, new_account,
email_unverified); see Troubleshooting.
| Method | Path | Notes |
|---|---|---|
| POST | /hubs/articles/{id}/ai/seo |
Title/description/keywords suggestions |
| POST | /hubs/articles/{id}/ai/hero-spec |
Hero image spec + generation |
| GET / POST | /hubs/ai/jobs · /hubs/ai/jobs/{id} |
AI jobs. kind is article_draft (default), image, social_posts (long post 600 to 1,300 characters ending with an invitation to read, short post under 280; the article's address is appended to both once it is on a site; never against the firm's positions) or answer_block (lead answer + FAQs; applied FAQs arrive unapproved); anything else is refused with 400 unknown_kind before anything is queued or spent. POST { kind: article_draft, article_id, brief, source_url?, length?, write?: "review" | "live" } starts a draft; poll the job until status is done. Through the API a draft for an existing article goes to the review slot (data.ai_review), never the live fields; write: "live" is honoured only for an article with no body that is not published. Runs on mxAURA's AI with your identity and prompt packs; pack text is never returned article_draft reads its sources: source_url is a Library file read in full, sources is up to 12 web links whose article text is fetched and used (first five); the result lists sources_unread with a reason for any that could not be read. |
| POST | /identity/brand-brief |
{ branding_id, framework_id?, notes? } drafts a Brand Kit's About this brand; returns { markdown } |
| POST | /hubs/{id}/ai/seo-batch |
SEO for every article in a hub |
| POST | /identity/intake · /identity/sources/refresh · /identity/sources/extract |
Identity generation |
| GET / POST | /advisor/plans · /advisor/plans/{id} · /advisor/plans/{id}/items |
Campaign plans from a Campaign Brief. Each planned pool says use, adapt or create against existing pools; approving items creates or clones them |
| POST | /advisor/rows · /advisor/variants |
The Composer: pool rows drafted against the brief, positioning, brand brief and the plan; review: true runs the message rubric pass |
| POST | /advisor/review |
Review messages against the critics (writes nothing to the messages) |
| POST | /identity/ask |
Ask mxAURA about mxAURA, answered from the help and developer docs. 40 an hour per account |
| POST | /identity/generate · /identity/fix · /identity/rate |
Draft, repair and rate positioning sections |
| POST | /identity/start (and its steps) |
Baseline: the analysis behind Find, then confirm |
| POST | /identity/suggestions/refresh · /identity/targets/draft · /identity/compare/rate · /identity/guardrails/suggest · /identity/guardrails/fix |
Article suggestions, ICP drafts, side-by-side rating, guardrail drafting and repair |
| POST | /identity/competitors/find |
Competitor candidates from the home website (paid search); nothing added until you add one |
| POST | /identity/competitors/compare · /identity/competitors/{id}/read |
Read a competitor's site; compare pillar by pillar |
| POST | /identity/market/brief · /identity/market/topics/run |
The market brief (search plus one model call; uncited shifts dropped) and the topic check (search only) |
| POST | /hubs/evidence/sources/{id}/read |
Read a source into claims, each with the exact words it rests on |
| POST | /hubs/articles/ai/outline · /hubs/articles/ai/source-brief · /hubs/ai/images · /hubs/brief/suggestion · /hubs/eval |
Outlines, source briefs, images, brief suggestions, the Quality test |
| POST | /hubs/articles/{id}/social · /hubs/articles/{id}/faqs/generate |
Social posts; lead answer and FAQs (arrive unapproved) |
| POST | /hubs/authors/{id}/voice/generate · /hubs/authors/{id}/voice/test |
Author voice profiles (a person approves before use) |
Publishing an article with a key that lacks ai.write keeps the edit record and skips the
suggestions it would otherwise draft from your edits.
Identity (identity.*)
The account's messaging identity. Audience, value proposition, persona and voice, which is what every message and article composes from.
| Method | Path | Notes |
|---|---|---|
| GET | /identity |
Frameworks |
| GET / PUT | /identity/{id} |
One framework (DELETE is app-only) |
| GET | /identity/challenges |
Challenge log |
| GET | /identity/sources · /identity/sources/{id} |
Source documents and extracts |
| GET / POST / DELETE | /identity/competitors · /identity/competitors/{id} |
Competitors on a positioning, by website, with read age (reading and finding are ai.write) |
| GET | /identity/market |
The latest market brief, tracked topics and their hits |
| POST / DELETE | /identity/market/topics · /identity/market/topics/{id} |
Track a topic (ten at most) |
| PUT | /identity/market/hits/{id} |
{ status: "added" | "dismissed" } |
Evidence library (articles.*)
| Method | Path | Notes |
|---|---|---|
| GET | /hubs/evidence |
Sources, claims and their grounds |
| POST | /hubs/evidence/scan |
Collect sources from your own pages and articles (reads nothing) |
| POST | /hubs/evidence/sources |
{ url, title? } a web source |
| POST | /hubs/evidence/documents |
{ media_id, party?: "third" } a PDF, Word or PowerPoint file from the Library |
| DELETE | /hubs/evidence/sources/{id} |
Remove a source and its claims |
| PUT | /hubs/evidence/claims |
{ ids, status?, permission?, pillar?, icp? } decide claims |
Reading a source is ai.write (above).
Radar (campaigns.*, Ask ai.write)
Radar's reads are data: the same reads Radar Ask makes, returned without a model. Ask is Radar's analyst answering in plain words, on the account's AI allowance.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /radar/tools |
campaigns.read | The reads, each with name, description and input_schema |
| POST | /radar/read |
campaigns.read | { tool, args } runs one read: { tool, args, result }, at most 60 rows and 9,000 characters. Nothing is spent or changed |
| POST | /radar/ask |
ai.write | { question, thread_id? }: at most 8 reads, then { thread_id, turn: { answer, confident, gaps, citations, moves, reads } }. 30 questions an hour; 402 when the allowance refuses |
| GET | /radar/threads · /radar/threads/{id} |
campaigns.read | Ask threads of the person the credential belongs to |
| GET | /radar/review |
campaigns.read | The week in review: last 7 days against the 7 before, as cards. ?fresh=1 reads again |
| GET | /radar/review/agency · /radar/agency/learning |
campaigns.read | Agency accounts: clients this week; what works across clients (90 days) |
| GET | /radar/findings |
campaigns.read | Findings from the last run, dismissed ones left out |
| POST | /radar/findings/run |
campaigns.read | Run the checks (rules, no model). Rewrites only Radar's findings list |
| POST | /radar/findings/{id}/dismiss |
campaigns.write | Quiet for 30 days, sooner back if it gets worse |
The market read (text from other people's websites) is app-only, and Ask on the API does not use it.
Deleting a thread is app-only.
The Architect (campaigns.*, model stages ai.write)
The Architect plans a campaign stage by stage: brief, research, suggestion, templates and blocks, content, Setup HubSpot, HubSpot automation; the review runs on confirm; Go builds in mxAURA. Every stage is proposed by a model call (setup is a code diff), confirmed by you, and read by the next one; a plan keeps every version. Plan bodies carry briefs, rows, filters and plans, never message bodies.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /architect/plans |
campaigns.read | Plans with each stage's status |
| POST | /architect/plans |
campaigns.write | { name? }: a plan with an empty brief |
| GET | /architect/plans/{id} |
campaigns.read | Brief, stages (latest version, status, body), review, build, targets, catalogue |
| DELETE | /architect/plans/{id} |
campaigns.write | The plan and its history; nothing built is touched |
| POST | /architect/plans/{id}/chat |
ai.write | { message }: an intake turn; { reply, brief, complete, plan } |
| POST | /architect/plans/{id}/stages/{stage}/run |
ai.write | research (?fresh=1 reads again), suggestion, templates, content, setup (no model), automation. 409 until the stages before it are confirmed |
| GET | /architect/plans/{id}/stages/{stage} |
campaigns.read | The latest version, raw reads included |
| PUT | /architect/plans/{id}/stages/{stage} |
campaigns.write | { body?, confirm?: true | false, despite_review?: [lines] }. A refused confirm is 422 { review, plan }; the edits are kept |
| GET | /architect/plans/{id}/estimate |
campaigns.read | The bill: rows, pools, messages, Blocks, content, tokens against the allowance, holds |
| POST | /architect/plans/{id}/build |
ai.write + campaigns.write | Go. { despite_review? }. Review first (422), 402 over the allowance, then { plan, build }. Again fills gaps |
| GET | /architect/plans/{id}/catalogue |
campaigns.read | Templates with sections, block kinds and Blocks, pools, the library |
| POST | /architect/plans/{id}/setup/apply |
campaigns.write | { keys? }: create the ticked, missing portal items. Create only |
| POST | /architect/plans/{id}/automation/apply |
campaigns.write | { second? }: the workflow, off (Professional), or the flow and form (Starter) |
| POST | /architect/plans/{id}/templates/register |
campaigns.write | { library_ids? }: the library designs as emails and templates, once each |
| GET | /architect/library/{id} |
campaigns.read | A design's HTML (lib-letter, lib-brief, lib-notice) |
| GET | /architect/tools |
campaigns.read | The reads the Architect makes (Radar's plus the planning reads) |
The review refuses on a rule with the line: a row that admits nobody, a second row of a pool that can
never send, a variant that can never apply, a brief with no claim and no gap (code: cannot be confirmed
past), and what the model refuses on the standing rules (can be, by naming the line in despite_review;
recorded in Activity). Nothing the Architect does enables, publishes or sends.
App-only
DMARC reports and the Google connection (Analytics, Postmaster) are app features. They are not on the API or the MCP server; Radar reads them, so their numbers reach you through Radar.
Folders (folders.*)
| Method | Path |
|---|---|
| GET / POST | /folders |
| GET / PUT / DELETE | /folders/{id} |
Media (media.*)
The account's own library in R2. Third-party drives (/media/providers) are
app-only; /media/usage too.
| Method | Path | Notes |
|---|---|---|
| GET | /media/files |
?folder_id= ?category= ?q=. Paginated by the worker (page, limit) |
| POST | /media/files |
Multipart upload |
| GET / PUT / DELETE | /media/files/{id} |
Rename, move, delete |
| GET / POST | /media/folders · /media/folders/{id} |
Webhooks (webhooks.*)
| Method | Path | Notes |
|---|---|---|
| GET / POST | /webhooks |
List / register an endpoint. Create returns the signing secret once |
| PUT / DELETE | /webhooks/{id} |
Change url/events/status; remove |
| POST | /webhooks/{id}/test |
Send a webhook.test event now |
| POST | /webhooks/{id}/rotate-secret |
New secret; old one stops immediately |
| GET | /webhooks/{id}/deliveries |
Last 100 deliveries with status, attempt, HTTP code, latency |
Details, payloads and verification in Webhooks.
Guardrails (Identity)
Two lists per identity. Words: the AI does not use them, and a code check enforces it. Phrases: instructions the AI follows, such as "Do not use defer as a strategy type. Defer is fine as the word for the result." A phrase that names a word wins over the word: the word is then taught by the phrase and no longer checked by code. Other rule kinds exist through the API (below) and show in the app as phrases. Every rule belongs to one identity (framework_id is required); a generation that names no identity uses the account's default. Taught to the AI through the guardrails/core prompt pack with the rules rendered under it, and enforced by a code check on social posts, the answer layer, article drafts, SEO suggestions, email rows and advisor messages. Severity: block (output not used; 422 from the API), revise (one rewrite naming the rule, then flagged), flag (kept and marked). Saving is the approval: every change saves the next version, which each generation records as guardrails_version.
| Method | Path | Notes |
|---|---|---|
| GET | /identity/guardrails?framework_id= |
words, phrases, the rules behind them, the rendered prompt text, the current version |
| PUT | /identity/guardrails |
{ framework_id, words?, phrases? } (identity.write). words: an array or a comma separated string. phrases: an array or one per line. Each list sent replaces that list; a list not sent is left alone. One version per save |
| POST / PUT | /identity/guardrails · /identity/guardrails/{id} |
One rule of a specific kind (identity.write) |
| DELETE | /identity/guardrails/{id} |
Remove (identity.delete) |
| POST | /identity/guardrails/import |
{ framework_id, rules: [...] }, all-or-nothing, duplicates skipped |
| POST | /identity/guardrails/import-brief |
{ framework_id }: the brief's never-say list becomes rules |
| POST | /identity/guardrails/check |
{ framework_id, text, field } or { items: [{ text, field, at, label }] }: identity.read; returns { pass, worst, issues }, each issue with its at / label |
| POST | /identity/guardrails/self-test |
Each rule against its own examples (identity.read) |
| POST | /identity/guardrails/suggest |
AI Assist suggests words and phrases (ai.write); saves nothing |
| POST | /identity/guardrails/fix |
{ framework_id, items: [{ at, text, field? }] } (ai.write). For each item that breaks a rule the AI makes the smallest edit that works: only the sentence with the conflict changes. Each repair is checked again. Returns { fixes: [{ at, before, after, conflicts, changed, clear, remaining }] }; saves nothing |
An identity's guardrails also travel in its JSON export (framework.guardrails); POST /identity/import takes parts (details, constituents, target_buckets, value_pillars, value_proposition, messaging_ladder, persona, vision, mission, guardrails) to bring in only those, into an identity (mode: "replace", the rest kept) or as a new one.
Load format (one rule per object):
{ "rules": [
{ "section": "instruction", "statement": "Do not use defer as a strategy type. Defer is fine as the word for the result." },
{ "section": "never_say", "term": "loophole", "severity": "block", "why": "Implies evasion", "disallowed": "Use this loophole to cut your bill." },
{ "section": "conditional", "term": "defer", "denied_in": ["title", "subject", "seo_title", "cta"], "allowed_in": ["body"], "condition": "describing how a named statute works", "severity": "revise" },
{ "section": "never_claim", "term": "up to * percent", "severity": "block" },
{ "section": "firm_position", "statement": "Some strategies still help after a sale closes.", "disallowed": "Once the deal closes, your options are gone." },
{ "section": "never_mention", "term": "Dirty Dozen", "severity": "block" },
{ "section": "style", "rule": "no_dashes", "severity": "flag" },
{ "section": "never_claim", "builtin": "irs_endorsement", "severity": "block" },
{ "section": "evidence", "disclaimer": "Results vary; client details are confidential.", "severity": "revise" },
{ "section": "never_say", "term": "fee share", "scope": { "audience": "Attorneys" }, "severity": "block", "why": "Rule 5.4" }
] }In the app an identity's guardrails are the two lists. A word added there is never_say at severity revise; a phrase is instruction (taught, not matched). The other kinds are for the API, and each shows in the app's phrases as its sentence.
Fields for conditional rules: title, subtitle, body, seo_title, meta_description, subject, cta, social_long, social_short, answer, faq_question, faq_answer, tags. Scope: { "audience": "all" | "client" | "partner" | "<bucket or partner type>", "channels": ["all" | "article" | "social" | "faq" | "seo" | "email"] }. A rule for a named audience applies only to copy for that audience (email rows and advisor messages for that bucket). Built-in claims: savings_percent, fee_back, irs_endorsement, guaranteed_outcome. Evidence: figures (percentages, amounts, multiples) must appear in the source the copy was written from; disclaimer must accompany any figure. Guardrails also apply to email rows and advisor messages (v3.1.773).