API Conventions

The rules every Spysay API endpoint follows: versioning, snake_case, object IDs, pagination, timestamps, HTTP methods and errors.

These rules apply to every endpoint, so the rest of the reference doesn't repeat them.

Versioning

All endpoints live under /v1. Breaking changes ship as a new version. Additive changes, like new fields, new endpoints or new enum values, can land in v1 at any time, so ignore fields you don't recognize.

txt
https://spysay.com/v1

snake_case

Every field name, query parameter and enum value is snake_case, in requests and responses.

json
{ "origin_host": "my-app.lovable.app", "on_demand_render": true, "status": "connected_via_api" }

Request bodies are strict. Sending a field that doesn't exist, including a camelCase spelling of a real one, fails with 400 parameter_unknown instead of being silently ignored:

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_unknown",
    "message": "Received unknown parameter: originHost",
    "param": "originHost",
    "doc_url": "/docs/api-reference/error-codes#parameter_unknown"
  }
}

Objects and IDs

Every object has an object field naming its type and an id whose prefix matches that type, so an ID on its own tells you what it refers to.

Prefix Object
dom_ domain
pur_ cache_purge
snp_ render snapshot
aud_ audit
pg_ audit_page
prm_ prompt
prr_ prompt_run
rpt_ visibility_report
biz_ business
mkt_ market
act_ action
json
{
  "id": "dom_8f3k2j9x",
  "object": "domain",
  "hostname": "your-app.com",
  "created_at": 1761350400
}

Passing the wrong kind of ID, for example an aud_ ID where a prm_ ID belongs, fails with 400 parameter_invalid.

Domains are the exception to ID-only references. Anywhere a domain is expected, whether the :domain path segment or a domain field or filter, you can pass either its dom_ ID or its hostname.

Lists and pagination

Every list endpoint returns the same envelope:

json
{
  "object": "list",
  "data": [{ "id": "prm_4k2", "object": "prompt" }, { "id": "prm_9xa", "object": "prompt" }],
  "has_more": true,
  "next_cursor": "prm_9xa"
}
Parameter Type Description
limit number Items per page, 1 to 100. Default 20.
starting_after string Pass the previous page's next_cursor to get the next page

Cursors stay stable while new items are added, so paging through a list won't skip or repeat anything. Every list has a fixed order with ties broken by ID (or by host for sources), so the same request always returns the same pages.

A starting_after that isn't in the list returns 400 parameter_invalid with param: "starting_after", instead of quietly starting over at the first page. That happens when the item was deleted after you fetched its page: start the listing again.

Lists filtered by a date window (sources and the analytics endpoints) default to to today. When you page through one of those, pass the same from and to on every request, so the window can't move under you if a check finishes while you're paging.

sh
curl "https://spysay.com/v1/prompts?business=biz_7hQm2&limit=100&starting_after=prm_9xa" \
  -H "x-spysay-api-key: <API_KEY>"
ts
import { createClient } from "@spysay/sdk";

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

let startingAfter: string | undefined;
do {
  const page = await spysay.prompts.list({ business: "biz_7hQm2", limit: 100, startingAfter });
  for (const prompt of page.data) handle(prompt);
  startingAfter = page.hasMore ? (page.nextCursor ?? undefined) : undefined;
} while (startingAfter);

Timestamps and dates

Points in time, such as created_at, started_at and last_run_at, are integer Unix seconds: 1761350400. Calendar days, such as from, to, date and the daily buckets in analytics, are YYYY-MM-DD strings in UTC: "2026-10-25". The API doesn't use milliseconds or ISO timestamp strings anywhere.

Booleans, lists and nested data

Booleans are true and false rather than 0 and 1; in a query string that means ?active=true. Lists are JSON arrays ("tags": ["brand", "pricing"]), and nested data is sent as JSON objects rather than as JSON encoded inside a string.

HTTP methods

Method Meaning Success
GET Retrieve or list 200
POST /v1/things Create 201 with the object
POST /v1/things/:id/verb Run an action 200
PATCH Partial update. Omitted fields don't change; lists replace wholesale. 200 with the full object
DELETE Delete 200 with { "id", "object", "deleted": true }

Errors

Failures return a Stripe-style error object with type, code, message, param and doc_url. Branch on type, then on code.