Documentation
Marketing
marketing.offers
REST
ShippingPOST /api/v1/marketing/offersMCP tool
Livemarketing.offersExposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Marketing domain.
Operating contract
Every promotion this shop currently has on: the shop-wide percentage off online bookings, and any per-service discount, with what each one takes off and when it runs.
phase IS THE ANSWER TO 'IS THIS ACTUALLY ON'. live is running now, scheduled has not started, ended has finished, and standing is the dangerous one — a discount with NO end date, which keeps discounting every booking until somebody notices. A shop asking why margins are down is often looking at a standing offer nobody meant to leave on.
ends_at: null on a live offer is exactly that case. Say it out loud.
The shop-wide offer is always a PERCENTAGE, deliberately: a flat dollar promo means something different for every booking, because it multiplies with however many lines a customer orders.
online says whether the discount reaches the public booking page; a per-service discount can be set to apply in the shop only, and that is a different promise to a different audience.
READ ONLY. Starting, changing or ending an offer re-prices the shop's public booking page immediately, with nothing between the call and the customer.
This is a rollup over the shop's settings and its live services, so nothing is truncated.
Who may call it
- Permission
offers.accessThe caller must hold Offers 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 |
|---|---|---|
| live_only | boolean | True returns only promotions that are running right now. |
Output
| Field | Type | Description |
|---|---|---|
| shop_wide | object | null | — |
| shop_wide.enabled | boolean | — |
| shop_wide.percent | number | — |
| shop_wide.label | string | — |
| shop_wide.include_addons | boolean | — |
| shop_wide.starts_at | string | null | — |
| shop_wide.ends_at | string | null | — |
| shop_wide.phase | string | — |
| services | object[] | — |
| services[].service_id | string | — |
| services[].service_name | string | — |
| services[].kind | string | — |
| services[].value | number | — |
| services[].label | string | — |
| services[].online | boolean | — |
| services[].in_shop | boolean | — |
| services[].starts_at | string | null | — |
| services[].ends_at | string | null | — |
| services[].phase | string | — |
| standing_count | number | — |
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/offers \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch("https://www.servicevin.com/api/v1/marketing/offers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
// 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/offers",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={},
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.offers",
"arguments": {}
}
}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 offers.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. |