Enterprise
Multi-tenant SMS: every client of your app texts from their own number
Your app serves many businesses. On Enterprise, each client links their own Android phone by QR code, and your app texts their customers from their number.
By NARBOWEB SRL, the team behind SMSTunnel Published 9 min read
Your app serves many businesses. An invoicing app, a booking system, a platform for schools or clinics: each of your clients has its own customers, and when your app texts those customers, the text should come from your client. Not from a number you rented that nobody recognizes, and not from your own company.
Picture an invoicing app used by a few hundred small companies. When one of them issues an invoice, its customer gets a text: “Acme Dental: invoice no. 2087 has been issued. Total: $240.” With SMSTunnel’s Enterprise plan, that text goes out from Acme Dental’s own Android phone, from Acme Dental’s own number. The customer knows who wrote, and a reply or a call back reaches Acme Dental, not you.
This guide walks through the whole flow: your app links each client’s own phone with a QR code, gets that client’s key back on its own, and hands their texts to their phone. You’ll see the code with the Node.js SDK, what your dashboard shows, a real deployment, and an honest comparison with the usual workarounds.
Why each client’s own number matters
- People know the number. A text from the business they deal with every day reads as a message from that business, because it is one.
- Replies reach the right people. When a customer answers, the reply lands on your client’s phone. You don’t end up running a support inbox for your clients’ customers.
- Each client uses their own SIM and plan. SMSTunnel charges no per-message fees, so the texts a client’s phone sends go on that client’s mobile plan, at their carrier’s rates.
- No numbers to rent or register. Your clients already have a phone and a number. Your app only needs to link them.
How the Enterprise flow works
The flow has five steps, and only one of them involves your client:
- You create an Enterprise app in your SMSTunnel dashboard and get its key. That key carries the
enterprise:saaspermission, which links clients; other keys get a 403. - Your backend starts linking a client with
activateEnterpriseClient(): the client’s name, your own id for them (externalId) and a callback URL. SMSTunnel makes the client’s account (or reuses it) and returns a QR code and the client’s own API key. - Your client scans the QR code. They install the SMSTunnel app from Google Play on their own Android phone, open Connections, tap Link with QR code and scan the code your app shows them.
- SMSTunnel calls your callback URL with a
pairing_completedevent, the token from the QR code and the client’s key. - Your app sends that client’s texts with their key. Each text goes to their phone, which sends it from their number.
getEnterpriseClient(externalId)tells you at any time whether the phone is linked and online.
Your client never copies an API key and never opens SMSTunnel’s dashboard: they install the app and scan one code.
Step by step with the Node.js SDK
The Enterprise calls are in the official SDK, @narcisbodea/smstunnel-sdk, both as a standalone Node.js client and as a NestJS module.
npm install @narcisbodea/smstunnel-sdk
1. Create the Enterprise app
On the Enterprise plan your dashboard has a Company section. Under Company → Apps, choose Add an app: one app for each product of yours that serves clients. Copy its key and keep it in an environment variable on your server, never in code that runs in a browser or a mobile app.
2. Start linking a client
When a client turns on texting in your app, your backend asks SMSTunnel for their QR code:
import { SmsTunnelNodeClient } from '@narcisbodea/smstunnel-sdk/node';
// The key of your Enterprise app (Company → Apps).
const company = new SmsTunnelNodeClient({ apiKey: process.env.SMSTUNNEL_ENTERPRISE_API_KEY! });
const activation = await company.activateEnterpriseClient({
clientName: 'Acme Dental', // shown in the app and in your dashboard
externalId: 'tenant-42', // your own id for this client
callbackUrl: 'https://invoices.example.com/smstunnel/callback',
});
// Keep these with the client's record in your own database:
const clientKey = activation.apiKey.key; // works for sending once their phone has linked
const pairingToken = activation.pairingToken; // the token the callback will carry
const qrData = activation.activation.qrData; // show this as a QR code
The client’s key is a secret like any other API key: store it on your server only.
3. Show the QR code
Render qrData with any QR code library, on the page where your client turns on texting. They scan it with the SMSTunnel app, and the app shows the client name you sent, so they can see what they are linking.
4. Handle the callback
When the phone links, SMSTunnel posts pairing_completed to your callbackUrl, which must be an https address on a public host. Accept it only when its token is one you got from step 2:
app.post('/smstunnel/callback', express.json(), async (req, res) => {
const { event, token, apiKey } = req.body;
const tenant = await db.tenants.findOne({ smstunnelPairingToken: token }); // your own database
if (event !== 'pairing_completed' || !tenant) return res.sendStatus(404);
await db.tenants.update(tenant.id, { smstunnelKey: apiKey, smsReady: true });
res.sendStatus(200);
});
5. Send texts from the client’s phone
Each client’s texts go through a client object made with that client’s key. One text goes to one number:
import { SmsTunnelNodeClient, SmsTunnelApiError } from '@narcisbodea/smstunnel-sdk/node';
async function textInvoiceIssued(tenant, invoice) {
const sms = new SmsTunnelNodeClient({ apiKey: tenant.smstunnelKey });
try {
await sms.sendSms(
invoice.customerPhone, // international format, like +14155550123
`${tenant.name}: invoice no. ${invoice.number} has been issued. Total: ${invoice.total}.`,
);
} catch (error) {
if (error instanceof SmsTunnelApiError) console.error(error.status, error.message);
else throw error;
}
}
6. Check whether a client’s phone is linked and online
const { activation } = await company.getEnterpriseClient('tenant-42');
// activation?.status: 'pending' | 'connected' | 'expired' | 'cancelled'
// activation?.isOnline: whether their phone is online right now
Show that status next to the client in your own app, so they know whether texting works before they rely on it.
With NestJS
If your backend is NestJS, the SDK’s module has an Enterprise mode: pass your Enterprise key and the current organization’s id, and SmsTunnelService does the linking and the sending for that organization.
SmsTunnelModule.forRootAsync({
useFactory: (settingsModel, tenantService) => ({
storage: new MongoSmsTunnelStorage(settingsModel),
callbackBaseUrl: process.env.CALLBACK_URL,
displayName: 'My App',
enterpriseApiKey: process.env.SMSTUNNEL_ENTERPRISE_API_KEY,
externalId: tenantService.getCurrentTenantId(),
}),
inject: [getModelToken('Settings'), TenantService],
})
await smsTunnelService.getEnterpriseClientStatus() then tells you whether that organization’s phone has linked and is online. The other storage adapters (Prisma, a JSON file) are in the SDK’s README.
What your dashboard shows
The Company section has five tabs: Overview, Clients, Phones, Apps and Invitations. Clients and phones are many-to-many: a client can link more than one phone, and one phone can serve several clients. That is how a client without a phone of their own can still send: under Phones you can share one of your own phones with your clients. Each client and each phone has its own page, with the texts it sent over the last 30 days and the texts still waiting for a phone.
You don’t need code for every client either. Under Invitations you create a QR code for a client, send it to them or show it, and they scan it in the SMSTunnel app to join your company. An app with its own key is only needed when your software links clients by itself.
A real deployment: one office in every county
One of the apps built on this flow serves a public education network with an office in each county. The app keeps a single Enterprise key in its settings. Each county office is a client with its own externalId; someone at the office scans the QR code once with the office’s phone, and from then on the app sends that office’s texts from that office’s own number. People get texts from the local office they deal with, and when they call back, they reach that office.
Things to know before you build
- One recipient per text. Each call sends one text, up to 1600 characters, to one number. For several people, make one call per person.
- Pace your sending. The API accepts 30 send requests a minute per API key on each send endpoint (all the keys of one account together get three times that), so if your server sends for many clients at once, queue the texts and retry after a 429 when the
Retry-Aftertime has passed. - Phones go offline. If a client’s phone is off, their texts wait and go out when it is back, for up to 3 days; after that they are marked failed.
- Linking again is safe. Calling
activateEnterpriseClient()again for the sameexternalIdreplaces the earlier activation and its key, while a phone that already linked stays linked. Two overlapping calls for one client can get a 409 withcode: 'CONCURRENT_ACTIVATION'or'ALREADY_CONNECTED'. - With the NestJS module, a new QR code starts a new link. Since SDK 2.2.0,
createPairingToken()clears the organization’s stored key until its phone scans the new code, so its texts stop in between. Offer a QR code only while no phone is linked (getEnterpriseClientStatus()tells you). - Your clients stay in charge of their phones. A phone’s owner can unlink it in the app or in the dashboard.
- Privacy, if you need it. Your app can encrypt each text end to end for the phone that sends it, and the Enterprise plan includes Maximum privacy, where the number is sealed too.
How this compares to other ways of doing it
Other tools that send texts from an Android phone, such as httpSMS and textbee, are built around one account and its own phones. From our comparison pages, checked in November 2026:
- httpSMS: phone keys put several phones in one account, and each key is made by hand in its dashboard. See SMSTunnel vs httpSMS.
- textbee: phones belong to one account, 1, 5 or 15 depending on the plan, with no accounts for your clients. See SMSTunnel vs textbee.
With either, the usual workaround for a multi-tenant app is to ask each client to open their own account, link their phone there, create an API key and paste it into your app. It works, but every client has to understand API keys, your support team has to explain them, and you never see your clients’ phones in one place.
SMS API providers such as Twilio solve a different problem: they send from numbers you rent from them. That suits high volumes and many countries, but the texts don’t come from your clients’ own numbers.
What it costs
The Enterprise plan is $99 a month, or $990 a year, before tax. It has no daily cap (texts are rate-limited to protect the SIMs), sign-in codes with no daily cap, separate API keys for each of your apps, Maximum privacy and dedicated support. SMSTunnel adds no per-message fees: each text uses the mobile plan of the phone that sends it. The pricing page has every plan, and this comparison of costs explains what an own SIM costs next to an SMS API.
Want to try the phone side first? Any plan, including Free, lets you link your own phone and send from your own software. When you are ready to link your clients’ phones, the Enterprise plan adds the Company section and the calls above, and if you’d like help connecting your own systems, write to us.
See also
More from the blog
Try it with your own phone
Install the Android app, sign in, and your own software can send its first text in a few minutes. The Free plan needs no credit card.