Documentation
Marketing
marketing.ad_spend
REST
ShippingPOST /api/v1/marketing/ad_spendMCP tool
Livemarketing.ad_spendExposed 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
| Field | Type | Description |
|---|---|---|
| days | integer1–365 | How many days back to total, 1-365. Defaults to 30. Default:30 |
Output
| Field | Type | Description |
|---|---|---|
| schema_missing | boolean | — |
| window_days | number | — |
| currency | string | — |
| total_cost | number | — |
| total_clicks | number | — |
| total_conversions | number | — |
| cost_per_conversion | number | null | — |
| campaigns | object[] | — |
| campaigns[].campaign_id | string | — |
| campaigns[].campaign_name | string | null | — |
| campaigns[].cost | number | — |
| campaigns[].clicks | number | — |
| campaigns[].impressions | number | — |
| campaigns[].conversions | number | — |
| campaigns[].cost_per_conversion | number | null | — |
| campaigns[].roas | number | 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.
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}'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;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"]{
"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.
| Status | Code | When |
|---|---|---|
| 404 | not_found | The id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both. |
| 403 | forbidden | This login does not hold marketing.access. |
| 422 | validation_error | An argument was wrong. The message names the field. |
| 429 | rate_limited | Too many read calls. Back off and retry. |
| 500 | internal_error | Something failed on our side. Nothing was changed. |