SMS
Text messages through the gateway you already pay for. Addressed to a person, not a device — like email, and unlike push.
Notibase does not resell text messages. You connect your own Twilio, MSG91, Sinch or AakashSMS account — or any HTTP gateway at all — and the sender registration, the number and the per-message price stay yours. One text to one person is one send on your plan, exactly like one push to one device.
Connect a gateway
Console → your app → Settings → SMS.
| Provider | Where | You need |
|---|---|---|
| Twilio | Global | Account SID, Auth Token, and either a From number or a Messaging Service SID. |
| MSG91 | India | Auth key and a sender ID. Sender IDs and templates need DLT registration first or nothing delivers — that is between you and MSG91. |
| Sinch | Global | Service Plan ID, API token, a sender, and the region your account is in (us, eu, br, ca, au). |
| AakashSMS | Nepal | An auth token. |
| Anything else | — | An HTTPS endpoint and a request body. See below. |
MAIL FROM, APNs and FCM have dry runs. SMS has none: the only way to know
a gateway works is to pay for a message. So saving checks the shape, and a separate
Send a test button sends one real message to a number you type.Numbers are E.164 or nothing
Store +9779812345678, not 9812345678. A national number is
ambiguous without a country, and the failure is not a bounce — it is a message delivered to
a stranger in another country. A number that is not E.164 is refused at send time with
address_invalid and shows in the delivery log as refused rather than vanishing.
The SMS page counts how many of your people have unusable numbers, so you can find out before a campaign rather than during one.
What we send each provider is not always what you stored, because they disagree:
| Provider | Gets |
|---|---|
| Twilio, Sinch | +9779812345678 — E.164 as stored. |
| MSG91 | 9779812345678 — no plus. |
| AakashSMS | 9812345678 — the ten-digit national number; they serve Nepal only. |
Send one
An sms block on any send. On its own it is an SMS-only campaign; beside a
title it is one message on two channels.
await nb.messages.send({
audience: { segment_id: "..." },
content: {
sms: {
text: "Your code is {{code}}. It expires in 10 minutes.",
transactional: true
}
}
});
Attributes substitute the same way they do everywhere else, and an unknown one renders as
nothing rather than as {{name}}.
What a message costs
One "SMS" is one to ten billable messages, and the trap is invisible. The basic SMS alphabet (GSM 03.38) fits 160 characters in one message. A single character outside it — an emoji, or the curly apostrophe a word processor puts in don’t — switches the whole message to a 16-bit encoding where the limit is 70. The same text becomes three messages and you are billed for three.
| Encoding | One message | Each part of a longer one |
|---|---|---|
| GSM-7 | 160 characters | 153 |
| UCS-2 | 70 characters | 67 |
The composer counts as you type, names the exact character that forced UCS-2, and offers to swap decorative punctuation — curly quotes, en dashes, ellipses — for plain equivalents. It will never touch an accented letter: cafe is not café, and rewriting somebody's copy to save money is not ours to do. Anything over ten segments is refused, because at that point it is almost always a paste rather than a message.
The count and encoding are recorded on every delivery, so the number is answerable after the fact too. To be clear about who charges what: three segments is three messages on your provider’s bill and one send on your Notibase plan.
Opting out
Notibase does not append "reply STOP". The carrier and your gateway already handle STOP, HELP and the rest before we hear about it, and our own version would cost eleven characters of every message to do nothing new.
When your provider tells us a number has opted out, that comes back as
address_gone and the number is suppressed for that app — the same way a hard
bounce suppresses an email address. You can also opt somebody out yourself:
// server key
await fetch("https://api.notibase.com/v1/unsubscribe", {
method: "POST",
headers: { authorization: `Bearer ${SERVER_KEY}`, "content-type": "application/json" },
body: JSON.stringify({ phone: "+9779812345678", reason: "user_request" })
});
// or by your own user id, which covers their number AND their email at once
// { external_id: "u_123" }
Any other gateway
Pick Anything else and describe the request. {{to}},
{{text}} and {{sender}} are substituted and escaped, so a message
containing a quote cannot break the body you wrote.
URL https://gateway.example.com/send
Body {"to":"{{to}}","message":"{{text}}","from":"{{sender}}"}
Headers Authorization: Bearer …
Success "status":"queued"
HTTP 200 to say they refused a message. Without a string we can
look for in the response, every rejection is recorded as a delivery and you find out when
somebody says they never got the code.The URL must be HTTPS and must not resolve to a private address — a gateway on
169.254.169.254 or 10.0.0.5 is refused, because otherwise this
feature is a way to make our servers fetch things inside our own network.
Getting numbers in
| How | What to call |
|---|---|
| From your backend | nb.users.upsert({ external_id, phone: "+977…" }) — server key. |
| From an SDK | nb.identify("u_123", { attributes: { phone: "+977…" } }). Turn on identity verification if clients set numbers — otherwise an extracted client key lets somebody write their number onto your user and receive that person’s codes. |
| From a file | Import a CSV with a phone column. Numbers without a country code import, and are counted as unreachable rather than silently dropped. |
Delivery, and what "sent" means
sent means your gateway accepted the message. Whether a handset rang is
between your provider and the carrier, and no platform can honestly tell you otherwise —
so we do not pretend to. The raw response from your gateway is kept per message, which is
usually the whole diagnosis.
| Code | Means |
|---|---|
address_invalid | Not E.164, or the gateway says it is not a mobile number. Permanent. |
address_gone | They opted out. Permanent, and the number is suppressed. |
credentials_invalid | The gateway rejected your key. Permanent until you fix it. |
provider_throttled | Too fast. Retried. |
provider_down | The gateway failed or is out of credit. Retried only when it looks temporary. |
payload_invalid | Empty message, or over ten segments. |
Webhooks carry SMS like any other channel:
"channel": "sms", a null device_id, and the recipient's
address.
Limits and plan
| Metering | One text to one person is one send out of monthly_messages, however many segments it takes — exactly like one push to one device. Your gateway bills you per segment; we do not. The composer shows the segment count because it decides their invoice, not ours. |
| Rate | Ten a second per credential. Every gateway throttles, and a shared short code throttles hard. |
| Cost | Your provider bills you. We never touch the money. |
| Inbound | Not supported. Replies go to your gateway. |
| Timing | Send now, at a time you pick, or at the same local hour everywhere — see Scheduling & time zones. A text arriving at 3 AM is worse than one arriving a day late. |