Multi-tenant architectures on Sent: profiles vs. accounts

This guide shows you how to choose and set up a tenant model for a platform or ISV that sends messages on behalf of multiple customers. Sent's primary multi-tenancy mechanism is Sender Profiles: isolated messaging identities inside one organization, provisioned through the API. A single shared account and fully separate accounts remain workable alternatives at the two ends of the isolation spectrum, and both are covered below with their constraints.

It assumes you can already send messages. The profile-based setup additionally requires an organization account (GET /v3/me returns "type": "organization") and an API key whose user has the admin role. See roles and permissions.

Compare the three tenant models

Sender ProfilesOne shared accountSeparate accounts
IsolationContacts and templates are always dedicated per profile; the identity, campaign, and billing are inherited or dedicatedNone (every tenant shares all resources)Complete (nothing shared)
Tenant provisioningAPI: POST /v3/sender-profiles, one callNone neededManual: each tenant signs up for their own account, and the API has no account-creation endpoint
BillingOrganization, dedicated, or dedicated with organization fallback (billing)One bill to youEach tenant pays Sent directly
CredentialsPer-profile API key, or one organization key scoped with x-profile-idOne API keyOne API key per tenant, managed by you
Rate limit poolA separate pool per profile key; requests scoped via x-profile-id draw from the organization's poolOne pool shared by all tenantsA separate pool per account

To pick a model:

  • If you onboard tenants programmatically, or tenants need a dedicated US A2P (TCR) registration, a dedicated sender, or a WhatsApp presence of their own, use Sender Profiles. Isolated contact lists and templates come with the model rather than being a reason to choose it.
  • If every tenant sends the same kind of content under your platform's single brand and sender identity, a shared account is enough, provided you accept the shared rate limit pool and build tenant attribution yourself.
  • If tenants must own their Sent relationship end to end (their own login and billing relationship, no shared organization), use separate accounts and treat each one as an independent integration.

Build on Sender Profiles

This is the recommended model: one organization, one profile per tenant. The full provisioning walkthrough with every field, status, and failure mode is Create and activate Sender Profiles via the API; the sequence below is the architecture-level view.

Provision a profile per tenant

One call. name and short_name are the only required fields; billing says who pays, and a channels.sms block says whether the tenant gets a dedicated sender:

curl -X POST "https://api.sent.dm/v3/sender-profiles" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "Idempotency-Key: create-profile-tenant-42" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Tenant 42 Coffee Co",
    "short_name": "T42COFFEE",
    "description": "Sender profile for tenant 42",
    "billing": { "inherit": true }
  }'

billing: {"inherit": true} keeps charges on your platform's bill; omit the block to bill the tenant directly. Contacts and templates stay isolated to the tenant either way — there is nothing to configure and no way for one tenant to see another's.

There is no completion step and no callback to receive: the profile is usable as soon as it is created, inheriting your identity and terminating through the shared routes for its country. Give it a dedicated sender, and register its compliance, with POST /v3/channels/sms when the tenant needs one. The response carries the profile's api_key, shown once and never returned again.

Route each tenant's traffic

Two options, usable side by side:

  • Per-profile API keys: store one key per tenant and instantiate the client with it. Each profile key gets a separate 10,000 requests-per-minute rate limit pool: one tenant's burst cannot starve another's.
  • One organization key + x-profile-id: keep a single credential and scope each request to a profile by UUID. All scoped requests draw from the organization's rate limit pool.
curl -X POST "https://api.sent.dm/v3/messages" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -H "Content-Type: application/json" \
  -d '{"to": ["+15551234567"], "template": {"id": "tmpl_123"}}'

Only organization keys may send x-profile-id; profile keys are rejected with 403, and a profile outside your organization returns 404. Per-language client code for both patterns is in Integrating Sender Profiles into Your Application.

Attribute webhook events to tenants

Select events under sender_profile on your organization's webhook, and Sent clones it onto every profile, including profiles you create later, so every tenant's events arrive at one endpoint. Each event names its profile in payload.account_id, so map that ID to the tenant. The receiver code is in Track usage per profile in webhooks, and the clones are documented in Sender Profile Events.

Isolation is per resource, not all-or-nothing: contacts, templates, and the sender are always dedicated to the profile, while the brand, the campaign, and billing can be inherited from your organization. See the governance model for what each resource does.

Alternative: one shared account

If all tenants message under your platform's brand, with the same sender identity, the same templates, and no per-tenant compliance registration, you can run everything through a single account.

The send request has no tenant or metadata field, so tenant attribution is entirely your app's job. Record the mapping when you send, using the message_id values the API returns:

const response = await client.messages.send({
  to: recipients,
  template: { id: templateId }
});

// The request body carries no tenant identifier — persist the mapping yourself
await db.messages.insertMany(
  response.data.recipients.map((r) => ({
    sentMessageId: r.message_id,
    to: r.to,
    tenantId: tenant.id
  }))
);

Webhook handlers then look up the tenant by payload.message_id. Every tenant shares your account, so payload.account_id is the same for all of them and can't tell them apart. That field is what the per-profile attribution pattern keys on instead.

Constraints to plan for:

  • All tenants share one 10,000 requests-per-minute pool. The ceiling is high enough that a single tenant is unlikely to starve the others, but one runaway tenant can still consume it. Enforce per-tenant quotas in your own app. See How to handle Sent API rate limits.
  • Every tenant sends from the same numbers, templates, and TCR registration; a compliance problem caused by one tenant affects all of them.
  • If you later need isolation, you can move a tenant onto a Sender Profile without leaving your organization. That migration path is the main argument for starting with profiles even when sharing would work today.

Alternative: separate accounts per tenant

If each tenant must be Sent's customer of record (their own login, billing, and compliance standing, and no shared organization), give each tenant a separate account. There is no API for creating accounts, so each tenant signs up through the dashboard themselves; your platform stores their API key and treats each account as an independent integration:

// One secrets-manager entry per tenant; a leaked key exposes one tenant, not all
const client = new SentDm({
  apiKey: await secrets.get(`sent-api-key-${tenant.id}`)
});

Each account has its own rate limit pool and its own bill. The costs are operational: onboarding cannot be automated, nothing (templates, contacts, registration) can be shared, and there is no organization-level view across tenants. Before choosing this model, check whether a Sender Profile with dedicated billing and a dedicated sender already gives the tenant what they actually need (direct billing and full data isolation) without giving up API provisioning.

Verify your setup

  • Send a test message as one tenant and confirm the 202 response, then confirm its delivery events resolve to that tenant. With their profile key or x-profile-id scope, payload.account_id is the tenant's profile. With an account key, look the message_id values up in the mapping you stored when sending.
  • For profile-based setups, list templates with tenant A's key and confirm you see only that profile's templates, never tenant B's.
  • For shared-account setups, confirm your per-tenant quota enforcement triggers before the account-wide limit does: a tenant at its quota should be throttled by your app, not by a 429 that affects every tenant.

On this page