Multi-tenant SaaS with Spelo
If you’re a SaaS that hosts websites for your customers — agencies, white-label site builders, dental practices on a shared platform, real-estate franchises, etc. — you can install Spelo across every tenant with one workspace and one integration.
This guide covers the architecture, the API moves, and the gotchas.
The mental model: one site_id per tenant
Spelo’s unit of config is a site. A site has:
- A
site_id(the public string you paste into<script data-site-id="...">) - A registered domain (the customer’s site URL)
- Voice + personality config
- Optional data-adapter config (Postgres / Shopify / etc.)
- Optional lead-capture + webhook config
The mapping is:
Your SaaS workspace├── Site (site_id=ab1c2d3e) ← Tenant A's "Restaurant DTLA"├── Site (site_id=4f5g6h7i) ← Tenant B's "Real Estate Vancouver"├── Site (site_id=8j9k0lmn) ← Tenant C's "DUI Lawyers Phoenix"└── ... one site per tenantAll sites share one API key (issued to your workspace) for programmatic management. End-users on each tenant’s site never see that key — they see only the public site_id baked into the script tag.
Provisioning a new tenant — the 4 API calls
When a new tenant signs up on your platform:
-
Create a site for them
Terminal window curl -X POST https://api.spelo.ai/v1/sites \-H "Authorization: Bearer vk_live_..." \-H "Content-Type: application/json" \-d '{"business_name": "Patel Dental — Westwood","business_description": "Family dentistry, root canals, emergency care.","domain": "pateldental.com","industry": "healthcare","timezone": "America/Los_Angeles","language": "en"}'Returns a full SiteConfig including a generated
site_id. Store thesite_idagainst the tenant in your DB. -
Set up the data adapter (optional)
If the tenant has data you want the AI to search (services, listings, products), call
PATCH /v1/sites/:idwith the adapter config:{"adapter": {"type": "postgres","config": {"connectionString": "postgresql://readonly:...@tenant-db.example.com:5432/db"},"collections": { "services": { "source": "service_listings", ... } }}}For OAuth-based adapters (Shopify, Airtable, Google Sheets, WooCommerce), use the OAuth flow with the
tenant_idas state. -
Register webhooks for the tenant
Either a per-tenant webhook (you give every tenant their own endpoint) or a single shared endpoint that filters by
site_id:Terminal window curl -X POST https://api.spelo.ai/v1/webhooks \-H "Authorization: Bearer vk_live_..." \-d '{"url": "https://yourapp.com/spelo-webhooks?tenant_id=acme-co","events": ["lead.captured", "conversation.ended"],"site_ids": ["ab1c2d3e"]}'site_idsfilters the subscription to one tenant’s events. Omit it to subscribe to all your tenants’ events on a single endpoint. -
Render the snippet on the tenant’s site
Bake the tenant’s
site_idinto their served HTML:<scriptsrc="https://spelo.ai/spelo.js"data-site-id="{{ tenant.spelo_site_id }}"async></script>The
{{ }}is your templating syntax — Liquid, Handlebars, Jinja, JSX, whatever you use. The widget itself is identical across all tenants; onlydata-site-iddiffers.
That’s it. The tenant’s visitors get a voice orb tuned to that tenant’s business.
Domain verification
Each site has a domain field. Spelo enforces it on every API call — the browser’s Origin header must match domain (or one of the registered alternatives). This prevents cross-tenant leakage: if someone scrapes Tenant A’s script tag and pastes it on a different domain, Tenant A’s config doesn’t load.
For multi-domain tenants (e.g. apex + www, or custom domains + your SaaS subdomain):
PATCH /v1/sites/:id{ "additional_domains": ["www.pateldental.com", "pateldental.acme-saas.com"]}Adding a new custom domain mid-lifecycle is one PATCH call. The change takes effect at the next config-refresh tick (≤5s).
Voice + personality per tenant
Every tenant can have completely different voice config, even on the same plan:
| Field | Per-tenant choice |
|---|---|
voice | alloy / echo / fable / onyx / nova / shimmer (or any Gemini voice) |
industry | Loads the matching template prompt as a starting point |
personality | Free-form custom instructions overlay |
restricted_topics | Hard-block topics for compliance (legal, medical disclaimers) |
appearance | Color, position, size — match the tenant’s brand |
pronunciations | Brand names, product names spoken correctly per tenant |
Spin up a “default config” template object in your provisioning code, then merge per-tenant overrides on top.
Lead routing — three patterns
Captured leads need to land in the right tenant’s CRM. Three viable patterns:
Pattern A — shared webhook, filter by site_id
One webhook URL on your platform handles all tenants’ leads. Your handler routes by site_id.
app.post('/spelo-webhooks', async (req, res) => { const { event, data } = req.body; if (event === 'lead.captured') { const tenant = await db.tenants.findBySpeloSiteId(data.site_id); await tenant.crm.createLead(data); } res.json({ ok: true });});Pro: one webhook to maintain. Con: every lead transits your servers.
Pattern B — per-tenant webhooks
Each tenant gets a dedicated webhook URL pointing at their own CRM (HubSpot, Salesforce, Pipedrive, custom).
Pro: leads go direct to tenant CRM, no proxy. Con: you maintain N webhook configs.
Best when tenants want to “own” their lead data and you don’t want it touching your servers.
Pattern C — Zapier / Make.com per tenant
Each tenant configures their own Zapier / Make zap that watches their site_id’s lead events.
Pro: tenant self-serves. Con: requires tenant to be technical enough to wire it up.
API key strategy
Two viable approaches:
| Approach | Pro | Con |
|---|---|---|
| One workspace key, your SaaS controls all API calls | Simple, all sites visible in one dashboard | Tenants can’t manage their own site from your dashboard |
| Per-tenant scoped keys (issue one key per tenant on signup) | Tenant can see their own usage / config via your UI | More keys to rotate; need a key-vault layer |
Most SaaS pick one workspace key + a custom UI layer in their own product that proxies to Spelo’s API. Spelo’s dashboard then is your dashboard (you, the SaaS), not your customers’.
If you do issue per-tenant keys, scope them to sites:read,write for that tenant’s site_id only — see API Keys. The Spelo API enforces scope on every call.
Billing model
You pay Spelo for total usage across all tenants, then meter your customers however you like (per-minute, per-conversation, per-month tier, included-in-plan, etc.).
Use the analytics endpoint to pull per-site_id usage and roll it up:
curl https://api.spelo.ai/v1/analytics?from=2026-04-01&to=2026-04-30&group_by=site_id \ -H "Authorization: Bearer vk_live_..."Returns minutes used per tenant for the month. Pipe into your billing system.
Common gotchas
- CORS / domain mismatch — most common provisioning bug. The tenant’s
domainfield must exactly match theOriginbrowsers send. Include both apex andwww.versions, and any staging subdomains. - OAuth callback URLs — for OAuth-based adapters, the redirect URL is per-tenant. Spelo handles this automatically when you initiate OAuth via the dashboard or
/v1/sites/:id/oauth/start. See OAuth. - Pronunciation dictionary leakage — pronunciations are per-site, not per-workspace. If two tenants both want “AT&T” pronounced “ay-tee-and-tee”, you set it on both. Use your provisioning template.
- Rate limits — per-key (workspace-wide). If you have 10,000 tenants and a burst of signups, you can hit the 1,000 req/min API limit. Throttle provisioning, or contact sales for a higher limit.
Plans + pricing
- Free — single site only. Not useful for SaaS.
- Starter — 5 sites max.
- Pro — 50 sites.
- Business — unlimited sites, dedicated support, custom domains for OAuth callbacks, SSO into the Spelo dashboard.
- Enterprise — multi-region deployment, dedicated infra, contract billing.
See Plans and limits for current pricing.
See also
- Sites API — CRUD operations
- Webhooks — event subscription
- Analytics endpoint — per-site usage
- API Keys — scoping for per-tenant access
- Plans and limits — tier comparison