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.
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.
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).
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.
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.
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".
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:writeand “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.