API reference

Statistics and campaign actions for your MarsDSP account, over HTTPS with a bearer token.

https://marsdsp.com/api/v1

Quick start

1. In MarsDSP, open Settings > API access and create a token. Pick only what it needs to do. The token is shown once: keep it in your secret store.

2. Send it in the Authorization header of every request. Every answer is JSON, except the .csv endpoints.

curl https://marsdsp.com/api/v1/me \
  -H "Authorization: Bearer $TOKEN"

curl "https://marsdsp.com/api/v1/stats?preset=yesterday&groupBy=campaign" \
  -H "Authorization: Bearer $TOKEN"

Tokens and scopes

A token belongs to one account and acts as the person who created it, with no more than their role allows today. If their role changes, the token can do less; if they leave the team, it stops. Owners and admins create and revoke tokens; a revoked or expired token answers 401 at once.

A token only works on MarsDSP's own address, https://marsdsp.com/api/v1.

stats.readRead statistics
campaigns.readRead campaigns, creatives and audiences
campaigns.writeCreate, edit, launch, pause, resume and end campaigns
wallet.readRead the wallet balance and transactions
library.readLook up placements in the inventory library
library.writeCreate library packages and add placements to them
webhooks.manageSubscribe to events with webhooks (Zapier, Make, n8n)

Money, time and paging

Money is always an integer in cents (2500 = 25.00) in the account's currency. Times are ISO 8601 in UTC; report days are UTC days.

Lists take page and pageSize and answer { items, total, page, pageSize }.

For hourly figures, call /campaigns/{id}/breakdown?dimension=hour. For a file with every metric per day, campaign and channel, call /stats/data.csv (the same file the scheduled stats emails attach). To get the same data by email every day or hour, schedule a report in Reports.

Changing campaigns safely

Calls that change something (POST and PATCH) accept an Idempotency-Key header of 8 to 128 characters. Send a new key for each change and the same key when you retry it: a retry answers the first result (with the header idempotent-replayed: true) instead of acting twice.

Every change goes through the same checks as the app (wallet balance, creative review, launch checks) and appears in the account's activity log.

curl -X PATCH https://marsdsp.com/api/v1/campaigns/$CAMPAIGN_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: budget-2026-10-18-1" \
  -d '{"dailyBudget": 7500}'

AI agents (MCP)

MarsDSP is also an MCP (Model Context Protocol) server, so an AI agent such as Claude, ChatGPT or your own can work in your account with the same token and scopes: read statistics, plan, create drafts and launch once you confirm. Point the agent at https://marsdsp.com/mcp (Streamable HTTP) and send the token as a Bearer header.

Every JSON endpoint below is a tool, named in snake_case (getCampaignStats is get_campaign_stats). The agent only sees the tools the token's scopes allow, and every tool call is an API call: the same checks, activity log and rate limit.

create_campaign_from_text turns one sentence ("$50 a day for my app in Israel") into a draft with your brand kit's defaults. launch_campaign and end_campaign change nothing unless called with confirm: true; without it, launch_campaign answers with the launch check, so the agent shows it to you and asks first.

# Claude Code
claude mcp add --transport http ads https://marsdsp.com/mcp \
  --header "Authorization: Bearer $TOKEN"

# Any client: one JSON-RPC message per POST
curl -X POST https://marsdsp.com/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Ad Context Protocol (AdCP)

The MCP server also answers the AdCP media-buy tasks, so a buying agent that speaks AdCP can plan and buy here without custom code. A media buy is one campaign (media_buy_id is the campaign id) and each package is one of its channels (package_id is the channel). Budgets in these tasks are in major units, as AdCP expects.

create_media_buy creates a draft and never spends: the buy is pending_activation until launch_campaign is called with confirm: true. Pass buyer_ref to make retries safe.

get_productsThe channels MarsDSP buys (display, native, video, CTV, audio, in-game, DOOH and more) with their formats and pricing models; needs campaigns.read.
create_media_buyA draft campaign with one channel per package, the total budget spread over the flight, its dates and countries; needs campaigns.write.
get_media_buy_deliveryImpressions, spend, clicks, completions and conversions per buy and per package; needs stats.read.

Errors and limits

Errors answer { error, message, details? } with the HTTP status below.

Each token may make 600 requests a minute. Over that, the answer is 429 with Retry-After in seconds.

400 bad_requestA parameter or the body is not valid; details say which.
401 invalid_tokenNo token, an unknown one, or one that was revoked or expired.
403 insufficient_scopeThe token lacks the scope this endpoint needs.
403 forbiddenThe person who created the token can't do this any more (their role changed).
404 not_foundNo such endpoint, or no such record in this account.
409 idempotency_in_progressA request with the same Idempotency-Key is still running.
422 idempotency_key_reusedThe Idempotency-Key was used for a different request.
429 rate_limitedToo many requests; wait Retry-After seconds.

Endpoints

Account

GET/me

The token and the account it works in

Use it to check a token: it returns the account, the token's scopes and when it expires.

Scope: Any token

Response

{
  "account": {
    "id": "01JB…",
    "name": "Acme",
    "type": "advertiser",
    "currency": "USD"
  },
  "token": {
    "id": "01JD…",
    "name": "BI export",
    "prefix": "dsp_a1b2c3d4",
    "scopes": [
      "stats.read"
    ],
    "expiresAt": "2027-01-16T08:00:00.000Z"
  }
}

Statistics

GET/stats

Statistics for the account

The same report as the Reports page: totals, the change against the comparison period, a time series and one row per group. An agency account's report covers its clients too. Money values are in cents.

Scope: stats.read

Parameters

  • preset (query, string, default last_30) The period.today | yesterday | last_7 | last_14 | last_30 | last_90 | mtd | last_month | qtd | ytd | custom
  • from (query, string) First day (YYYY-MM-DD, UTC) when preset is custom.
  • to (query, string) Last day (YYYY-MM-DD, UTC, inclusive) when preset is custom.
  • groupBy (query, string, default campaign) What each row is.campaign | channel | platform | pricing_model | goal | status | day | week | month | geo | device | os | placement | app_or_site | creative | audience | split | hour
  • granularity (query, string, default auto) Time step of the series.auto | day | week | month
  • compare (query, string, default previous) Period the deltas compare with.previous | last_year | none
  • campaign (query, string) Only these campaigns (comma-separated ids).
  • channel (query, string) Only these channels (comma-separated).
  • status (query, string) Only campaigns in these statuses (comma-separated).draft | learning | live | paused | paused_no_funds | ended
  • sort (query, string, default spend) Metric the rows are sorted by.spend | impressions | reach | clicks | ctr | views | completedViews | vtr | conversions | cvr | conversionValue | ecpm | cpc | cpv | cpcv | cpa | roas | profit | epc | billed | agencyMarkup
  • dir (query, string, default desc) Sort direction.asc | desc
  • limit (query, integer, default 50) Rows to return (up to 500).

Response

{
  "period": {
    "from": "2026-09-18",
    "to": "2026-10-17"
  },
  "currency": "USD",
  "totals": {
    "spend": 125000,
    "impressions": 980000,
    "clicks": 6400,
    "ctr": 0.0065,
    "conversions": 212,
    "cpa": 590,
    "roas": 3.1
  },
  "rows": [
    {
      "key": "01JC0Q…",
      "label": "Spring sale",
      "metrics": {
        "spend": 80000,
        "impressions": 610000,
        "clicks": 4100,
        "conversions": 150
      }
    }
  ],
  "rowsTotal": 1
}
GET/stats.csv

Statistics for the account, as CSV

The rows of /stats as a CSV file, with the same parameters.

Scope: stats.read · Answers a CSV file.

Parameters

  • preset (query, string, default last_30) The period.today | yesterday | last_7 | last_14 | last_30 | last_90 | mtd | last_month | qtd | ytd | custom
  • from (query, string) First day (YYYY-MM-DD, UTC) when preset is custom.
  • to (query, string) Last day (YYYY-MM-DD, UTC, inclusive) when preset is custom.
  • groupBy (query, string, default campaign) What each row is.campaign | channel | platform | pricing_model | goal | status | day | week | month | geo | device | os | placement | app_or_site | creative | audience | split | hour
  • granularity (query, string, default auto) Time step of the series.auto | day | week | month
  • compare (query, string, default previous) Period the deltas compare with.previous | last_year | none
  • campaign (query, string) Only these campaigns (comma-separated ids).
  • channel (query, string) Only these channels (comma-separated).
  • status (query, string) Only campaigns in these statuses (comma-separated).draft | learning | live | paused | paused_no_funds | ended
  • sort (query, string, default spend) Metric the rows are sorted by.spend | impressions | reach | clicks | ctr | views | completedViews | vtr | conversions | cvr | conversionValue | ecpm | cpc | cpv | cpcv | cpa | roas | profit | epc | billed | agencyMarkup
  • dir (query, string, default desc) Sort direction.asc | desc
  • limit (query, integer, default 50) Rows to return (up to 500).
GET/stats/data.csv

Every metric per day, campaign and channel, as CSV

The same data file the scheduled stats emails attach: one line per UTC day, campaign and channel with every metric, for the period and filters of /stats. Use preset=today for today so far.

Scope: stats.read · Answers a CSV file.

Parameters

  • preset (query, string, default last_30) The period.today | yesterday | last_7 | last_14 | last_30 | last_90 | mtd | last_month | qtd | ytd | custom
  • from (query, string) First day (YYYY-MM-DD, UTC) when preset is custom.
  • to (query, string) Last day (YYYY-MM-DD, UTC, inclusive) when preset is custom.
  • campaign (query, string) Only these campaigns (comma-separated ids).
  • channel (query, string) Only these channels (comma-separated).
  • status (query, string) Only campaigns in these statuses (comma-separated).draft | learning | live | paused | paused_no_funds | ended
GET/campaigns/:id/stats

Statistics for one campaign

Totals and a daily series for the last `days` days.

Scope: stats.read

Parameters

  • id (path, string, required) The campaign id.
  • days (query, integer, default 30) How many days back from today (1 to 365).
GET/campaigns/:id/stats.csv

Daily statistics for one campaign, as CSV

Scope: stats.read · Answers a CSV file.

Parameters

  • id (path, string, required) The campaign id.
  • days (query, integer, default 30) How many days back from today (1 to 365).
GET/campaigns/:id/breakdown

One campaign split by hour, creative, country, device and more

Use dimension=hour for hourly figures.

Scope: stats.read

Parameters

  • id (path, string, required) The campaign id.
  • dimension (query, string, required) How to split the figures.creative | geo | device | os | placement | hour | audience
  • days (query, integer, default 30) How many days back from today (1 to 365).

Response

[
  {
    "key": "14",
    "label": "14:00",
    "impressions": 41000,
    "clicks": 270,
    "conversions": 9,
    "spend": 5200,
    "ctr": 0.0066,
    "costPerKpi": 578
  }
]
GET/agency/summary

Agency accounts: the last 30 days per client

Spend, what each client is billed, your markup and fee share, per client and in total. Agency accounts only.

Scope: stats.read

Campaigns

GET/campaigns

List campaigns

Scope: campaigns.read

Parameters

  • status (query, string) Only these statuses (comma-separated).draft | learning | live | paused | paused_no_funds | ended
  • q (query, string) Name contains.
  • page (query, integer, default 1) Page number, from 1.
  • pageSize (query, integer, default 25) Rows per page (up to 100).

Response

{
  "items": [
    {
      "id": "01JC0Q6Z8M2R7K4S9T1V3W5X7Y",
      "name": "Spring sale",
      "goal": "sales",
      "status": "live",
      "pricingModel": "cpm",
      "dailyBudget": 5000,
      "totalBudget": null,
      "startAt": "2026-10-01T00:00:00.000Z",
      "endAt": null,
      "channels": [
        {
          "channel": "display",
          "sharePct": 60
        },
        {
          "channel": "ctv",
          "sharePct": 40
        }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 25
}
GET/campaigns/:id

One campaign

Scope: campaigns.read

Parameters

  • id (path, string, required) The campaign id.

Response

{
  "id": "01JC0Q6Z8M2R7K4S9T1V3W5X7Y",
  "name": "Spring sale",
  "goal": "sales",
  "status": "live",
  "pricingModel": "cpm",
  "dailyBudget": 5000,
  "totalBudget": null,
  "startAt": "2026-10-01T00:00:00.000Z",
  "endAt": null,
  "channels": [
    {
      "channel": "display",
      "sharePct": 60
    },
    {
      "channel": "ctv",
      "sharePct": 40
    }
  ]
}
GET/campaigns/:id/launch-check

What a draft still needs before it can launch

Scope: campaigns.read

Parameters

  • id (path, string, required) The campaign id.
GET/campaigns/:id/changes

Changes the optimizer made to a campaign, each with its reason

Scope: campaigns.read

Parameters

  • id (path, string, required) The campaign id.

Campaign actions

POST/campaigns

Create a draft campaign

Creates a draft with the goal's recommended channel mix. Set its budget, dates, audiences and creatives with PATCH, then launch it.

Scope: campaigns.write

Body

  • goal (string, required) The campaign goal, e.g. sales, leads, app_installs, awareness, traffic, video_views.
  • name (string) Up to 120 characters.
  • kpiTarget (integer) Target cost per result in cents (×100 for ROAS: 250 = 2.5x). Omit for the benchmark.
  • pricingModel (string) How you are charged (default cpm).
  • unitPrice (integer) All-in price per billable unit in cents; required for every model but cpm.
  • channels (array) Starting mix: [{ channel, sharePct }]. Omit for the recommended mix.
{
  "goal": "sales",
  "name": "Spring sale"
}

Response

{
  "id": "01JC0Q6Z8M2R7K4S9T1V3W5X7Y",
  "name": "Spring sale",
  "goal": "sales",
  "status": "draft",
  "pricingModel": "cpm",
  "dailyBudget": 5000,
  "totalBudget": null,
  "startAt": "2026-10-01T00:00:00.000Z",
  "endAt": null,
  "channels": [
    {
      "channel": "display",
      "sharePct": 60
    },
    {
      "channel": "ctv",
      "sharePct": 40
    }
  ]
}
POST/campaigns/from-text

Create a draft campaign from one sentence

"$50 a day for my app in Israel" becomes a draft with the goal, daily budget, countries and interests it names; the rest comes from the account's brand kit (name, target, channel mix, AI-picked audiences). Nothing is launched. `defaulted` lists what the sentence didn't give.

Scope: campaigns.write

Body

  • text (string, required) The request in plain words, up to 500 characters (English or Hebrew).
  • locale (string) Language of the summary (en or he); defaults to the sentence's own.
{
  "text": "$50 a day for my app in Israel"
}

Response

{
  "campaign": {
    "id": "01JC…",
    "name": "Acme – App installs",
    "status": "draft",
    "goal": "installs",
    "dailyBudget": 5000,
    "currency": "USD",
    "geo": [
      "IL"
    ],
    "interests": []
  },
  "defaulted": [],
  "summary": "Created the draft \"Acme – App installs\": App installs, $50/day, Israel.\nReview it, add creatives and launch it from the wizard. Nothing runs until it is launched.",
  "reviewPath": "/wizard/01JC…/audience"
}
PATCH/campaigns/:id

Edit a campaign: budget, dates, name, target, channels, audiences, creatives

Send only the fields you change. Budgets are in cents (daily budget at least 500).

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.

Body

  • name (string) Up to 120 characters.
  • dailyBudget (integer) Daily budget in cents (at least 500).
  • totalBudget (integer | null) Lifetime budget in cents; null for none.
  • startAt (string) ISO date-time.
  • endAt (string | null) ISO date-time; null to run until stopped.
  • kpiTarget (integer) Target cost per result in cents.
  • autopilot (boolean) Let the optimizer move budget between channels.
  • channels (array) [{ channel, sharePct, pinned? }].
  • audiences (array) [{ audienceId }].
  • creativeIds (array) Creative ids to run.
  • targeting (object) { geo, cities, ageMin, ageMax, genders, devices, os, languages… } (any subset).
{
  "dailyBudget": 7500
}

Response

{
  "id": "01JC0Q6Z8M2R7K4S9T1V3W5X7Y",
  "name": "Spring sale",
  "goal": "sales",
  "status": "live",
  "pricingModel": "cpm",
  "dailyBudget": 7500,
  "totalBudget": null,
  "startAt": "2026-10-01T00:00:00.000Z",
  "endAt": null,
  "channels": [
    {
      "channel": "display",
      "sharePct": 60
    },
    {
      "channel": "ctv",
      "sharePct": 40
    }
  ]
}
POST/campaigns/:id/launch

Launch a draft

Runs the same checks as the app (wallet, creatives, review). A refused launch answers 422 with the checklist in details.

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.
POST/campaigns/:id/pause

Pause a campaign

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.
POST/campaigns/:id/resume

Resume a paused campaign

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.
POST/campaigns/:id/end

End a campaign for good

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.
POST/campaigns/:id/duplicate

Copy a campaign into a new draft

Scope: campaigns.write

Parameters

  • id (path, string, required) The campaign id.

Creatives

GET/creatives

List creatives

Scope: campaigns.read

Parameters

  • campaignId (query, string) Only the creatives of this campaign.
  • formatId (query, string) Only this format.
GET/creatives/:id

One creative

Scope: campaigns.read

Parameters

  • id (path, string, required) The creative id.

Audiences

GET/audiences

Search the audience catalogue and your own audiences

Scope: campaigns.read

Parameters

  • q (query, string) Search words.
GET/audiences/:id

One audience

Scope: campaigns.read

Parameters

  • id (path, string, required) The audience id.

Inventory library

GET/library/sites

What the library knows about a website or app

Pass a domain, a URL or an app id. Answers the library's placement for it (score, warnings, measured viewability and invalid traffic, ads.txt and store readings), or item null when the library has none, and the account's packages with whether each already holds it. The browser extension calls this on every site.

Scope: library.read

Parameters

  • domain (query, string, required) A domain, URL or app id, for example news.example.com.

Response

{
  "domain": "news.example.com",
  "item": {
    "id": "01JE…",
    "name": "News Example",
    "kind": "web",
    "score": 82,
    "lowViewability": false,
    "highIvt": false
  },
  "packages": [
    {
      "id": "01JF…",
      "name": "News sites",
      "hasItem": false
    }
  ]
}
POST/library/packages

Create a package of placements

A new package in the account's library. Start a campaign from it with /wizard?package=<id>.

Scope: library.write

Body

  • name (string, required) Up to 80 characters.
  • itemIds (array) Library placement ids (from /library/sites).
{
  "name": "news.example.com",
  "itemIds": [
    "01JE…"
  ]
}

Response

{
  "package": {
    "id": "01JF…",
    "name": "news.example.com"
  }
}
POST/library/packages/:id/items

Add placements to a package

Adds the placements a package does not hold yet; the ones it holds stay as they are, so a repeat is harmless.

Scope: library.write

Parameters

  • id (path, string, required) The package id.

Body

  • itemIds (array, required) Library placement ids.
{
  "itemIds": [
    "01JE…"
  ]
}

Wallet

GET/wallet

Wallet balance and today's spend

Scope: wallet.read

Response

{
  "balanceCents": 245000,
  "currency": "USD"
}
GET/wallet/ledger

Wallet transactions

Scope: wallet.read

Parameters

  • page (query, integer, default 1) Page number, from 1.
  • pageSize (query, integer, default 50) Rows per page (up to 200).
GET/wallet/ledger.csv

Every wallet transaction, as CSV

Scope: wallet.read · Answers a CSV file.

Webhooks

GET/webhooks

The account's webhooks and the events they can subscribe to

Scope: webhooks.manage

POST/webhooks

Subscribe a URL to events

Each event is a signed POST (Standard Webhooks headers webhook-id, webhook-timestamp and webhook-signature, HMAC-SHA256 with the secret this call returns once). Failed deliveries retry for about a day and a half. This is the REST hook Zapier, Make and n8n subscribe with.

Scope: webhooks.manage

Body

  • url (string, required) An https address that answers 2xx within 10 seconds.
  • events (string[], required) Events to send, such as campaign.launched or wallet.low.
  • description (string) A note to tell webhooks apart.
{
  "url": "https://hooks.zapier.com/hooks/standard/123/abc/",
  "events": [
    "campaign.launched",
    "wallet.low"
  ]
}

Response

{
  "item": {
    "id": "01JF…",
    "url": "https://hooks.zapier.com/hooks/standard/123/abc/",
    "events": [
      "campaign.launched",
      "wallet.low"
    ],
    "active": true
  },
  "secret": "whsec_…"
}
DELETE/webhooks/:id

Unsubscribe a webhook

Scope: webhooks.manage

Parameters

  • id (path, string, required) The webhook id.

Response

{
  "ok": true
}
GET/webhooks/sample

An example of an event's body

Scope: webhooks.manage

Parameters

  • event (query, string, required) The event, such as campaign.launched.

Response

{
  "id": "evt_01JF…",
  "type": "campaign.launched",
  "createdAt": "2026-10-26T08:00:00.000Z",
  "account": {
    "id": "01JB…"
  },
  "data": {
    "campaign": {
      "id": "01JC…",
      "name": "Spring sale"
    }
  }
}