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
IsolationPer resource: contacts, templates, TCR registration, and WhatsApp Business Account are inherited or dedicated per profileNone (every tenant shares all resources)Complete (nothing shared)
Tenant provisioningAPI: POST /v3/profiles + completionNone neededManual: each tenant signs up for their own account, and the API has no account-creation endpoint
BillingOrganization, per profile, or profile with organization fallback (billing_model)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 poolOwn pool per profile key; requests scoped via x-profile-id draw from the organization's poolOne pool shared by all tenantsOwn pool per account

To pick a model:

  • If you onboard tenants programmatically, or tenants need isolated contact lists, their own templates, their own US A2P (TCR) registration, or their own WhatsApp presence, use Sender Profiles.
  • 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, their own 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 flag, status, and failure mode is Create and activate sub-account profiles via the API; the sequence below is the architecture-level view.

Provision a profile per tenant

Create the profile with the inheritance and billing flags that encode your isolation policy, then run the asynchronous completion step:

curl -X POST "https://api.sent.dm/v3/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",
    "inherit_contacts": false,
    "inherit_templates": false,
    "billing_model": "organization"
  }'

inherit_contacts: false and inherit_templates: false give the tenant isolated data; billing_model: "organization" keeps charges on your platform's bill (use "profile" to bill the tenant directly). Then call POST /v3/profiles/{profileId}/complete with a webHookUrl; Sent calls it back with COMPLETED (ready to send) or SUBMITTED (pending registration or channel setup) when background provisioning finishes. Refer to the provisioning guide for the flag tables, the completion prerequisites, and troubleshooting.

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 is its own account, so each key gets its own 200 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: a profile can keep dedicated contacts while inheriting your organization's template library. See the governance model for what each flag controls.

Alternative: one shared account

If all tenants message under your platform's own 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 200 requests-per-minute pool, so one tenant's campaign can rate-limit everyone. Enforce per-tenant quotas in your own app and pace bulk sends. 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, their own billing, their own compliance standing, and no shared organization), give each tenant their own 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 billing_model: "profile" and dedicated resources 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 own and inherited templates, never tenant B's dedicated resources.
  • 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