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 → SettingsSMS.

ProviderWhereYou need
TwilioGlobalAccount SID, Auth Token, and either a From number or a Messaging Service SID.
MSG91IndiaAuth key and a sender ID. Sender IDs and templates need DLT registration first or nothing delivers — that is between you and MSG91.
SinchGlobalService Plan ID, API token, a sender, and the region your account is in (us, eu, br, ca, au).
AakashSMSNepalAn auth token.
Anything elseAn HTTPS endpoint and a request body. See below.
Saving does not test the credentials, on purpose. Every other channel can be verified without delivering anything — SMTP has a handshake that stops before 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:

ProviderGets
Twilio, Sinch+9779812345678 — E.164 as stored.
MSG919779812345678 — no plus.
AakashSMS9812345678 — 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.

EncodingOne messageEach part of a longer one
GSM-7160 characters153
UCS-270 characters67

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"
Set "success looks like". A great many gateways answer 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

HowWhat to call
From your backendnb.users.upsert({ external_id, phone: "+977…" }) — server key.
From an SDKnb.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 fileImport 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.

CodeMeans
address_invalidNot E.164, or the gateway says it is not a mobile number. Permanent.
address_goneThey opted out. Permanent, and the number is suppressed.
credentials_invalidThe gateway rejected your key. Permanent until you fix it.
provider_throttledToo fast. Retried.
provider_downThe gateway failed or is out of credit. Retried only when it looks temporary.
payload_invalidEmpty 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

MeteringOne 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.
RateTen a second per credential. Every gateway throttles, and a shared short code throttles hard.
CostYour provider bills you. We never touch the money.
InboundNot supported. Replies go to your gateway.
TimingSend 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.
← EmailREST API →