Businesses and Markets API

Create and update the business you track in Spysay, and the markets you check it from, down to a city or district.

A business is what Spysay tracks. It doesn't need a website: a shop known only by its Maps listing or Facebook page is tracked from those profiles. Each business has one or more markets, the places its prompts are asked from, as wide as a country or as narrow as a district.

The business object

json
{
  "id": "biz_7hQm2",
  "object": "business",
  "name": "Lumen Dental",
  "aliases": ["Lumen", "流明牙科"],
  "domain_id": "dom_8f3k2j9x",
  "profiles": ["https://maps.google.com/?cid=1234567890", "https://www.instagram.com/lumendental"],
  "verified_at": 1761350400,
  "first_check_at": 1761350460,
  "created_at": 1761350400
}

aliases are the other names customers and AI use, including names in other scripts. An answer naming any of them counts as a mention. An answer citing one of the profiles counts as citing the business. domain_id is null for a business without a website. verified_at is null until the business is verified. first_check_at is when its first check started; scheduled checks begin after it.

Create a business

POST /v1/businesses

Parameter Type Description
name (required) string Max 200 chars
aliases string[] Up to 20. Default [].
domain string dom_ ID or hostname of a connected domain. Leave it out for a business without a website.
profiles string[] Up to 20 URLs: Maps listings and social pages. Default [].

Returns 201 with the business.

sh
curl -X POST "https://spysay.com/v1/businesses" \
  -H "Content-Type: application/json" \
  -H "x-spysay-api-key: <API_KEY>" \
  -d '{"name":"Lumen Dental","aliases":["Lumen"],"profiles":["https://www.instagram.com/lumendental"]}'
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

const business = await spysay.businesses.create({
  name: "Lumen Dental",
  aliases: ["Lumen"],
  profiles: ["https://www.instagram.com/lumendental"],
});

List, retrieve, update and delete

Endpoint Description
GET /v1/businesses Lists businesses. Takes limit and starting_after.
GET /v1/businesses/:id Retrieves a business
PATCH /v1/businesses/:id Changes any create field. List fields replace wholesale. domain: null detaches the website.
DELETE /v1/businesses/:id Deletes the business with its markets, prompts and actions. Returns { "id": "biz_…", "object": "business", "deleted": true }.

Verify a business

On Growth, a business's prompts are checked only once it's verified as yours. Agency accounts skip this.

POST /v1/businesses/:id/verify

Verifies the business when the signed-in person's email is at the business's own website domain: its registrable domain, so an@lumen.dental verifies lumen.dental and shop.lumen.dental. An address on a subdomain (an@mail.lumen.dental), free mail such as Gmail, and throwaway mail never verify. It needs a signed-in session, not an API key, and returns the business.

A domain verifies one business, in one account, at a time. Adding a website outside the verified domain undoes the verification.

With a code in the body, it verifies with the six-digit code that POST /v1/businesses/:id/verification_emails mailed to an address at the website's domain instead, so an API key works too. A code works for an hour and for five wrong tries; sending a new one replaces it.

json
{ "code": "482913" }
Error When
verification_failed (422) The email isn't at the website's domain, there's no signed-in person, or the code is wrong or expired
business_already_claimed (409) Another account has verified this website

Markets

The market object

json
{
  "id": "mkt_3xUs",
  "object": "market",
  "business_id": "biz_7hQm2",
  "country": "US",
  "region": "Texas",
  "city": "Austin",
  "district": null,
  "languages": ["en"],
  "geo": { "lat": 30.2672, "lng": -97.7431, "timezone": "America/Chicago" },
  "competitors": ["Austin Smile Studio", "Bright Dental TX"],
  "created_at": 1761350400
}

region, city and district narrow the market step by step; leave the rest null to stay at that level. A prompt is written in one of the market's languages.

geo is the device location sent to providers that read it. With it, Google AI Mode and AI Overviews answer "near me" questions for that exact spot, whichever IP the request leaves from. ChatGPT and most other providers ignore it and go by IP alone, so every run records where it was really checked from in checked_from.

competitors are counted in answers alongside the business.

POST /v1/markets

Parameter Type Description
business (required) string biz_ ID
country (required) string ISO 3166-1 alpha-2, e.g. US, VN
region, city, district string Each optional, max 120 chars
languages (required) string[] 1 to 5 BCP 47 tags, e.g. ["en", "zh-Hans"]
geo object { lat, lng, timezone }, with an IANA time zone
competitors string[] Up to 20 names. Default [].

Returns 201 with the market.

sh
curl -X POST "https://spysay.com/v1/markets" \
  -H "Content-Type: application/json" \
  -H "x-spysay-api-key: <API_KEY>" \
  -d '{"business":"biz_7hQm2","country":"US","region":"Texas","city":"Austin","languages":["en"],"geo":{"lat":30.2672,"lng":-97.7431,"timezone":"America/Chicago"}}'
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

const market = await spysay.markets.create({
  business: "biz_7hQm2",
  country: "US",
  region: "Texas",
  city: "Austin",
  languages: ["en"],
  geo: { lat: 30.2672, lng: -97.7431, timezone: "America/Chicago" },
});
Endpoint Description
GET /v1/markets Lists a business's markets. Takes business (required), limit and starting_after.
GET /v1/markets/:id Retrieves a market
PATCH /v1/markets/:id Changes any field except business and country; a new country is a new market. null clears region, city, district or geo.
DELETE /v1/markets/:id Deletes the market and its prompts. Returns { "id": "mkt_…", "object": "market", "deleted": true }.
Status Codes
400 parameter_invalid, parameter_unknown, parameter_missing
404 resource_missing