Back to Blog
Developer ResourcesFebruary 17, 2026Updated October 2, 20269 min read

Add Phone Numbers to Your SaaS: A Multi-Tenant Guide

How to give each customer of your SaaS their own phone number with AgentCall: one account, one labeled number per tenant, and webhook routing by number.

Short answer: run one AgentCall account for your product, provision one labeled number per tenant, and route every webhook to the right tenant by the number it arrived on. AgentCall does not have a sub-account or tenant API today, so tenant mapping and per-tenant billing live in your database. This guide shows the pattern and the limits you should plan around.

Your Customers Want Phone Features

If you build a CRM, helpdesk, marketing platform, or any SaaS that touches customer communication, your users will eventually ask: “Can I text customers from the app?” or “Can I get a dedicated phone number for my business?”

Building this on raw carrier APIs means months of work: number provisioning, inbound routing, opt-out handling, delivery tracking. With AgentCall you skip the carrier layer and keep the tenant logic, which is the part that is specific to your product anyway.

The Multi-Tenant Phone Problem

  • Number isolation: Tenant A's customers should never see Tenant B's messages. Each tenant needs their own number.
  • Webhook routing: When an SMS arrives, you need to know which tenant it belongs to.
  • Usage tracking: If you bill tenants for phone use, you need per-tenant counts.
  • Provisioning at scale: New tenants should get a number during onboarding, not through manual setup.

The Pattern: One Account, One Number per Tenant

Your AgentCall account owns every number. Each number gets a label that carries your tenant ID, and you store the returned number ID next to the tenant record. One webhook on the account receives events for all numbers, and each event includes the number it concerns (to for inbound texts and calls), so routing is a lookup.

Provision on Tenant Signup

import AgentCall from 'agentcall';

const client = new AgentCall(process.env.AGENTCALL_API_KEY);

async function onboardTenant(tenantId: string, areaCode?: string) {
  // Provision a local number labeled with the tenant
  const number = await client.numbers.provision({
    country: 'US',
    type: 'local',
    label: `tenant-${tenantId}`,
    areaCode, // optional, e.g. '314'
  });

  // Save the mapping. This table is your source of truth for routing.
  await db.tenant.update({
    where: { id: tenantId },
    data: { phoneNumber: number.number, phoneNumberId: number.id },
  });

  return number;
}

// One-time setup: a single webhook for the whole account
await client.webhooks.create({
  url: 'https://my-saas.com/api/phone-events',
  events: ['sms.inbound', 'call.inbound', 'call.status'],
});

Provisioning is rate limited (10 numbers per hour and 100 per 24 hours per account), so queue tenant onboarding rather than provisioning in a burst. If a provisioning call fails ambiguously, list your numbers before retrying so you never buy a duplicate.

Send SMS on Behalf of a Tenant

async function sendTenantSMS(tenantId: string, to: string, message: string) {
  const tenant = await db.tenant.findUnique({ where: { id: tenantId } });

  return client.sms.send({
    from: tenant.phoneNumber,
    to,
    body: message,
  });
}

Route Inbound Messages to the Right Tenant

// POST https://my-saas.com/api/phone-events
// Body: { event, timestamp, data }, signed with X-AgentCall-Signature
app.post('/api/phone-events', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'sms.inbound') {
    // data.to is the tenant's number
    const tenant = await db.tenant.findFirst({
      where: { phoneNumber: data.to },
    });

    if (tenant) {
      await db.message.create({
        data: {
          tenantId: tenant.id,
          from: data.from,
          body: data.body,
          direction: 'inbound',
        },
      });
      await pusher.trigger(`tenant-${tenant.id}`, 'new-message', data);
    }
  }

  res.json({ received: true });
});

Verify the signature on every delivery before you trust the payload.

Per-Tenant Usage and Billing

client.usage.get() returns usage and cost for your whole account, broken down by numbers, SMS, calls, AI voice, and recording. It does not split by tenant. To bill a tenant, count their events yourself: every sms.inbound and call.status webhook already carries the number, and client.calls.list() and the message inbox give you the history to reconcile against.

// Account-level usage for a month. Split by tenant in your own database.
const usage = await client.usage.get('2026-10');
// usage.breakdown has numbers, sms, calls, voiceAi, and recording sections.

Current rates for your pricing model: Pro is $19.99/month plus number rental and usage: local/mobile numbers $2/month each, toll-free $2.50/month, outbound SMS $0.015/message, standard outbound voice $0.035/min, managed AI voice $0.40/min, BYOK voice $0.10/min plus your AI provider's charges, or Premium Voice $0.59/min. AI calls round up to whole minutes per call. New-account destination/business verification can still apply after upgrading.

Know the Limits Before You Build

  • No tenant or sub-account API. Isolation between tenants is enforced by your routing code and your database, not by AgentCall. Never accept a tenant-supplied number ID without checking that it belongs to that tenant.
  • One account per person or organization. The terms do not allow creating many accounts to get around trials or limits, and they do not allow reselling access without authorization. If your product resells phone service to your customers, email support@agentcall.co before you build so we can agree on a setup.
  • Texting rules still apply. New accounts can only text verified destinations until they are verified, STOP opt-outs are honored on every send, and your tenants are responsible for consent to message their own customers.
  • Pro for production. New accounts receive a one-time 72-hour live trial with one US/Canada local number, 300 managed inbound AI seconds (five minutes) total, and five SMS send attempts to verified destinations. Card verification is required before number allocation; the clock starts when allocation succeeds. There is no monthly reset or automatic paid subscription. Live OTP extraction and outbound calls require Pro.

How This Compares to Building on Twilio Directly

Twilio offers subaccounts, which map naturally to tenants and give you separate usage per tenant out of the box. If strict per-tenant accounts and billing are a hard requirement, that is a real advantage and worth weighing. AgentCall is the lighter choice when you want a number, an inbox, webhooks, caller memory, and an AI receptionist per tenant without registering and operating your own carrier setup, and you are comfortable keeping the tenant mapping in your own database.

Use Cases

CRM with SMS

Give each sales team their own phone number. Reps send texts from the CRM, replies come back to the team's inbox, and your routing table keeps each team's messages separate.

Helpdesk with Phone Support

Each support team gets a dedicated number. Turn on an inbound AI receptionist per number, with its own system prompt per tenant, and let it take messages after hours.

Marketing Platform with SMS Campaigns

Each business gets a number for SMS conversations. Replies route back by number, and STOP handling is done for you per number. Check the current messaging requirements for campaign volume before launch.

Related Reading


FAQ

How many tenants can I support?

Pro has no cap on the number of phone numbers, at $2/month each, so the practical limits are the provisioning rate limit and your own routing and billing code. Because tenants share one account and one webhook, plan for a tenant lookup table that scales with your user base.

Can tenants have multiple phone numbers?

Yes. Provision as many numbers as a tenant needs and label each one, for example tenant-42-sales and tenant-42-support. A tenant might use one number for an inbound AI receptionist and another for outbound texts.

How do I handle tenant offboarding?

When a tenant leaves, release their numbers with client.numbers.release(numberId) and delete the mapping row. A released number can be bought by someone else, so do not promise a tenant they can get it back. Number rental is billed monthly, so check your invoice for how a mid-month release is prorated.

Ready to get started?

Give your AI agents their own phone numbers in minutes.

Start Building