# Mementom marketing API integration guide The marketing API is company-neutral. Your company can integrate it into its own marketing automation, backend and future AI tools. It uses the same application workspace, validation and saved records as the marketing console. A server can read the insight catalog, build audiences, save offer or questionnaire content, create and edit campaign drafts, control campaigns, count eligible matches and inspect aggregate reports. Changes made through the API appear in the console for that company and application. Aster is a working teaching example that shows the complete message flow. Replace its company, registered insight, content and landing URL with your own configuration; the API has no dependency on Aster. This contract covers campaign authoring, lifecycle control and measurement. A successful draft save does not send an offer or charge anyone. The [Marketing MCP server](/developers/mcp/) is a separate integration layer using this API. - [Marketing API reference](/developers/marketing-api/) - [Aster marketing demonstration](/aster/marketing/) - [Manage API credentials](/marketing/api/#credentials) - [OpenAPI 3.1 contract](/developers/mementom-marketing-openapi.json) - [Server SDK](/developers/downloads/mementom-sdk.mjs) - [Runnable server example with replaceable Aster configuration](/developers/downloads/mementom-aster-marketing.mjs) - [Configurable marketing workflow](/developers/downloads/mementom-aster-marketing-workflow.mjs) - [Developer API reference](/developers/api/) The consolidated server SDK includes marketing alongside the company API and insight helpers. Marketing still requires its own scoped credential; a company connection session or Personal MCP shared key does not supply that authority. ## 1. Connection and access Use `https://mementom.app` as the SDK `baseUrl`. Raw HTTP paths begin with `/api/v1/marketing`. Marketing routes belong to the public application service, not the separately served `auth.mementom.app` authorization surface. | Client | Authentication | Where it runs | | --- | --- | --- | | Company or agency integration | `Authorization: Bearer mkt_…` | Your server | | Mementom marketing console and walkthrough | Existing marketing session cookie | Same-origin Mementom page | | Credential creation, listing and revocation | Existing marketing session cookie only | [Marketing API credentials](/marketing/api/#credentials) | A marketing credential is bound to one company, one active application and its issuing staff account. Current company membership, staff account status, company status, application ownership/status, expiry and revocation are checked on every call. The issuing account must still have an active owner, admin or marketing role. One staff account may belong to more than one company. Select the company before issuing the credential and use a separate scoped credential for each application. A credential does not switch companies, grant agency access to another company, sign a user in or inherit another token's permissions. Keep the credential in your server secret store. Do not put it in JavaScript delivered to browsers, query strings, source control, command-line arguments, request captures or logs. Browser-origin Bearer requests are rejected. Do not strip browser headers and forward arbitrary browser-selected requests; expose only your own authenticated, authorized server operations. ### Issue and revoke a credential Sign in to [Marketing API credentials](/marketing/api/#credentials) with an owner, admin or marketing account. Choose the application, a useful label, the necessary scopes and an expiry. The default lifetime is 30 days; the accepted range is 1–90 days. `marketing:read` is mandatory. `POST /api/v1/marketing/api-tokens` accepts this same-origin, console-session request: ```json { "applicationId": "app_your_company", "label": "Marketing automation", "scopes": [ "marketing:read", "marketing:audiences:write", "marketing:content:write", "marketing:campaigns:write" ], "expiresInDays": 30 } ``` `app_your_company` is illustrative. Choose your actual application returned by the console; Aster is not required. The `201` response contains credential metadata and a `credential` value shown once. The service stores a hash, not the reusable secret. Save it directly in the server secret store; if it is lost, issue a replacement. `GET /api/v1/marketing/api-tokens` returns metadata and available scopes. Owners/admins can see the company's credentials; marketers can see their own. No secret is returned by a later list. `DELETE /api/v1/marketing/api-tokens/{tokenId}` revokes a credential. Owners/admins can revoke company credentials; marketers can revoke their own. Revoking an already-revoked visible credential is safe. A Bearer token cannot call any of these management endpoints. For rotation, issue the replacement, update the server secret, confirm a read succeeds, then revoke the old credential. Do not assume a still-unexpired token survives removal of its staff account's company access. ## 2. Scope and operation reference In this table, `{workspace}` means `/api/v1/marketing/applications/{applicationId}/workspace`. | Method and path | Required Bearer scope | Effect | | --- | --- | --- | | `GET /api/v1/marketing/applications` | `marketing:read` | Returns the credential's one active application | | `GET {workspace}` | `marketing:read` | Reads catalog, current revision and saved marketing records | | `POST {workspace}/count` | `marketing:read` | Counts current permitted matches; does not save | | `GET {workspace}/report` | `marketing:read` | Reads aggregate campaign results | | `GET {workspace}/reportcomparison` | `marketing:read` | Compares campaigns with weighted aggregate rates | | `GET {workspace}/activity` | `marketing:read` | Reads safe operational traces | | `POST {workspace}/audiences` | `marketing:audiences:write` | Creates or replaces a saved audience | | `POST {workspace}/content` | `marketing:content:write` | Creates, revises or duplicates content | | `POST {workspace}/campaigns` | `marketing:campaigns:write` | Creates or edits an unpublished campaign draft | | `POST {workspace}/campaignduplicate` | `marketing:campaigns:write` | Copies a campaign into an independent editable draft | | `POST {workspace}/campaignpublish` | `marketing:campaigns:publish` | Launches or resumes a validated campaign | | `POST {workspace}/campaignstate` | `marketing:campaigns:manage` | Pauses or permanently stops a campaign | | `POST {workspace}/campaigndelete` | `marketing:campaigns:manage` | Ends delivery and soft-deletes, retaining results | | `POST {workspace}/campaignaudience`, `/campaignaudiences` | `marketing:campaigns:manage` | Revises live targeting with campaign and targeting revision checks | | `POST {workspace}/pricing` | `marketing:pricing:write` | Saves planning assumptions | | `POST {workspace}/proposals` | `marketing:insights:propose` | Proposes an insight for developer review | | `POST {workspace}/objective` | `marketing:objective:write` | Changes workspace suggestions | Developer definition registration/review, dataset configuration, ingestion and credential ceremonies remain developer operations. Local QA sample installation and simulation endpoints are not available to external marketing credentials. ## 3. Revision handling and reliable writes Every saving operation takes an exact envelope: ```json { "expectedRevision": 12, "payload": {} } ``` The revision is the workspace revision from the latest `GET` or accepted mutation. Each successful save returns the complete marketing workspace with the new revision, saved IDs and request `activityId`. Use that returned workspace for the next operation. Unknown fields are rejected; do not send service-owned properties such as `status`, snapshots or consumer identifiers back as payload fields. Concurrent writes with the same revision cannot both succeed. A stale revision returns `409`. Content edits also require `expectedContentRevision`; a content conflict uses `content_revision_conflict`. Do not blindly replay a write after a timeout, disconnected response or `409`. A write may already have succeeded. Fetch the workspace, locate the record you intended to create or edit, reconcile it with your pending intent and decide whether another write is needed. The SDK does not automatically retry. There is no public idempotency-key contract. Refreshing a revision and reissuing a create without reconciliation can create a second draft. A multi-step workflow is not one transaction. If content validation fails after an audience save, the audience remains saved and visible in the console. Resume from those records; do not start over by creating duplicates. ## 4. Server SDK Download the SDK from the same environment being tested. It is an ES module and uses Node 22+ `fetch` and bounded request timeouts. `baseUrl` must be an HTTPS origin without a path, query, fragment or embedded credentials. Explicit loopback-only HTTP support is available for automated local tests through `allowInsecureLocal: true`; do not disable TLS verification for LAN or production testing. ```js import { MementomMarketingClient } from './mementom-sdk.mjs'; const client = new MementomMarketingClient({ baseUrl: process.env.MEMENTOM_BASE_URL || 'https://mementom.app', credential: process.env.MEMENTOM_MARKETING_TOKEN, timeoutMs: 10000, onTrace: ({ method, path, status, activityId, revision, error }) => { // Safe metadata only. Never log Authorization or the credential. console.log({ method, path, status, activityId, revision, error }); } }); const { applications } = await client.getApplications(); const applicationId = applications[0]?.applicationId; if (!applicationId) throw new Error('No active application is available.'); let workspace = await client.getWorkspace(applicationId); ``` | Method | Arguments after `applicationId` | | --- | --- | | `getWorkspace` | None | | `saveAudience` | `expectedRevision, payload` | | `saveContent` | `expectedRevision, payload` | | `createCampaign`, `updateCampaign`, `duplicateCampaign` | `expectedRevision, payload` | | `publishCampaign`, `pauseCampaign`, `stopCampaign`, `deleteCampaign` | `expectedRevision, payload` with campaign ID and expected campaign revision | | `setCampaignAudiences`, `editCampaignAudience` | `expectedRevision, payload` with campaign, engagement and targeting revisions | | `updatePricing` | `expectedRevision, payload` | | `proposeInsight` | `expectedRevision, payload` | | `setObjective` | `expectedRevision, payload` | | `countAudience` | `expectedRevision, audience` | | `getReport` | `campaignId, mode = 'live'` | | `getCampaignComparison` | `{ campaignIds, mode = 'live' }`; omit IDs for current campaigns | | `getActivity` | `{ offset = 0, limit = 20, activityId = '' }` | `getApplications()` takes no arguments. HTTP failures expose `status`, `code` and `activityId` on the thrown error. Network, timeout, redirect or invalid-response failures use `marketing_transport_error`; their messages do not forward potentially sensitive transport diagnostics. The SDK refuses redirects rather than forwarding a credential to another location. A diagnostic callback failure does not turn a successful API write into an apparent failure. ## 5. Learn with Aster, integrate your company Aster demonstrates the developer-to-marketer flow with one concrete example. Its standard `aster.activity` enum includes `entry_authorized`: the holder authorized entry. That value does not mean a visit was completed or a payment was made. To run that specific demonstration, choose the existing Aster application and set up its developer integration if the field is absent. For your integration, select your company's application and a standard or approved, non-sample insight from its workspace. Use one of that field's permitted values and operators. Register and, when necessary, review new definitions through the developer workflow first. Marketing access cannot install a standard template or approve its own definition. Both the authenticated browser walkthrough and the downloadable server example use `runMarketingDraftJourney(client, applicationId, configuration)`. The browser uses a console session without a token. Your server uses `MementomMarketingClient` with its application-scoped credential. The exported `asterMarketingExample` supplies demonstration values; it is not an API requirement. `runAsterMarketingJourney` remains a compatibility wrapper. ```mermaid sequenceDiagram participant I as Your company integration participant M as Mementom marketing API participant W as Shared marketing workspace participant C as Marketing console I->>M: GET workspace for your application M-->>I: Current revision and registered insight definitions I->>M: Save configured audience with expectedRevision M->>W: Validate and save audience M-->>I: Full workspace, audience ID, next revision, activity ID I->>M: Count audience using current revision M-->>I: Permission-aware aggregate count I->>M: Save your offer content with expectedRevision M->>W: Save content revision M-->>I: Full workspace and content ID/revision I->>M: Create campaign draft with audience/content IDs M->>W: Save draft and snapshots M-->>I: Full workspace and campaign ID I->>M: GET report mode=live; GET workspace M-->>I: Aggregate report, saved records and capabilities C->>W: Read the same company/application W-->>C: Same audience, content and campaign draft IDs ``` ### Run the demonstration Download these three files into one directory without renaming them. Their Aster filenames identify the teaching example, not a restriction on the API or workflow: 1. `mementom-sdk.mjs` 2. `mementom-aster-marketing.mjs` 3. `mementom-aster-marketing-workflow.mjs` Set `MEMENTOM_BASE_URL`, `MEMENTOM_APPLICATION_ID` and `MEMENTOM_MARKETING_TOKEN` in your server environment/secret manager. For the unmodified demonstration, the application and credential must be for Aster. Then run: ```sh node mementom-aster-marketing.mjs ``` The workflow reads the configured standard or approved definition, creates or reuses its named audience/content/draft, counts the audience, reads the live-mode report and verifies the saved IDs by reading back the workspace. Repeated runs reuse matching records; they are not an atomic publish operation or a substitute for resolving concurrent edits. For an interactive demonstration, open the separate [Aster marketing demonstration](/aster/marketing/). Each message displays its request/response metadata and correlation ID. After completion, open the same application in the marketing console and compare the audience, content and campaign IDs. The draft is real saved work; no outreach is sent. A zero live count is valid if there are no current permitted matching insights. Do not replace it with a synthetic success number. ### Replace Aster with your company The configurable helper separates the example data from the reusable API sequence. Replace every demonstration field with values appropriate to your actual application: | Configuration | Meaning | | --- | --- | | `companyName` | Your company name for generated draft labels | | `insightKey` | A standard or approved non-sample insight key in your application's workspace | | `insightValue` | A value accepted by that definition; preserve its string, number or boolean type | | `operator` | Supported operator, default `equals` | | `landingUrl` | Your public HTTPS landing page, without query parameters or a fragment | | `audienceName`, `contentName`, `campaignName` | Optional explicit names used to identify the intended workflow records | | `content` | Optional `{ headline, body, cta }` for your invitation | For example, after your developer has registered the standard `shopping.interest` definition for your real application, replace Aster's configuration with your own: ```js import { MementomMarketingClient } from './mementom-sdk.mjs'; import { runMarketingDraftJourney, asterMarketingExample } from './mementom-aster-marketing-workflow.mjs'; const client = new MementomMarketingClient({ baseUrl: process.env.MEMENTOM_BASE_URL || 'https://mementom.app', credential: process.env.MEMENTOM_MARKETING_TOKEN, onTrace: trace => console.log(trace) // Safe metadata only. }); const applicationId = process.env.MEMENTOM_APPLICATION_ID; // The teaching configuration is available as asterMarketingExample. // Use it only with Aster. Replace it for your company's integration. const companyConfiguration = { companyName: 'Your company', insightKey: 'shopping.interest', insightValue: 'compared', operator: 'equals', landingUrl: 'https://your-company.example/offers', audienceName: 'Visitors comparing products', contentName: 'An invitation to take another look', campaignName: 'Product comparison follow-up', content: { headline: 'Still considering your options?', body: 'Take another look at the choices that interested you.', cta: 'Explore options' } }; const result = await runMarketingDraftJourney( client, applicationId, companyConfiguration ); console.log({ audienceId: result.audienceId, contentId: result.contentId, campaignId: result.campaignId, revision: result.revision }); ``` Replace the reserved `.example` URL with your company's actual landing page and set an application ID/credential for that company. The helper resolves the configured insight's current ID and version from the workspace; it does not register the insight or create company access. An unavailable or incompatible definition is an integration error, not a reason to substitute Aster's data. The helper is one demonstration of the API's building blocks. A production automation can call the SDK methods directly to support multiple audiences, typed questions, insight requests, revised content and different draft terms. Keep the same revision checks, saved-record reconciliation, company/application isolation and readback verification. ## 6. Audience definitions and counts Read the current catalog from the workspace; never hard-code definition IDs or invent enum values. Use a definition's exact version. Approved/standard own-company definitions and the outside catalog are available to the audience validator. Preserve `isSample` and catalog provenance in your presentation. Simple audiences use `name`, `mode: 'all' | 'any'` and 1–8 rules. A rule has `definitionId`, `version`, `operator` and `value`. Use a definition once in a simple audience. Operators are `equals` for boolean/enum/numeric values, `gte`/`lte` for numeric fields and `contains` for enum sets. Values must fit their registered type and range. For grouped criteria, use schema version 2. This example has one group and two leaf bindings: ```json { "schemaVersion": 2, "name": "Interested visitors ready for follow-up", "logic": { "id": "head", "logic": "AND", "children": [ { "id": "entry-node", "insight": "entry", "logic": "AND" }, { "id": "recent-node", "insight": "recent", "logic": "AND" } ] }, "criteria": [ { "id": "entry", "definitionId": "aster_activity_definition_id", "version": 1, "operator": "equals", "value": "entry_authorized" }, { "id": "recent", "definitionId": "visits_recency_definition_id", "version": 1, "operator": "equals", "value": "today" } ] } ``` Replace both illustrative IDs with registered definitions from that workspace. The root is `head`. Each node has a unique ID; each criterion binding has a unique ID and must be used. Maximums are 64 criteria, 256 nodes and depth 16. Joins evaluate left to right using leaf `logic` and group `peerLogic`; both default to `AND`. This is the console's existing tree contract, not SQL operator precedence. To edit an audience, add `audienceId` to the payload. Referencing campaigns retain their original audience snapshots and receive `audienceChanged: true`; saving an audience does not silently revise a campaign. Count requests differ from mutation envelopes: ```json { "expectedRevision": 12, "audience": { "name": "Visitors who authorized entry", "mode": "all", "rules": [{ "definitionId": "aster_activity_definition_id", "version": 1, "operator": "equals", "value": "entry_authorized" }] } } ``` Replace the illustrative definition ID and revision with the current workspace values. The result contains either `available: true`, `count`, `eligible`, a structured-node `breakdown`, `asOf` and `basis`, or `available: false`, `count: null` and a reason. Sample definitions cannot be reported as a live count. Counts reveal no holder/member IDs, contact details or per-person insight values. ## 7. Content, questions and campaign drafts Content supports `offer`, `questionnaire` and `insight_request`. All require a name, creative, `questions` array and `requestedInsightIds` array. Creative requires a plain-text headline (120 characters), body (2,000) and CTA (60). Leave `content.landingUrl` empty or omit it for the Mementom offer page, or supply a company HTTPS destination without embedded credentials, query parameters or a fragment. Saved offers return `hostedUrl` separately. Opening that preview requires the owning company’s marketing session; saving a draft does not publish it. Keep the authored `landingUrl` empty when using the default. Optional creative images are PNG, JPEG or WebP data URLs; the complete content or campaign request must fit the 4 MiB JSON limit. A questionnaire has 1–10 questions; the other types require an empty questions array. Prefer typed question schema version 1 bound to an approved company insight. Multiple choice uses its registered enum domain, boolean uses its true/false domain and sliders use its bounded integer range. Set a slider step that divides that range; boolean and slider accept one answer. The API also reads existing legacy questions with 2–8 plain-text options, but those are not a registered insight binding. An insight request requires at least one requested insight. All types allow at most 20 distinct requested insight IDs. Saving content or a campaign does not itself obtain the holder's consent or release data. For a content edit, pass `contentId`, `expectedContentRevision` and all author-controlled content fields. For a duplicate, pass `duplicateFromId` and a full payload; do not combine this with `contentId` or `expectedContentRevision`. Saved editions are retained. A campaign referencing `contentId` and `contentRevision` must also carry that edition's unchanged creative (including design and offer details), questions, tags and requested insights. The service supplies snapshots and provenance. Optional library `engagementMinor` and `purchaseMinor` are default combined fees in even USD cents, 0–100000 each and positive in combination; purchase fees apply only to offers. Omission preserves existing or duplicated values. New content defaults to 200 and 0. Campaign placements may choose different fees without changing the library edition. ### Creative customization Creative accepts optional plain-text `brandName` (80 characters), `offerLabel` (120), `terms` (1000), `promoCode` (80) and `imageAlt` (180). Offer labels and terms appear as authored. The preview does not insert reward amounts or completion-event wording into the creative. The optional `content.design` object accepts only the following settings. Omitted values use the defaults; null, unknown keys, arbitrary CSS and invalid values are rejected. | Setting | Values | Default | | --- | --- | --- | | `layout` | `image-top`, `background`, `text` | `image-top` | | `backgroundColor`, `textColor`, `accentColor`, `buttonTextColor`, `overlayColor` | Six-digit hex colors | `#ffffff`, `#182840`, `#245cdb`, `#ffffff`, `#000000` | | `overlayOpacity` | Number from 0 to 85, as a percentage | 45 | | `imagePosition` | `center`, `top`, `bottom`, `left`, `right` | `center` | | `textAlign` | `left`, `center` | `left` | | `contentPosition` | `top`, `center`, `bottom` | `bottom` | | `fontFamily` | `sans`, `serif` | `sans` | | `headlineSize` | `small`, `medium`, `large` | `medium` | | `buttonStyle` | `rounded`, `pill`, `square` | `rounded` | | `imageFit` | `cover`, `contain` | `cover` | Keep all intended author fields when editing or duplicating creative. Existing records without these fields render with the defaults. Saved campaign and historical content editions preserve their original creative. Use campaign schema version 2 for new integrations: - 1–10 engagements; each selects 1–20 saved audience IDs and can exclude up to 20. The same audience cannot be both included and excluded. - Objectives: `return_visitors`, `learn_preferences` or `collect_insights`. - A 24-, 72- or 168-hour response window. - Both schedule dates null, or both timezone-bearing dates with the end after the start. Publishing applies the validated schedule; saving alone does not activate it. - Positive total budget and daily cap in USD cents; daily cap cannot exceed total budget. Both must cover one fully priced engagement and purchase fee. - Engagement/purchase fees are whole even cents with a positive combined amount. Only offers accept a purchase fee. - Saved campaigns use first-matching-engagement routing and once-per-campaign frequency after publication. Creation returns `draft`. To edit an unpublished version2 draft, include `campaignId` and `expectedCampaignRevision` with its complete authored payload. Publishing uses `campaignpublish` with those two fields; pause/stop use `campaignstate` and `state: "paused"` or `"stopped"`; deletion uses `campaigndelete`. Every mutation also requires the current workspace `expectedRevision`. Existing draft-write credentials do not acquire manage or publish rights automatically. Published creative remains bound to its delivery receipts. `campaignduplicate` copies it into an independent editable draft and accepts an optional new name. It preserves source provenance and complete creative snapshots, while the original retains spend, frequency and results. Live targeting changes use `campaignaudience` or `campaignaudiences`, with `expectedCampaignRevision` and `expectedTargetingRevision`, without replacing delivered creative. The older simple-campaign shape remains a compatibility format; lifecycle publication uses version2. ## 8. Insight proposals, objectives and planning prices A proposal defines a custom non-identifying key, label, description, type, reporting window and expiry. Both windows are 1–30 days. Supported types are enum (2–20 distinct categories), boolean and bounded integer (0–99). Definitions reject identifying metadata, tracking identifiers and links. Free-text insight value types are not supported. Existing keys are immutable. Marketing proposals are saved `pending_review`; developer review is required before matching or typed questionnaire use. Standard template registration remains a developer operation. The workspace objective is either `return_visitors` or `learn_preferences`. It tunes suggestions; it is separate from an individual campaign's objective. Pricing updates save either `{ policy: ... }` or `{ definitionId, profile: ... }`. Profiles apply to an outside-catalog insight. These inputs set planning assumptions, including confidence, age, freshness, source coverage and purchase mode. They create no paid license, subscription, invoice or settlement. Omitted policy/profile fields use the documented defaults rather than preserving older customized values. Submit your full intended policy/profile when changing it. Price changes recompute audience estimates and mark existing campaign drafts `pricingChanged: true`; they do not rewrite accepted campaign snapshots. Insight values outside their assumed lifetime must be reviewed before a new campaign can be saved. ## 9. Reports and truthful measurement `GET {workspace}/report?campaignId=…&mode=live` defaults to live mode. The response preserves its `mode`, `isSample`, reporting window, aggregate overview, engagement-level metrics, question distributions, recommendations and limitations. `mode=test` is explicitly synthetic. Some starter-pack campaigns can produce a synthetic preview even without persisted events. Never combine test and live series or label a test result as revenue, settled spend or measured ROI. API credentials cannot post fabricated delivery events or invoke the local QA simulation operation. Response rates are ratios, not percentages. A ratio is `null` when its denominator is zero. Response times are milliseconds. `modeledSpendMinor` and `modeledConsumerMinor` follow draft terms; `settledMinor` is the recorded settlement amount, or `null` when no amount is recorded. `revenueMinor` is `null` unless recorded revenue covers all net purchases. A missing value means unavailable, not zero. Live questionnaire distributions are withheld below five recorded answers. Recommendations are descriptive signals, not randomized evidence of lift. Views do not prove attention. Keep the report's limitations visible alongside charts. ## 10. Debugging the complete message flow Marketing responses carry `x-mementom-activity-id`; workspace responses also include `activityId`. Save the method, path, status, revision and that ID. The SDK's `onTrace` callback supplies those safe fields. Do not attach a full request/response dump to a support ticket. Query one request's operational trace: ```js const trace = await client.getActivity(applicationId, { activityId: workspace.activityId, offset: 0, limit: 20 }); ``` The activity endpoint accepts only `activityId`, `offset` and `limit`, with each key once. Offset is 0–10,000; limit is 1–100, default 20. Results are scoped to the active company and application. Events expose ID, event type, result, timestamp, correlation ID, operation, method, status and, when relevant, revision/error/source. Source distinguishes server, console and hosted MCP calls. Activity reads are not recursively added to the trace. For your integration, use Aster's trace sequence as the debugging pattern rather than treating the last screen as proof: 1. Confirm the credential lists your intended application and the workspace contains the configured standard or approved definition. For the Aster teaching example, that definition is `aster.activity`. 2. Check each save's status, activity ID and returned revision. A later request must use the previous successful response's revision. 3. Verify the audience ID and its exact definition/version/value, then the content ID/revision and the campaign's snapshots. 4. Read those same IDs in the marketing console for the same company/application. 5. Read report mode, event counts, capabilities and limitations. An accepted draft is not evidence of delivery or payment. Expected error recovery: | HTTP | Typical code or condition | Action | | --- | --- | --- | | `400` | `insight_workspace_invalid`, `invalid_audience_definition`, `invalid_content`, `invalid_question`, `invalid_campaign`, `invalid_pricing_assumptions`, `company_insight_invalid`, `invalid_activity_query` | Correct the documented fields/types or refresh catalog bindings; do not retry unchanged input | | `401` | `marketing_token_required`, `marketing_token_inactive` | Check expiry/revocation and current account/company/application access; obtain a valid credential through the console | | `403` | `marketing_scope_required`, `company_server_required`, `console_session_required`, `invalid_origin` | Use the correct scope, server/client surface and authorized console account | | `404` | `application_not_found`, `marketing_token_not_found`, unavailable campaign | Check the bound company/application and saved ID; foreign records are not disclosed | | `409` | Workspace revision mismatch, `content_revision_conflict`, duplicate immutable definition key | Refresh, preserve intended edits, inspect existing records and reconcile before another write | | `413` | `invalid_request` body limit or `invalid_marketing_event` report limit | Reduce request size; report currently supports up to 10,000 stored events per campaign/mode | | `429` | Service/gateway throttling or staff sign-in cooldown | Honor `Retry-After` when supplied; use bounded backoff for safe reads, never a blind create retry | | `500` / `503` | Service/dependency failure | Save the correlation ID when present; after recovery read back the workspace before deciding whether to resend a mutation | Early transport/proxy failures may not have an activity ID or a JSON error body. Do not infer a mutation failed solely because its response was lost. ## 11. Limits and validation boundary - Count and credential-issue JSON requests allow 32 KiB. Authenticated content/campaign saves allow 4 MiB for the entire JSON body, including every base64 image. Other workspace writes retain the 192 KiB limit. - There is no separate image-character ceiling. The marketing console automatically prepares uploaded images; direct API integrations must keep the complete encoded request within its limit. - Current per-application limits are 100 non-starter-pack audiences, content records, campaign drafts and registered insight definitions. Existing sample-derived records retain their sample provenance. - Use current application-local IDs and definition versions. Cross-application references fail validation or return an unavailable-resource response. - Filters use registered finite definitions, not arbitrary code or raw SQL. Do not send consumer IDs, email addresses, device identifiers, keyring credentials or private photos/captions as insight data or filter values. - Catalog outside-insight examples are planning examples. Presence in the catalog is not a claim of an active paid source or holder permission. ## 12. Marketing automation and MCP tools The API is the reusable integration surface. A company's scheduled jobs, internal tools or agency backend can use the same SDK to maintain audiences, draft content and campaigns, and read measurements. Keep a minimally scoped server credential per application. A reporting job normally needs only `marketing:read`; a draft-building workflow adds only the audience/content/campaign scopes it actually uses. Treat the company and application as server-side authorization context. Do not accept an arbitrary company/application from an unauthenticated caller or let a model choose credentials. The API still checks current membership, credential scope and application ownership on every call; an automation or MCP wrapper must preserve those checks rather than bypassing them. The [Marketing MCP server](/developers/mcp/) exposes tools backed by the existing API operations: | Tool purpose | Existing SDK method | Important behavior to preserve | | --- | --- | --- | | Read the marketing workspace/catalog | `getWorkspace` | Return current revisions, capabilities and sample provenance | | Count an audience | `countAudience` | Return permitted aggregate counts, never holder identities | | Save an audience | `saveAudience` | Validate catalog bindings and require `expectedRevision` | | Save content | `saveContent` | Preserve content editions and expected content revision on edits | | Create a campaign draft | `createCampaign` | State clearly that it creates a draft; it does not publish or spend | | Read campaign measurements | `getReport` | Keep test/live mode, unavailable values and modeled/settled distinctions | | Inspect a request trace | `getActivity` | Return only the existing safe operational fields | See the [MCP connection guide](/developers/guides/marketing-mcp.md) for hosted OAuth and local stdio setup, exact tool names and input schemas. MCP access preserves the staff member's company access and granted scopes. Credential issuance remains a console operation; it should not become a model-visible tool. Keep credentials, private photos/captions, keyring material, consumer identifiers and contact details out of model inputs, tool arguments and generated traces. Send only the bounded catalog definitions, draft fields and aggregate results needed for the task. The company backend injects its credential after authorization. Treat draft copy and external content as data, not instructions for selecting credentials or changing permissions. For unattended jobs and MCP tools, persist the returned record IDs and workspace revision for each intended operation. On `409` or an uncertain transport outcome, read back and reconcile before retrying. Do not let an agent repeatedly refresh the revision and replay create operations. Make the intended mutation and its resulting IDs visible to the operator. Follow the company's own approval policy for draft changes; future publishing, spending and data-release tools need their own explicit controls when those capabilities exist. ## 13. Acceptance checks A complete company integration should demonstrate the same sequence taught by Aster with its own application and configuration: server credential access, catalog read, permitted audience count, saved audience/content/campaign draft, readback in the console and correlated activity. Verify a stale revision is rejected, a read-only token cannot save, a foreign application cannot be read and a revoked credential stops working. Preserve the distinction between saved drafts, synthetic reports and permitted aggregate counts. ### Shared tags Audiences, campaigns, content library items, and campaign engagements accept an optional `tags` array. Use up to 12 tags of 40 characters each. The service trims and lowercases tags, replaces whitespace with hyphens, deduplicates them, and resolves explicit aliases such as `footwear` to `shoes`. Plain custom tags are supported. The shared catalog is available at `/shared/content-tags.js`. Tags classify the authored record; they do not change audience matching, grant access, publish content, or deliver a campaign. Campaign and audience tags are not inherited by offer engagements. Choosing saved content copies its content tags into the engagement. Editing those tags makes the message an independent copy when necessary. When editing an audience or content item, omitting `tags` preserves its existing tags. Sending `"tags": []` clears them. Saved campaign audience/content snapshots retain the tags from their referenced edition. Reports include campaign tags and each engagement's tags, alongside the existing metrics. ```json {"tags":["shoes","running"]} ``` ## Cross-campaign performance `GET {workspace}/reportcomparison?mode=live` compares current nondeleted campaigns. Add `campaignIds=id1,id2` for distinct selected IDs, including retained deleted campaigns. Unknown IDs are rejected within the credential's application. `test` and `live` are separate report modes. Repeated or unknown query parameters and duplicate IDs are rejected instead of double-counted. All campaigns use one report timestamp. Totals sum campaign participations and compute rates from the combined numerators and denominators; they do not represent unique people across campaigns. Each campaign retains its response window and individual overview. Aggregate response-time percentiles are not averaged. Modeled spend remains separate from settlement, and missing revenue stays null.