Skip to main content
Documentation

Marketing

marketing.offers

List the discount campaigns running on the shop's booking page
Read-onlyRead budget

REST

Shipping
POST /api/v1/marketing/offers

MCP tool

Live
marketing.offers

Exposed 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

FieldTypeDescription
live_onlyboolean

True returns only promotions that are running right now.

Output

FieldTypeDescription
shop_wideobject | null

shop_wide.enabledboolean

shop_wide.percentnumber

shop_wide.labelstring

shop_wide.include_addonsboolean

shop_wide.starts_atstring | null

shop_wide.ends_atstring | null

shop_wide.phasestring

servicesobject[]

services[].service_idstring

services[].service_namestring

services[].kindstring

services[].valuenumber

services[].labelstring

services[].onlineboolean

services[].in_shopboolean

services[].starts_atstring | null

services[].ends_atstring | null

services[].phasestring

standing_countnumber

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

TypeScript (fetch)
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;

Python (requests)
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"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "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.

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 offers.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.