Skip to content
GitHub
Get started →

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 tenant

All 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:

  1. 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 the site_id against the tenant in your DB.

  2. Set up the data adapter (optional)

    If the tenant has data you want the AI to search (services, listings, products), call PATCH /v1/sites/:id with 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_id as state.

  3. 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_ids filters the subscription to one tenant’s events. Omit it to subscribe to all your tenants’ events on a single endpoint.

  4. Render the snippet on the tenant’s site

    Bake the tenant’s site_id into their served HTML:

    <script
    src="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; only data-site-id differs.

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:

FieldPer-tenant choice
voicealloy / echo / fable / onyx / nova / shimmer (or any Gemini voice)
industryLoads the matching template prompt as a starting point
personalityFree-form custom instructions overlay
restricted_topicsHard-block topics for compliance (legal, medical disclaimers)
appearanceColor, position, size — match the tenant’s brand
pronunciationsBrand 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:

ApproachProCon
One workspace key, your SaaS controls all API callsSimple, all sites visible in one dashboardTenants 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 UIMore 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:

Terminal window
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 domain field must exactly match the Origin browsers send. Include both apex and www. 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