---
title: Multi-tenancy
path: concepts/multi-tenancy
status: published
---

# Multi-tenancy

ScaiSend is multi-tenant by default. Every object — API key, template, message, suppression, webhook endpoint, sender domain — belongs to a tenant, and every request resolves to exactly one tenant before any business logic runs. This page describes the hierarchy and how your credentials map to it.

## The three levels

```
Partner
  └── Tenant
        └── User
```

**Partner.** The top level. A partner is typically a reseller, an agency, or a platform building on top of ScaiSend. Partners group tenants together and can see aggregate stats across their tenants.

**Tenant.** An isolated environment within a partner. A single end customer of the platform. Tenants own the entities developers actually create: API keys, templates, messages, suppressions, webhook endpoints, sender domains. Cross-tenant visibility is **not supported**.

**User.** A human identity, scoped to a tenant via role assignments. A user can belong to multiple tenants by holding roles in each; the active tenant is resolved per request from the JWT claim.

Partner, tenant, and user identities all originate in [ScaiKey](architecture#scaikey-integration). ScaiSend maintains local mirrors (the `partners`, `tenants`, `users` tables) kept in sync via webhooks and bulk sync.

## How tenant is resolved

Each request carries one credential. That credential determines the tenant:

| Credential | How tenant is resolved |
|------------|------------------------|
| API key (`sg_live_*` / `sg_test_*`) | API key row has `tenant_id`; that's the tenant for every request using this key. |
| JWT | JWT's `tenant_id` claim (may be a ScaiKey ID like `tnt_abc123`). Resolved to the local tenant record. |

If the credential doesn't resolve to a tenant, the request returns `401`. If the resolved tenant is suspended, the request returns `403`.

## Tenant-scoped objects

The following are all scoped to the active tenant. You can never see or modify another tenant's:

- API keys (`/v3/api_keys`)
- Templates (`/v3/templates`)
- Messages and events (`/v3/messages`)
- Suppressions — bounces, spam reports, unsubscribes, groups (`/v3/suppression/*`, `/v3/asm/*`)
- Statistics (`/v3/stats*`)
- Webhook endpoints and signing secrets (`/v3/user/webhooks`)
- Images (`/v3/images`)
- Sender domains (`/api/admin/domains`)
- Roles and role-to-user assignments (`/api/admin/roles`)
- Tracking settings (`/api/admin/tenants/{id}/tracking`)

## Partner-scoped visibility

Users with a partner-level role can list tenants and aggregate stats across all tenants under their partner. This is for operators and resellers, not for sending mail.

```bash
# As a partner-admin JWT
curl https://scaisend.scailabs.ai/api/admin/tenants?partner_id=prt_acme \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

Returns every tenant under `prt_acme`. A tenant-scoped JWT gets only its own tenant back from this endpoint.

## Creating tenants

Tenants are created in ScaiKey, not in ScaiSend. ScaiKey emits a `tenant.created` webhook; ScaiSend receives it at `/webhooks/scaikey` and provisions the local record (including default roles and tracking settings).

If you need to bulk-import:

```bash
scaisend sync --full
```

This walks the ScaiKey API and mirrors partners, tenants, users, and group memberships. Run it on first install and after any prolonged ScaiKey outage.

## Group-to-role mapping

ScaiKey groups can be mapped to ScaiSend roles. Every user in the group inherits the permissions of the mapped role, automatically, whenever their group membership changes.

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/group-mappings \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"group_id": "grp_engineering", "role_id": "role_developer"}'
```

Nested groups inherit. If `Backend Team` is nested inside `Engineering`, and `Engineering` maps to the `developer` role, every member of `Backend Team` gets `developer` permissions without any additional mapping.

See [Roles and Permissions](roles-and-permissions) for the roles themselves.

## What tenants share

Tenants share infrastructure but not data. In particular:

- **A shared sender domain** (flagged with `is_shared: true`) can be used as a `From:` address by any tenant. Use this for transactional "noreply" addresses across your platform.
- **Global bounce suppression.** If address `X` bounces hard for tenant A, tenant B can still send to `X` — each tenant maintains its own suppression list by default. (There's a platform-level bounce list for very large reputations; see [Suppressions](suppressions).)
- **The outbound SMTP IP pool** is shared across the ScaiSend deployment. Use `ip_pool_name` on a send to route through a specific pool if your deployment is configured with multiple pools.

## What tenants don't share

- **API keys.** A `sg_live_*` key for tenant A will never authenticate against tenant B's data.
- **Templates.** Each tenant has its own template library.
- **Suppressions.** Bounce, spam-report, and unsubscribe lists are independent per tenant.
- **Stats.** Per-tenant daily counters, per-tenant category breakdowns.
- **Webhook endpoints.** Each tenant subscribes its own endpoints.

## Suspending a tenant

Suspend in ScaiKey (not in ScaiSend). ScaiSend receives the `tenant.suspended` webhook and marks the tenant as suspended locally. From that point:

- Every API request returns `403 TENANT_SUSPENDED`.
- Queued but unsent messages stay queued — they're not dropped — and resume on unsuspend.
- Inbound bounces and FBL reports continue processing (you still want the suppression data).
- Webhook deliveries continue draining to avoid losing visibility on in-flight mail.

## What's next

- [Roles and Permissions](roles-and-permissions) — the RBAC model in detail.
- [Sender Domains](sender-domains) — adding and verifying domains.
- [Authentication](authentication) — API keys vs JWTs.
