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
| Method | Path | Scope |
|---|---|---|
POST | /v1/sites | sites:write |
GET | /v1/sites | sites:read |
GET | /v1/sites/:id | sites:read |
PATCH | /v1/sites/:id | sites:write |
DELETE | /v1/sites/:id | sites: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/sitesAuthorization: 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=0Authorization: Bearer vk_live_...Query params:
| Param | Default | Notes |
|---|---|---|
limit | 25 | Max 100 |
offset | 0 | |
q | — | Substring match on business_name / domain |
Response 200:
{ "success": true, "data": [ { /* SiteConfig */ }, ... ], "meta": { "total": 42, "limit": 25, "offset": 0 }}Get one site
GET /v1/sites/ab1c2d3eAuthorization: Bearer vk_live_...Returns one SiteConfig. 404 if the site isn’t yours.
Update a site
PATCH /v1/sites/ab1c2d3eAuthorization: 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/ab1c2d3eAuthorization: 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-connectionAuthorization: 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
| Code | Cause |
|---|---|
invalid_request | Body failed schema validation |
site_id_taken | Someone else claimed this site_id; try another |
domain_taken | You already have a site on this domain |
plan_limit_reached | Your plan’s site limit is full — upgrade or delete an existing site |