Menu

API reference · Media Planner

Campaigns

media_planner · campaigns · 9 operations

GET/v1/media-planner/plans/{plan_id}/campaigns

List the rows of a media plan

media_planner.campaigns.list

ReadIdempotentSince 2026-11-01

Lists the rows of a media plan in their display order, paginated by cursor (limit up to 200): campaigns in a campaigns-mode plan, platform rows or account rows in the other modes (row_kind says which). Each row has platform, budget, currency, status, own flight dates if any (start_day/end_day), labels, the link to a real campaign and the known spend (last_synced_cost). Filters status and platform fill the page (the rows are read until limit rows match). 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_campaigns_listOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.list".

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

Parameters

plan_idstringpathrequired

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

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 rows in this status (draft, scheduled, active, paused, completed, cancelled).

Values: draft, scheduled, active, paused, completed, cancelled

platformenumqueryoptional

Only rows of this ad platform.

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

Response

dataarray<object>
has_moreboolean
next_cursorstring

Errors

Example

curl "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns" \
  -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}/campaigns" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/media-planner/plans/{plan_id}/campaigns/{campaign_id}

Get one row of a media plan

media_planner.campaigns.get

ReadIdempotentSince 2026-11-01

Returns one row of a media plan (a campaign, a platform row or an account row) with its budget, currency, status, dates, labels, link to a real campaign, carryover applied, manual spend and known spend. Not found: media_plan_not_found for the plan, media_plan_row_not_found for the row. 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_campaigns_getOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.get".

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

Parameters

plan_idstringpathrequired

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

campaign_idstringpathrequired

Row id inside the plan: a campaign, a platform row or an account row (from media_planner.campaigns.list).

Response

idstring

Row id.

row_kindenum

campaign (campaigns mode), platform (one per ad platform) or account (one per currency).

Values: campaign, platform, account

namestring

Row name (in platform and account modes, set by MB Suite).

platformstring

google_ads, meta_ads, tiktok_ads, linkedin_ads, twitter_ads or other.

budgetnumber

Planned budget in the row's currency (carryover included).

original_budgetnumber

The budget without the carryover applied (budget − carryover.amount).

currencystring

The row's currency (ISO code).

statusenum

draft, scheduled, active, paused, completed or cancelled.

Values: draft, scheduled, active, paused, completed, cancelled

label_idsarray<string>

Campaign labels (media_planner.labels.list).

media_plan_idstring

The plan the row belongs to.

platform_custom_namestring

The name of an 'other' platform (e.g. Pinterest).

start_datenumber

Own flight start (epoch ms); absent = the plan's window.

end_datenumber

Own flight end (epoch ms).

start_daystring

Own flight start (YYYY-MM-DD): the format media_planner.campaigns.update takes.

end_daystring

Own flight end (YYYY-MM-DD, inclusive).

ordernumber

Position in the plan.

spend_sourcestring

Platforms and account modes: where the spend comes from (integration or manual).

manual_spendnumber

Spend entered by hand (already included in last_synced_cost).

manual_spend_updated_atnumber

When the manual spend was last edited (epoch ms).

manual_spend_updated_bystring

Who edited it (uid).

manual_spend_updated_by_namestring | null

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

excluded_account_idsarray<string>

Account mode: connected ad accounts left out of this row's total.

mapped_campaign_idstring

Campaigns mode: the real campaign on the ad platform whose spend is synced.

mapped_campaign_namestring

That campaign's name on the platform.

external_customer_idstring

The ad account of the linked campaign.

last_synced_costnumber

The row's known spend (synced + manual), in its currency.

last_synced_atnumber

When the spend was last synced (epoch ms).

carryoverobject

The carryover applied to this row.

comment_countnumber

Comments on the row (media_planner.comments.list); absent when it never had any (0).

auto_budget_enabledboolean

Whether a Google Ads Auto-Budget config adjusts this campaign.

auto_budget_last_runnumber

When the Auto-Budget last ran on it (epoch ms).

auto_budget_last_budgetnumber

The last daily budget the Auto-Budget set.

auto_budget_last_actionstring

What the Auto-Budget did last (adjusted, capped_max, no_change…).

created_atnumber

When the row was created (epoch ms).

updated_atnumber

Its last change (epoch ms).

Errors

Example

curl "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns/{campaign_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}/campaigns/{campaign_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/media-planner/plans/{plan_id}/campaigns

Add a row to a media plan

media_planner.campaigns.create

WriteSince 2026-11-01

Adds a row to a media plan and recomputes the plan's totals. What a row is depends on the plan's mode: in campaigns mode it is a planned campaign (name and platform required; it can be linked to a real campaign with mapped_campaign_id + external_customer_id, an ad account connected to this organization for that platform, to sync its spend); in platforms mode it is one row per ad platform (platform required, one row per platform except 'other', which needs platform_custom_name; MB Suite names it and its spend is the total of that platform's accounts); in account mode it is one row per currency (MB Suite names it; excluded_account_ids leaves connected accounts out of its total). manual_spend (platforms and account modes) adds spend entered by hand. Fields that do not apply to the plan's mode are rejected (field_not_in_mode). Defaults: budget 0, the plan's currency, status draft for a campaign and active for a platform or account row, no own dates (the row follows the plan window), last position. label_ids must be campaign labels of this organization or of its workspace. Errors: platform_row_exists, currency_row_exists and limit_reached (409; a plan holds up to 500 rows, details.resource media_plan_rows), external_account_not_linked (404: an ad account that is not connected to the organization; details.platform), link_incomplete and unknown_label_ids (400). Returns the row and the plan's new totals. 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_campaigns_createOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.create".

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

Parameters

plan_idstringpathrequired

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

namestringbodyoptional

Campaign name (campaigns mode only; platform and account rows are named by MB Suite).

platformenumbodyoptional

Ad platform (campaigns and platforms modes; account rows are always 'other').

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

platform_custom_namestringbodyoptional

Name of an 'other' platform (e.g. Pinterest).

budgetnumberbodyoptional

Planned budget in the row currency.

Default: 0

currencystringbodyoptional

Row currency (default: the plan currency). In account mode each row is one currency.

start_datestringbodyoptional

Own flight start (YYYY-MM-DD). Without it the row follows the plan window.

end_datestringbodyoptional

Own flight end (YYYY-MM-DD, inclusive).

statusenumbodyoptional

Default: draft for a campaign, active for a platform or account row.

Values: draft, scheduled, active, paused, completed, cancelled

label_idsarray<string>bodyoptional

Campaign labels of the row (ids from media_planner.labels.list).

mapped_campaign_idstringbodyoptional

Campaigns mode: the real campaign id on the ad platform to sync its spend (requires external_customer_id).

mapped_campaign_namestringbodyoptional

The linked campaign's name on the platform (shown in the plan).

external_customer_idstringbodyoptional

Ad account of the linked campaign: one of the organization's connected accounts for that platform.

manual_spendnumberbodyoptional

Platforms and account modes: spend entered by hand (a platform without integration, or an adjustment on top of the synced spend).

excluded_account_idsarray<string>bodyoptional

Account mode: connected ad accounts left out of this row's total.

orderintegerbodyoptional

Position in the plan (default: last).

Response

campaign_idstring

The new row's id.

campaignobject

The new row.

plan_totalsobject

The plan's totals after the change.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns" \
  -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}/campaigns" \
  -H "Authorization: Bearer $MB_TEST_KEY"
PATCH/v1/media-planner/plans/{plan_id}/campaigns/{campaign_id}

Update a row of a media plan

media_planner.campaigns.update

WriteIdempotentSince 2026-11-01

Updates one or more fields of a row, sent inside updates ({ plan_id, campaign_id, updates: { … } }): name, platform and platform_custom_name (campaigns mode), budget, currency, start_date/end_date (YYYY-MM-DD; null clears them so the row follows the plan), status, label_ids, the link to a real campaign (mapped_campaign_id + external_customer_id together; mapped_campaign_id null unlinks; changing the platform of a linked campaign unlinks it), manual_spend (null clears it; the known spend keeps the synced part), excluded_account_ids (account mode) and order. What a row is depends on the plan's mode: in campaigns mode it is a planned campaign (name and platform required; it can be linked to a real campaign with mapped_campaign_id + external_customer_id, an ad account connected to this organization for that platform, to sync its spend); in platforms mode it is one row per ad platform (platform required, one row per platform except 'other', which needs platform_custom_name; MB Suite names it and its spend is the total of that platform's accounts); in account mode it is one row per currency (MB Suite names it; excluded_account_ids leaves connected accounts out of its total). manual_spend (platforms and account modes) adds spend entered by hand. A platform row cannot change its platform (platform_immutable). Each change is kept in the campaign history (media_planner.changes.list); budget and currency changes recompute the plan's totals. skip_activity: true keeps the change out of the account's Activity feed, for automated syncs that name each row after its real campaign: it only goes with name and the link fields (mapped_campaign_id, mapped_campaign_name, external_customer_id); with any other field it is invalid_input (400) and nothing is saved. The change stays in the audit log and the history, and the webhook still fires. 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_campaigns_updateOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.update".

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

Parameters

plan_idstringpathrequired

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

campaign_idstringpathrequired

Row id inside the plan: a campaign, a platform row or an account row (from media_planner.campaigns.list).

updatesobjectbodyrequired

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

skip_activitybooleanbodyoptional

true: this change does not leave an entry in the account's Activity feed (for an automated process, such as keeping row names in sync with the real campaigns). It is still recorded in the audit log and in the campaign history (media_planner.changes.list), and media_planner.campaign.changed still fires. Only with name and the link fields (mapped_campaign_id, mapped_campaign_name, external_customer_id) in updates; with any other field the call is invalid_input.

Response

campaignobject

The row 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}/campaigns/{campaign_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}/campaigns/{campaign_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "updates": {}
}'
DELETE/v1/media-planner/plans/{plan_id}/campaigns/{campaign_id}

Delete a row of a media plan

media_planner.campaigns.delete

IrreversibleSince 2026-11-01

Deletes one row of a media plan and recomputes the plan's totals. The row goes to the 30-day trash, where an owner or admin can restore it (platform.trash.restore; its comments stay and come back with it, and restoring recomputes the plan totals; 409 trash_restore_parent_missing if the plan no longer exists). The deletion is kept in the plan's change history. Outside the app it needs a dry run 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_campaigns_deleteOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.delete".

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

Parameters

plan_idstringpathrequired

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

campaign_idstringpathrequired

Row id inside the plan: a campaign, a platform row or an account row (from media_planner.campaigns.list).

Response

oktrue

Always true.

plan_totalsobject

The plan's totals after the deletion.

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}/campaigns/{campaign_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}/campaigns/{campaign_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/media-planner/plans/{plan_id}/campaigns/reorder

Reorder the rows of a media plan

media_planner.campaigns.reorder

WriteIdempotentSince 2026-11-01

Sets the display order of a plan's rows: campaign_ids in the new order (position = index); rows not listed keep their relative order after them. All or nothing: an id that is not a row of the plan stops the whole reorder (404). In a platforms-mode plan the order matters: if two rows ever share a platform, the first one receives the platform's spend. Returns how many rows moved and the final order. Requires full access to the Media Planner.

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_campaigns_reorderOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.reorder".

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

Parameters

plan_idstringpathrequired

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

campaign_idsarray<string>bodyrequired

Row ids in the new order. Rows not listed keep their relative order after these.

Response

countnumber

Rows reordered.

orderarray<string>

The row ids in their new order.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns/reorder" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ]
}'

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}/campaigns/reorder" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ]
}'
POST/v1/media-planner/plans/{plan_id}/campaigns/bulk/update

Edit many rows of a media plan

media_planner.campaigns.bulk.update

WriteBulk · up to 500Since 2026-11-01

Applies the same change to up to 500 rows of a plan, like the selection bar of the app: status, start_date/end_date (YYYY-MM-DD; null clears), currency (not in account mode, where each row is one currency), add_label_ids and remove_label_ids. Ids that are not rows of the plan are skipped and returned in not_found; rows that would end before they start stop the whole call (date_range_invalid). Each changed row is kept in the campaign history. Outside the app it requires a dry run (which lists the rows) and a confirm_token. Requires full access to the Media Planner.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

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

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

Parameters

plan_idstringpathrequired

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

campaign_idsarray<string>bodyrequired

Row ids of the plan (from media_planner.campaigns.list), up to 500.

updatesobjectbodyrequired

What changes in every row (at least one field).

Response

updatednumber

Rows changed.

not_foundarray<string>

Ids sent that are not rows of the plan (skipped).

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns/bulk/update" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ],
  "updates": {},
  "dry_run": true
}'

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}/campaigns/bulk/update" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ],
  "updates": {},
  "dry_run": true
}'
POST/v1/media-planner/plans/{plan_id}/campaigns/bulk/delete

Delete many rows of a media plan

media_planner.campaigns.bulk.delete

IrreversibleBulk · up to 500Since 2026-11-01

Deletes up to 500 rows of a plan and recomputes its totals; each row goes to the 30-day trash and can be restored on its own. Ids that are not rows of the plan are skipped and returned in not_found. The deletions are kept in the plan's change history. Outside the app it requires a dry run (which lists the rows) and a confirm_token. Requires full access to the Media Planner.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

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

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

Parameters

plan_idstringpathrequired

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

campaign_idsarray<string>bodyrequired

Row ids of the plan (from media_planner.campaigns.list), up to 500.

Response

deletednumber

Rows deleted.

not_foundarray<string>

Ids sent that are not rows of the plan (skipped).

trash_item_idsarray<string>

The trash items this deletion left, one per record: the item_id of platform.trash.restore (POST /trash/{item_id}/restore).

trash_expires_atnumber

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

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns/bulk/delete" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ],
  "dry_run": true
}'

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}/campaigns/bulk/delete" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "campaign_ids": [
    "CAMPAIGN_ID"
  ],
  "dry_run": true
}'
POST/v1/media-planner/plans/{plan_id}/campaigns/import

Import campaigns from another plan

media_planner.campaigns.import

WriteSince 2026-11-01

Copies campaigns from another campaigns-mode plan of this organization (source_plan_id; campaign_ids to pick some, default all, up to 200) to the end of this plan, which must be in campaigns mode too. Each copy keeps name, platform, currency and labels, takes its original budget (without carryover), the window of this plan and status draft, and drops the link to the real campaign, the spend, the carryover and the Auto-Budget bookkeeping. Recomputes the plan's totals and keeps the import in the change history with the source plan's name. Returns the new row ids. Requires full access to the Media Planner.

Scope
media_planner:write
Permission
Edit in the module
MCP tool
media_planner_campaigns_importOn the full surface; on the compact one, mb_execute_operation with id: "media_planner.campaigns.import".

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

Parameters

plan_idstringpathrequired

The plan that receives the campaigns.

source_plan_idstringbodyrequired

A campaigns-mode plan of this organization to copy from.

campaign_idsarray<string>bodyoptional

Campaigns of the source plan to copy (default: all).

Response

imported_idsarray<string>

Ids of the rows created, in the order sent.

countnumber

Rows created.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/media-planner/plans/{plan_id}/campaigns/import" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "source_plan_id": "SOURCE_PLAN_ID"
}'

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}/campaigns/import" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "source_plan_id": "SOURCE_PLAN_ID"
}'