Skip to main content
Documentation

Marketing

marketing.ad_campaigns

List the shop's imported ad campaigns and their current settings
Read-onlyRead budget

REST

Shipping
POST /api/v1/marketing/ad_campaigns

MCP tool

Live
marketing.ad_campaigns

Exposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Marketing domain.

Operating contract

The ad campaigns this shop has imported from its ad account, with what each one is set to spend and whether it is running.

status is Google's own — ENABLED, PAUSED or REMOVED — reported as the last import saw it, not as a live read. last_seen_at says how fresh that is; a campaign whose last_seen_at is a week old was not in the most recent sync, which usually means it was deleted at the ad account.

managed IS THE CONSENT FLAG AND IT DEFAULTS TO FALSE. It is the owner's explicit 'yes, the optimizer may work on this campaign'. Importing is reading, never consent to act — so a campaign with managed: false is one the shop's autopilot will not touch, whatever else is switched on.

daily_budget is a setting, not a spend. What was actually spent is marketing.ad_spend, and the two routinely differ.

schema_missing: true means this shop's database has not had the ads-import migration applied, so an empty list is not evidence of no campaigns.

READ ONLY. Every ads write spends the shop's money at a third party, and switching the optimizer on is an owner action by design.

TRUNCATION: omitted is how many further campaigns exist. When it is not 0, raise limit before telling a shop what it is running.

Who may call it

Permission
marketing.accessThe caller must hold Reviews & market at the ACT level. A read-only dashboard grant on the same section is refused.
Plan
Every planNo plan gate. Available on every Service VIN plan.
Retries
naturalNaturally idempotent — running it twice leaves the same world as running it once. A retrying integration needs no key.
Rate class
readCounted against the read budget — the widest of the four.

Partial answers

omitted is how many matching records are not in the response, and 0 is a real answer meaning you have all of them. There is no cursor here — narrow the filter instead.

Do not summarise from a truncated response

When `omitted` is not 0 there are more campaigns than came back. Call again with a bigger `limit`.

Input

FieldTypeDescription
managed_onlyboolean

True returns only campaigns the owner opted into the optimizer for.

limitinteger1–200

How many campaigns to return, 1-200. Defaults to 50.

Default: 50

Output

FieldTypeDescription
schema_missingboolean

itemsobject[]

items[].idstring

items[].campaign_idstring

items[].namestring

items[].channelstring | null

items[].statusstring | null

items[].daily_budgetnumber | null

items[].bidding_strategystring | null

items[].managedboolean

items[].last_seen_atstring | null

omittednumber

Examples

Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.

curl
export SERVICEVIN_API_KEY=svk_live_…

curl -X POST https://www.servicevin.com/api/v1/marketing/ad_campaigns \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit":50}'

TypeScript (fetch)
const res = await fetch("https://www.servicevin.com/api/v1/marketing/ad_campaigns", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "limit": 50
  }),
});

// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;

Python (requests)
import os, requests

res = requests.post(
    "https://www.servicevin.com/api/v1/marketing/ad_campaigns",
    headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
    json={
    "limit": 50
},
    timeout=30,
)
payload = res.json()
if not res.ok:
    raise RuntimeError(payload["error"]["code"])
data = payload["data"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "marketing.ad_campaigns",
    "arguments": {
      "limit": 50
    }
  }
}

Refusals

The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.

StatusCodeWhen
404not_foundThe id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both.
403forbiddenThis login does not hold marketing.access.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many read calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.