mxAURA™Developer Center
API ReferenceResource reference

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.

json
{ "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.

json
{ "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, html when 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 with eligible and reason, e.g. opportunity_type is_equal_to [Partner] (got "Client").
  • would_send: false with would_not_send_because when 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_soon is no longer returned.
  • priority: true when a priority row serves the message; that row's reason reads matched; 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:

html
<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.
  • html is one <article class="mxa-article" data-mxa-static>: <h1>, the summary (.mxa-dek), the byline (.mxa-byline with 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.
  • jsonld is the graph for a <script type="application/ld+json">: Article, with FAQPage when there are approved FAQs. Put it on the page only together with html, since FAQ schema needs the questions visible on the same page.
  • Put html inside the mount element. embed.js keeps 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.
  • ETag and If-None-Match are honoured (304). Cached at the edge for 60 seconds. Keep your last good copy and serve it if the hub does not answer.
  • noindex is 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
text
# 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: 1 and 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; use content.json in 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):

json
{ "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).