Menu

API reference · Media Planner

Plans

media_planner · plans · 7 operations

GET/v1/media-planner/plans

List media plans

media_planner.plans.list

ReadIdempotentSince 2026-11-01

Lists the organization's media plans, newest first (by start date), paginated by cursor (limit up to 200). Each plan has its window (start_date/end_date in ms and start_day/end_day as YYYY-MM-DD), currency, mode (campaigns, platforms or account), status (draft, active, archived), planned budget (total_budget in the plan's currency, or null when its rows mix currencies, and budget_by_currency, the real total by currency) and, once synced, the spend (total_spent, with the same rule, and spent_by_currency); amounts are rounded to 2 decimals. Filters: status, mode, and from/to (YYYY-MM-DD) for plans whose window overlaps that range. The mode and from filters fill the page (plans are read until limit plans match, up to 600 per call; a rare filter can bring a shorter page with has_more: follow next_cursor). It does not include the rows: use media_planner.plans.get or media_planner.campaigns.list. Requires view access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Scope
media_planner:read
Permission
View the module
MCP tool
media_planner_plans_listOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.list".

Declared cost: up to 601 reads and 0 writes per call (they count against the daily quota).

Parameters

limitintegerqueryoptional

How many items this page brings at most (see the maximum and the default in the schema).

Default: 50

cursorstringqueryoptional

The next_cursor of the previous page, exactly as it came (only for the same list and filters); without it, the first page.

statusenumqueryoptional

Only plans in this status (default: all).

Values: draft, active, archived

modeenumqueryoptional

Only plans of this granularity (campaigns, platforms or account).

Values: campaigns, platforms, account

fromstringqueryoptional

Only plans whose window ends on or after this day.

tostringqueryoptional

Only plans whose window starts on or before this day.

Response

dataarray<object>
has_moreboolean
next_cursorstring

Errors

Example

curl "https://api.mb-suite.com/v1/media-planner/plans" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/media-planner/plans" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/media-planner/plans/{plan_id}

Get one media plan with its rows

media_planner.plans.get

ReadIdempotentSince 2026-11-01

Returns a media plan with its rows (campaigns, platform rows or account rows, in their display order; the first campaigns_limit, 100 by default and up to 500, with campaigns_truncated when there are more: media_planner.campaigns.list pages the rest). Each row has its platform, budget and currency, own flight dates if any, status, labels, the link to a real campaign (campaigns mode) and the known spend (last_synced_cost, last_synced_at; manual_spend in the platforms and account modes). Pass include_campaigns=false for the plan alone. total_budget is in the plan's currency (null when its rows mix currencies: read budget_by_currency); created_by_name goes next to created_by. Not found (media_plan_not_found) if the id is not a plan of this organization. Requires view access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Scope
media_planner:read
Permission
View the module
MCP tool
media_planner_plans_getOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.get".

Declared cost: up to 502 reads and 0 writes per call (they count against the daily quota).

Parameters

plan_idstringpathrequired

Media plan id (from media_planner.plans.list).

include_campaignsbooleanqueryoptional

Include the plan's rows (default true), in their display order.

Default: true

campaigns_limitintegerqueryoptional

How many rows come inline (default 100, up to 500); with more, campaigns_truncated is true and media_planner.campaigns.list pages the rest.

Default: 100

Response

idstring

Media plan id.

namestring

Plan name.

statusenum

draft, active or archived.

Values: draft, active, archived

modeenum

campaigns, platforms or account: fixed for the life of the plan.

Values: campaigns, platforms, account

start_datenumber

First day of the plan (epoch ms, 00:00 UTC). Send it back as start_day (YYYY-MM-DD).

end_datenumber

Last instant of the plan's last day (epoch ms, UTC).

currencystring

The plan's currency (ISO code).

total_budgetnumber | null

Planned budget of the plan in its currency; null when rows use another currency too (then read budget_by_currency, never add currencies).

created_atnumber

Epoch ms.

updated_atnumber

Epoch ms.

start_daystring

First day of the plan (YYYY-MM-DD): the format media_planner.plans.update takes.

end_daystring

Last day of the plan (YYYY-MM-DD, inclusive).

budget_by_currencyobject

Planned budget by currency (ISO code → amount): the plan's real total when it mixes currencies.

total_spentnumber | null

Known spend of the plan in its currency, after a cost sync; null when there is spend in another currency too (read spent_by_currency).

spent_by_currencyobject

Known spend by currency (ISO code → amount).

account_total_spendobject

Platforms and account modes: total spend of the connected ad accounts per platform (platform id → amount, in the currency of account_spend_meta).

account_spend_metaobject

Per platform: the currency of account_total_spend and how many ad accounts it adds up.

account_total_spend_synced_atnumber

When account_total_spend was last synced (epoch ms).

notesstring

Free notes of the plan.

carryover_enabledboolean

Whether the plan can receive the unspent balance of a finished plan.

carryover_platformsarray<string>

Platforms included in the carryover (absent or empty = all).

carryover_stateobject

The carryover this plan received or dismissed.

carryover_consumed_bystring | null

The plan that took this plan's unspent balance (null or absent: still available).

source_strategy_idstring

The Strategy Generator strategy the plan was created from, if any.

created_bystring

Who created the plan (uid).

created_by_namestring | null

That person's name (null when MB Suite does not know it: a former member).

campaignsarray<object>

The plan's rows in display order (with include_campaigns).

campaigns_truncatedboolean

true when the plan has more rows than campaigns_limit: read the rest with media_planner.campaigns.list.

Errors

Example

curl "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/media-planner/plans

Create a media plan

media_planner.plans.create

WriteSince 2026-11-01

Creates a media plan: name, window (start_date and end_date as YYYY-MM-DD, inclusive), currency (ISO code) and mode, which is fixed for the life of the plan: campaigns (rows are planned campaigns, each can be linked to a real one to sync its spend), platforms (one row per ad platform; the spend is the total of the platform's connected accounts) or account (one row per currency; the spend adds up every connected account). Optional: notes, status (draft by default, or active), carryover_enabled and carryover_platforms (to receive the unspent balance of a finished plan), template_id (campaigns mode: seeds the campaigns of a template with budget 0) and campaigns (initial rows, with the rules of media_planner.campaigns.create). The plan's totals are computed from its rows. Returns the plan and its rows. Requires full access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_plans_createOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.create".

Declared cost: up to 110 reads and 402 writes per call (they count against the daily quota).

Parameters

namestringbodyrequired

Plan name (e.g. "November 2026").

start_datestringbodyrequired

First day of the plan (YYYY-MM-DD).

end_datestringbodyrequired

Last day of the plan (YYYY-MM-DD, inclusive).

currencystringbodyrequired

The plan's currency (ISO code, e.g. USD, ARS): the default of its rows.

modeenumbodyoptional

Granularity, fixed for the life of the plan: campaigns (rows are planned campaigns), platforms (one row per ad platform, spend = the platform's accounts) or account (one row per currency, spend = all connected accounts).

Values: campaigns, platforms, account · Default: "campaigns"

notesstringbodyoptional

Free notes of the plan.

statusenumbodyoptional

draft (default) or active.

Values: draft, active · Default: "draft"

carryover_enabledbooleanbodyoptional

Let this plan receive the unspent balance of a finished plan (carryover).

Default: false

carryover_platformsarray<enum>bodyoptional

Platforms included in the carryover (empty = all).

Values: google_ads, meta_ads, tiktok_ads, linkedin_ads, twitter_ads, other

template_idstringbodyoptional

Campaigns mode: seed the plan with the campaigns of a template (own, inherited from the workspace or a published system template), with budget 0.

campaignsarray<object>bodyoptional

Initial rows, with the same rules as media_planner.campaigns.create.

Response

plan_idstring

The new plan's id.

planobject

The new plan.

campaignsarray<object>

Its rows, as created.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "name": "NAME",
  "start_date": "START_DATE",
  "end_date": "END_DATE",
  "currency": "CURRENCY"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "name": "NAME",
  "start_date": "START_DATE",
  "end_date": "END_DATE",
  "currency": "CURRENCY"
}'
PATCH/v1/media-planner/plans/{plan_id}

Update a media plan

media_planner.plans.update

WriteIdempotentSince 2026-11-01

Updates the plan, with the fields to change inside updates ({ plan_id, updates: { … } }): name, notes, window (start_date/end_date, YYYY-MM-DD), currency, status (draft or active; active also reactivates an archived plan) and carryover settings (carryover_enabled, carryover_platforms; an empty list means all platforms). The mode cannot change (duplicate the plan instead), and archiving goes through media_planner.plans.archive, which also closes its campaigns. Changing the window does not move the rows' own flight dates. Unknown fields are rejected; an update that changes nothing returns changed: false. Requires full access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_plans_updateOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.update".

Declared cost: up to 1 reads and 1 writes per call (they count against the daily quota).

Parameters

plan_idstringpathrequired

Media plan id (from media_planner.plans.list).

updatesobjectbodyrequired

The fields to change (at least one); the rest stay as they are.

Response

planobject

The plan as it is now.

changedboolean

false when the values sent were already the current ones: nothing was written.

Errors

Example

curl -X PATCH "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "updates": {}
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X PATCH "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "updates": {}
}'
POST/v1/media-planner/plans/{plan_id}/duplicate

Duplicate a media plan

media_planner.plans.duplicate

WriteSince 2026-11-01

Copies a plan with its rows into a new plan (typically next month's): optional name (default '<name> (Copy)', in the caller's language: (Copia), (Cópia)), start_date/end_date (when the window changes, every row takes the new window), status (active by default), carryover_enabled, carryover_platforms (empty = all) and notes (an empty string leaves the copy without notes); whatever is not sent comes from the original plan. Rows keep platform, budget, currency, labels and their link to real campaigns, but never the spend, the carryover (each budget goes back to its original amount), the Auto-Budget bookkeeping, the manual spend or the comments. campaign_status_mode: smart (default; completed campaigns come back active), draft (all drafts) or exact. Same mode as the original. To build next month's plan with what was agreed: exclude_campaign_ids leaves rows out of the copy, and campaign_overrides sets the budget (in the row's currency) and/or the status of a row in the copy. Every id must be a row of the original plan (400 unknown_campaign_ids with details.campaign_ids), a row can be overridden once and never when it is excluded (400 campaign_override_conflict), and leaving out every row is 400 no_campaigns_to_copy; budgets are numbers from 0 (invalid_input otherwise). The plan's totals come from the copied rows. Optional dry run: with dry_run=true nothing is written (no confirm_token needed): it runs the same checks and fails with the same errors, or returns a Preview whose counts say campaigns, excluded, budget_overrides, status_overrides and total_budget, whose items are the copied rows (up to 50) and whose warnings flag rows copied paused or cancelled and rows copied with budget 0, plus plan (name, start_day, end_day, status, mode, currency, carryover_enabled, carryover_platforms, total_budget, budget_by_currency), campaigns (per copied row: source_campaign_id, name, platform, currency, budget, original_budget, status, source_status) and excluded_campaign_ids. Removing a wrong copy later needs a key allowed to delete: check the dry run first. Reads the original plan and its rows (up to 500); writes the plan and one document per copied row. Returns the new plan. Requires full access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_plans_duplicateOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.duplicate".

Declared cost: up to 502 reads and 501 writes per call (they count against the daily quota).

Parameters

plan_idstringpathrequired

Media plan id (from media_planner.plans.list).

namestringbodyoptional

Default: the original name + ' (Copy)', in the caller's language.

start_datestringbodyoptional

The copy's first day (YYYY-MM-DD; default: the original's). When the window changes, every row takes the new window.

end_datestringbodyoptional

The copy's last day (YYYY-MM-DD, inclusive; default: the original's).

statusenumbodyoptional

Default active.

Values: draft, active

campaign_status_modeenumbodyoptional

smart (default): completed campaigns come back active, the rest keep their status; draft: all drafts; exact: same statuses.

Values: smart, draft, exact

carryover_enabledbooleanbodyoptional

Whether the copy can receive a carryover (default: the original's).

carryover_platformsarray<enum>bodyoptional

Platforms included in the copy's carryover (empty = all). Default: the original plan's.

Values: google_ads, meta_ads, tiktok_ads, linkedin_ads, twitter_ads, other

notesstringbodyoptional

The copy's notes (an empty string leaves it without notes). Default: the original plan's.

exclude_campaign_idsarray<string>bodyoptional

Rows of the original plan that the copy leaves out (row ids from media_planner.campaigns.list). Each one must be a row of that plan.

campaign_overridesarray<object>bodyoptional

Per-row changes in the copy: budget and/or status of a row of the original plan, at most once per row and never for an excluded row.

Response

plan_idstring

The copy's id.

planobject

The copy.

campaign_countnumber

Rows copied.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/duplicate" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/duplicate" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/media-planner/plans/{plan_id}/archive

Archive a media plan

media_planner.plans.archive

WriteIdempotentSince 2026-11-01

Archives a plan and closes its campaigns: scheduled ones become cancelled, active and paused ones become completed (drafts, completed and cancelled stay as they are); each change is kept in the campaign history and the plan.archived webhook fires. Call it with dry_run=true first to see the transitions (no confirm_token needed). Archiving is a status, not a deletion: media_planner.plans.update with status active reactivates the plan (campaign statuses do not go back). Idempotent: an archived plan with nothing to close returns changed: false. Requires full access to the Media Planner. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their fields are left out of the response (the permissions guide lists what each platform allows).

Instructions for agents

Tell the person how many campaigns changed status (transitions) when archiving.

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_plans_archiveOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.archive".

Declared cost: up to 502 reads and 501 writes per call (they count against the daily quota).

Parameters

plan_idstringpathrequired

Media plan id (from media_planner.plans.list).

Response

planobject

The plan as it is now.

changedboolean

false when it was already archived.

transitionsarray<object>

How many rows went from each status to another.

total_campaignsnumber

Rows of the plan.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/archive" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/archive" \
  -H "Authorization: Bearer $MB_TEST_KEY"
DELETE/v1/media-planner/plans/{plan_id}

Delete a media plan

media_planner.plans.delete

IrreversibleSince 2026-11-01

Deletes a plan with its rows and its campaign change history. It goes to the 30-day trash, where an owner or admin can restore it (platform.trash.restore); the comments of each campaign and the record of sent emails are kept and come back with it. The trash keeps up to 150 records with a plan, the rows first: when the history does not fit, its oldest entries are left out (the dry run warns and the trash item says linked_truncated), and a plan whose rows alone do not fit is refused before anything is deleted with 409 media_plan_too_large_for_trash (delete some rows first with media_planner.campaigns.bulk.delete). If the plan had received another plan's balance by carryover, that balance becomes available again (restoring the plan takes it again, or answers 409 carryover_source_consumed if another plan took it meanwhile). The last sync breakdown is discarded (the next sync rebuilds it). Outside the app it needs a dry run (which counts rows and history) and a confirm_token, except a REST DELETE with a key allowed to delete. Requires full access to the Media Planner.

With an API key, this DELETE runs directly: the record stays 30 days in the trash and the response carries its trash_item_id. With an OAuth token, and through the MCP, it needs dry_run → confirm_token.

Scope
media_planner:write and “Allow deletions” (media_planner:delete in OAuth)
Permission
Edit in the module
MCP tool
media_planner_plans_deleteOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.plans.delete".

Declared cost: up to 1100 reads and 1100 writes per call (they count against the daily quota).

Parameters

plan_idstringpathrequired

Media plan id (from media_planner.plans.list).

Response

oktrue

Always true.

deleted_campaignsnumber

Rows deleted with the plan.

released_carryovernumber

Finished plans whose balance this plan had received: it is available to carry over again.

trash_item_idstring

The trash item this deletion left: the item_id of platform.trash.restore (POST /trash/{item_id}/restore). Missing if nothing went to the trash.

trash_item_idsarray<string>

Only when the deletion left more than one copy: all of them, the first being trash_item_id.

trash_expires_atnumber

When the copy leaves the trash for good (epoch ms): 30 days after the deletion.

Errors

Example

curl -X DELETE "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X DELETE "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"