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

Webhook events do not carry your tenant identifiers. Persist the message_id values from each send response against the tenant's profile, then resolve incoming events through that mapping. The receiver code is in Track usage per profile in webhooks; the envelope is documented in the events reference.

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, exactly as in the per-profile attribution pattern.

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 (their profile key, x-profile-id scope, or their account key) and confirm the 202 response, then confirm the delivery events for those message_id values resolve to the same tenant in your database.
  • 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