Skip to content
GitHub
Get started →

Sites API

A site is the unit of configuration for Spelo. One site = one set of config (domain, personality, data connection, etc.) = one site_id you paste into the <script> tag.

Endpoints

MethodPathScope
POST/v1/sitessites:write
GET/v1/sitessites:read
GET/v1/sites/:idsites:read
PATCH/v1/sites/:idsites:write
DELETE/v1/sites/:idsites:write

All require Authorization: Bearer <API_KEY>. See Authentication.

SiteConfig shape

interface SiteConfig {
id: string; // internal ID
site_id: string; // public ID used in data-site-id (8-32 chars)
business_name: string;
business_description: string;
domain: string; // e.g. "example.com"
industry:
| 'restaurant'
| 'real_estate'
| 'law_firm'
| 'ecommerce'
| 'healthcare'
| 'professional_services'
| 'customer_support'
| 'custom';
voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
personality: string;
greeting?: string;
appearance: {
color: string; // hex
position: 'bottom-left' | 'bottom-center' | 'bottom-right';
size: 'small' | 'medium' | 'large';
edge_glow: boolean;
};
timezone: string; // IANA, e.g. "America/Los_Angeles"
enabled_pages: string[]; // URL patterns
disabled_pages: string[];
restricted_topics: string[];
pronunciations: { word: string; say_as: string }[];
custom_instructions?: string;
language: string; // "en", "es", ...
created_at: string;
updated_at: string;
}

Full type definition: packages/shared/src/types.ts.

Create a site

POST /v1/sites
Authorization: Bearer vk_live_...
Content-Type: application/json
{
"business_name": "Ember & Oak",
"domain": "emberandoak.com",
"industry": "restaurant",
"timezone": "America/Los_Angeles",
"language": "en"
}

All other fields get sensible defaults from the industry template. Returns a full SiteConfig with generated site_id.

Response 201:

{
"success": true,
"data": {
"id": "uuid-1234",
"site_id": "ab1c2d3e",
"business_name": "Ember & Oak",
...
}
}

List sites

GET /v1/sites?limit=25&offset=0
Authorization: Bearer vk_live_...

Query params:

ParamDefaultNotes
limit25Max 100
offset0
qSubstring match on business_name / domain

Response 200:

{
"success": true,
"data": [ { /* SiteConfig */ }, ... ],
"meta": { "total": 42, "limit": 25, "offset": 0 }
}

Get one site

GET /v1/sites/ab1c2d3e
Authorization: Bearer vk_live_...

Returns one SiteConfig. 404 if the site isn’t yours.

Update a site

PATCH /v1/sites/ab1c2d3e
Authorization: Bearer vk_live_...
Content-Type: application/json
{
"personality": "You are a friendly concierge at Ember & Oak...",
"pronunciations": [
{ "word": "Ember & Oak", "say_as": "EM-ber and oak" }
]
}

Partial update — send only the fields you want to change. The response is the full updated SiteConfig.

Delete a site

DELETE /v1/sites/ab1c2d3e
Authorization: Bearer vk_live_...

Response 204 (no body).

Deletion:

  • Immediately invalidates the site_id — the widget will stop loading on any page using it
  • Purges all transcripts and session metadata within 24 hours
  • Revokes any connected OAuth tokens (Shopify, Airtable, Google, WooCommerce)
  • Encrypted credentials in the data connection are securely wiped

Deletion cannot be undone.

Test connection

After updating the data connection, test it:

POST /v1/sites/ab1c2d3e/test-connection
Authorization: Bearer vk_live_...

Response 200:

{
"success": true,
"data": { "ok": true, "latency_ms": 42 }
}

Or on failure:

{
"success": true,
"data": {
"ok": false,
"error": "password authentication failed for user spelo_readonly"
}
}

Error codes

CodeCause
invalid_requestBody failed schema validation
site_id_takenSomeone else claimed this site_id; try another
domain_takenYou already have a site on this domain
plan_limit_reachedYour plan’s site limit is full — upgrade or delete an existing site

See also