Skip to main content
Documentation

Marketing

marketing.ad_spend

Total what each ad campaign spent, and what it brought back
Read-onlyRead budget

REST

Shipping
POST /api/v1/marketing/ad_spend

MCP tool

Live
marketing.ad_spend

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

Operating contract

What the shop's advertising actually cost over a window, per campaign, with the clicks, impressions and conversions the ad platform reported against it.

cost_per_conversion and roas are the two numbers worth saying. A campaign spending steadily with ZERO conversions is the finding — it is money leaving with nothing attributed to it, and nothing else in this product will mention it.

CONVERSIONS ARE THE AD PLATFORM'S OWN COUNT, not this shop's leads. They can differ badly: a platform counts a form view or a call button tap, and this product counts a lead that arrived. When they disagree, leads.sources is the honest side of that comparison — it counts leads that actually landed here.

roas is conversion value ÷ cost and is null when the platform reported no value, which is the common case for a shop that never configured conversion values. Null is 'we cannot say', not zero.

Money is converted from the platform's micros here — never do that arithmetic yourself, and never quote a micros figure to anybody.

schema_missing: true means the ad-spend migration has not been applied to this shop's database.

This is a rollup over every day in the window rather than a page, so nothing is truncated.

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.

Input

FieldTypeDescription
daysinteger1–365

How many days back to total, 1-365. Defaults to 30.

Default: 30

Output

FieldTypeDescription
schema_missingboolean

window_daysnumber

currencystring

total_costnumber

total_clicksnumber

total_conversionsnumber

cost_per_conversionnumber | null

campaignsobject[]

campaigns[].campaign_idstring

campaigns[].campaign_namestring | null

campaigns[].costnumber

campaigns[].clicksnumber

campaigns[].impressionsnumber

campaigns[].conversionsnumber

campaigns[].cost_per_conversionnumber | null

campaigns[].roasnumber | null

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_spend \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"days":30}'

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

// 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_spend",
    headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
    json={
    "days": 30
},
    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_spend",
    "arguments": {
      "days": 30
    }
  }
}

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.