Documentation

Everything needed to wire up contacts, send SMS under your own sender ID with automatic provider failover, connect your Hubtel Merchant Account for USSD payments, trigger SMS sends from your own backend, and auto-add contacts from another system.

00 Our services

Tunnelo SU ships four services on the same USSD and SMS infrastructure.

ServicePurpose
SMS GatewayBulk SMS sending under your own sender ID, with delivery tracking and automatic failover across up to 3 providers
USSD Menu BuilderBuild interactive USSD menus and deploy them to your shortcode
USSD Payment CollectionCollect money on a USSD menu via Hubtel's native AddToCart checkout
Contact Database SyncLive read-only sync of contacts from your own Postgres, MySQL, MongoDB, or Supabase database

01 Contacts & database connections

Add recipients manually, import a CSV, or keep a live sync from your own database.

MANUAL / CSV / EXCEL
Go to Contacts → Add contact to enter a single number, or Contacts → Import to upload a CSV or Excel file. A phone column is required — named phone, mobile, number, or msisdn — and is auto-detected. A name column (name, contact, person, first name, full name) is optional. No other custom fields are stored, and there's no merge-tag support in messages today.
GOOGLE SHEETS
In your sheet: Share → set general access to "Anyone with the link" (Viewer). Then just paste the sheet's normal URL — the one copied straight from the address bar — into Contacts → Import → import from Google Sheets and click Connect & sync. Tunnelo SU automatically converts it to the CSV export link it needs, so there's no manual link-building step (this replaces the older "Publish to web" flow, whose CSV option Google has been inconsistent about surfacing). Same column rules as CSV/Excel above. Once connected, Tunnelo SU automatically re-fetches and re-imports every 6 hours — new rows become contacts, existing ones get their name refreshed, a contact is never removed if it later disappears from the sheet. Click Sync now any time for an immediate re-check, or Disconnect to stop the automatic sync.
CONNECT YOUR DATABASE
Go to Contacts → Connect database. Supported: PostgreSQL, MySQL, MongoDB, Supabase. Enter a read-only connection (Mongo: connection string, Supabase: project URL + key) and pick a table or collection — Tunnelo SU only ever runs read queries against it, never writes. The table must have a phone/mobile column (auto-detected by name, no manual field mapping), and is capped at 10,000 rows — export a smaller or filtered subset first if yours is bigger.
// PostgreSQL connection
postgres://readonly_user:password@db.yourhost.com:5432/yourdb?sslmode=require

// MongoDB connection URI
mongodb+srv://readonly_user:password@cluster0.mongodb.net/yourdb

// Supabase (project API instead of direct Postgres)
project_url: "https://xyzcompany.supabase.co"
api_key: "YOUR_SUPABASE_KEY"
Note — use a dedicated read-only DB user, not your app's main credentials. Tunnelo SU never needs write access to your database.
Database on a private network? A private address (e.g. 192.168.x.x) is unreachable from Tunnelo SU's servers — there's no network path in. Port-forward your DB to a public address instead, and restrict that forwarding rule to accept connections only from Tunnelo SU's IP: 68.183.116.88. This keeps your database access-controlled to one source rather than open to the whole internet.
SYNC BEHAVIOR
Once connected, an automatic re-sync runs every 6 hours, or trigger Sync now manually. Matching is by phone number: a new number becomes a contact, an existing one gets its name refreshed. Sync never deletes or deactivates a contact, even if its row later disappears from your source table — and synced contacts land with no group assigned, so add them to a group afterward (see §03) or target them directly with a webhook (see §06).

02 Sender ID & sending SMS

Register the name recipients see instead of a phone number, then send.

STEP 1 — ADD YOUR SMS PROVIDER(S)
Go to Settings → SMS providers. SMS sends through your own business account on each provider you add — Hubtel, Arkesel, or Nalo, up to one of each (3 max). For every provider, enter the Sender ID already approved on that account (up to 11 characters, e.g. YOURBRAND) along with its credentials — Hubtel needs a Client ID and Client Secret; Arkesel and Nalo need only an API Key. There's no separate approval step on Tunnelo SU's side — credit and sender ID stay tied to your own provider account, never shared with other customers.
AUTOMATIC FAILOVER
With more than one provider configured, rank them using the controls next to each — the top one is tried first on every send. If that attempt fails (provider outage, rejected message, bad credentials), Tunnelo SU automatically retries the same message through the next provider in your list, live, within the same send — no manual switching, no delay waiting on a health check. This applies to every send path: immediate sends, scheduled broadcasts, webhook-triggered sends, and USSD payment-confirmation SMS. The delivery log records which provider actually delivered each message.
STEP 2 — COMPOSE & SEND
From Send SMS, choose recipients — a single number, or (under Broadcast/Schedule) one group or all contacts — write your message, and choose Send now or Schedule for a future date/time.

03 Groups & table selection

Organize contacts into reusable lists for targeted sends.

CREATE A GROUP
Go to Contacts → Groups → New group, name it, then add members by pasting numbers, uploading a CSV/Excel file, or pulling from a table in your connected database.
FROM A DATABASE TABLE
With a database connected (see §01), pick a table from the dropdown — every row with a detected phone/mobile column is pulled into the group. There's no column-mapping step or filter; if you only want a subset, prepare that subset as its own table or view first.
USING GROUPS IN A SEND
In Send SMS, switch to Broadcast or Schedule, then pick one group (or All Contacts) as the recipient.

04 Connect your payment provider (USSD)

USSD payment nodes collect money through Hubtel's own native checkout — there's no provider to pick, and no separate charge API call.

HOW IT WORKS
A payment, vote, or ticket item with a charge triggers Hubtel's own AddToCart prompt directly on the caller's phone as part of the USSD session — Tunnelo SU never makes a separate outbound call to start the charge. Money settles straight to your own Hubtel account; Tunnelo SU never holds customer payment funds.
HUBTEL MERCHANT ACCOUNT — REQUIRED FOR VERIFICATION
Go to Settings → Hubtel Merchant Account and enter your API ID, API Key, and Collection Account Number (Developer Portal → Programmable Keys → Payment Keys, and Account ID). AddToCart's own prompt isn't trusted as proof of payment on its own — Tunnelo SU uses these credentials to independently re-verify every payment via Hubtel's Transaction Status Check API before crediting it.
Note — without a Merchant Account configured, USSD payments can never be verified and are always marked failed, even if the caller actually paid. Hubtel also requires your Tunnelo SU server's IP to be whitelisted for that verification API — contact Tunnelo SU support for the IP to give your Hubtel Retail Systems Engineer if a payment gets stuck at "in progress".

05 Set up a USSD menu

Build the menu tree visually, then deploy it to your shortcode. Five node types, and — this trips people up — payment collection is built directly into two of them; it's not a step you bolt on separately.

THE FIVE NODE TYPES
Node typeWhat it does
messageShows text, then ends the session. Terminal — a caller can't go anywhere from it.
submenuNests further numbered options under it. Purely structural — no data collected.
paymentCollects a standalone charge with no vote or ticket attached — a fixed amount, or one the caller types in.
voteLists candidates/options to pick from. Optionally charges a fee per vote — set on the vote item itself, not a separate payment node.
ticketLists price tiers to pick from. Always charges — the price lives on each tier, not a separate payment node.
PAYMENT IS PART OF THE ITEM, NOT A SEPARATE STEP
There's no "add a payment node after your vote/ticket" step, and no menu item for the confirm/cancel prompt either — both are generated automatically. Configure the amount right on the vote or ticket item itself:
ItemWhere the amount livesWhat the caller sees
voteFee per vote field on the item (blank = free vote)Pick a candidate → Vote for X costs GHS5.00. 1. Confirm 0. Cancel → charged on 1
ticketPrice field on each tierPick a tier → Regular — GHS20.00. 1. Confirm 0. Cancel → charged on 1
Pressing anything other than 1 — including 0 — cancels: no vote recorded, no ticket issued, no charge attempted.
STANDALONE PAYMENT NODES
A payment item is for a charge that isn't tied to a vote or ticket. Two amount modes:
Amount typeFlow
FixedShows your Prompt text with the amount filled in, then 1. Confirm / 0. Cancel is added automatically — same as vote/ticket.
Caller enters itShows your Prompt, caller types a number (GHS1–10,000), then shows your Confirmation message as-is — the confirm/cancel prompt is not auto-added here. Write "Reply 1 to confirm" into the confirmation message yourself, or callers won't know what to press.
PLACEHOLDERS AND WHAT HAPPENS AFTER PAYMENT
Prompt, confirmation, success, and failure text on a payment item accept these placeholders:
PlaceholderResolves to
{amount}The charge amount, formatted as GHS12.50
{ref}An 8-character order reference
{description}The item's label
{phone}The caller's number
{date} / {time}When the payment completed
On success, the caller's phone also gets an SMS. If you leave the SMS template field blank, your Success message text is reused as the SMS body — same placeholders work in both. ticket items are the one exception: the confirmation SMS is always a fixed template with the event name, tier, and {ref} baked in, so there's nothing to configure there.
SUBMIT FOR REVIEW, THEN GO LIVE
Click Save, then Publish. This submits the menu to Tunnelo SU for review — it shows as pending until approved. Only one active menu can run on a given shortcode. Once approved, Tunnelo SU assigns your shortcode and the status changes to active — live for anyone dialing in.

06 Trigger an SMS from your own system

Webhooks on Tunnelo SU work in reverse from most platforms: your own backend calls us, and we send the SMS — there's no outbound event stream to subscribe to.

STEP 1 — CREATE A WEBHOOK
Go to Webhooks → New webhook. Pick who receives the message — an individual contact, a group, or all contacts — and write a message template using {key} placeholders for values you'll send at call time, e.g. {name} or {order_id}.
STEP 2 — CALL YOUR WEBHOOK URL
Tunnelo SU gives you a unique URL. From your own backend, POST a JSON body to it whenever the trigger happens — an order ships, a payment clears, whatever fits your flow. Tunnelo SU fills the template from the fields you send and dispatches the SMS.
// Your webhook URL
POST /api/inbound/<token>
Content-Type: application/json

{ "name": "Efua", "order_id": "8f2a1c" }

// Template: "Hi {name}, your order {order_id} has shipped."
// Sends:    "Hi Efua, your order 8f2a1c has shipped."
Note — the token in the URL is the only credential — there's no separate signature to verify. Keep it as secret as an API key; anyone with the URL can trigger a send on your account.

07 API keys — auto-add contacts

The opposite direction from §06: instead of Tunnelo SU sending an SMS, an external system creates a contact in Tunnelo SU — e.g. adding every new Shopify customer automatically, with no manual CSV export.

STEP 1 — CREATE A KEY
Go to Contacts → API keys → New key, name it, and copy the value shown — it's only ever displayed once. Tunnelo SU stores a one-way hash of it, not the key itself, so a lost key can't be recovered, only replaced.
STEP 2 — CALL THE ENDPOINT
Two endpoints, same auth. This key only works on these two — nothing else on your account (sending SMS, viewing data, billing, settings) is reachable with it.
// Single contact
POST /api/customer/contacts
Authorization: Bearer <your key>
{ "name": "Jane Doe", "phone": "+233241234567", "group_name": "shopify" }

// Bulk — up to 10,000 per call
POST /api/customer/contacts/bulk
Authorization: Bearer <your key>
{ "group_name": "shopify", "contacts": [{ "name": "Jane", "phone": "+233241234567" }, ...] }
phone is required on both. name is required on the single endpoint, optional on bulk. group_name is optional on both, and applies to the whole batch in bulk mode.
JAVASCRIPT EXAMPLE (NODE.JS)
Drop this into your signup handler — wherever a customer gets created on your side, call it right after.
// Keep the key server-side only — an env var, never in browser/client JS,
// since anyone who can read it could add contacts to your account.
const TUNNELO_API_KEY = process.env.TUNNELO_API_KEY;
const TUNNELO_URL = 'https://your-domain.com/api/customer/contacts';

async function addContactToTunnelo({ name, phone, groupName }) {
  const res = await fetch(TUNNELO_URL, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${TUNNELO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name, phone, group_name: groupName }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Tunnelo contact sync failed: ${err.error ?? res.status}`);
  }

  return res.json(); // { contact: { id, name, phone, network, group_name, created_at } }
}

// e.g. inside your own "customer created" event
await addContactToTunnelo({
  name: newCustomer.fullName,
  phone: newCustomer.phone,
  groupName: 'shopify',
});
SETUP BY PLATFORM
This endpoint needs a custom Authorization header, so it only works in tools that let you set headers — a native "Webhook URL" field that just takes a bare URL (like most e-commerce/form apps' own settings) can't hold this key.
PlatformWhere
ZapierAdd step "Webhooks by Zapier" → POST → URL + Headers (Authorization) + Data (body) all live in that one step
MakeAdd an "HTTP" module → "Make a request" → URL, Method POST, Headers, Body
n8n"HTTP Request" node → URL, Method POST, Header Auth, Body
HubSpotAutomation → Workflows → pick a trigger → action "Send a webhook" (Operations Hub Starter+; on lower tiers use Zapier's HubSpot trigger instead)
Your own backendStraight into the POST call wherever a customer record is created
Revoking — takes effect immediately, the very next request with a revoked key gets rejected. Keys don't expire on their own — unlike a login session, they stay valid until you revoke them.