Sign-up form
Validate a Senegalese phone number with /v1/telephone/analyse and offer a cascading region, department and commune choice with /v1/geographie.
Goal: a form that accepts a valid Senegalese mobile number and a real commune. Two routes of the hub's API are enough.
Call the API from your server
The hub's API only allows the hub's origin in CORS. Your page therefore calls your server, which calls the API. Your API key also stays server-side.
The examples read the base URL from API_URL (see Get started in 5
minutes). Without a key, the daily
quota is low. With a key, add the Authorization: Bearer <key> header.
1. Validate the number
GET /v1/telephone/analyse?numero=77 123 45 67 returns:
{
"input": "77 123 45 67",
"valid": true,
"country": "SN",
"senegalese": true,
"type": "MOBILE",
"e164": "+221771234567",
"national": "77 123 45 67",
"international": "+221 77 123 45 67",
"operator": { "name": "Orange (SONATEL)", "prefix": "77", "type": "mobile", "sources": ["…"] },
"notice": "Indicatif : l'opérateur est déduit du préfixe d'origine. Avec la portabilité, un abonné peut avoir changé d'opérateur en gardant son numéro."
}Useful rules:
- Accept the number if
validandsenegaleseare true andtypeisMOBILE(orFIXED_LINE_OR_MOBILE). A landline such as33 821 00 00is rejected for an OTP. - Store
e164: one single form in your database. - Do not use
operatorto pick a payment channel. The prefix does not guarantee the current operator (number portability). The API returns thisnoticein French. - Unreadable input returns
400withcode = "INVALID_PHONE".
2. Offer a cascading commune choice
The reference goes from region to commune (ANSD, RGPH-5 2023). A commune belongs to a department, an arrondissement or a city. The region → department → commune cascade therefore sometimes needs one more call.
- Regions:
GET /v1/geographie?level=region&per_page=100(14 places). - Departments:
GET /v1/geographie?parent_id=<region id>&per_page=100. - Communes: the department's children; for each arrondissement or city, its own children.
Display name when present, otherwise name_source (the ANSD name in
capitals). Accented commune names are not verified yet. Store the id: it is
the hub's stable identifier.
Server code
// Node.js 18+ (built-in fetch)
const API = process.env.API_URL ?? 'http://localhost:8787';
const headers = process.env.API_KEY ? { Authorization: `Bearer ${process.env.API_KEY}` } : {};
async function call(path) {
const res = await fetch(`${API}${path}`, { headers });
const body = await res.json();
if (!res.ok) throw new Error(body.message ?? `Error ${res.status}`);
return body;
}
export async function checkPhone(number) {
const r = await call(`/v1/telephone/analyse?numero=${encodeURIComponent(number)}`);
const mobile = r.type === 'MOBILE' || r.type === 'FIXED_LINE_OR_MOBILE';
return { ok: r.valid && r.senegalese && mobile, e164: r.e164, display: r.national, operator: r.operator?.name ?? null, notice: r.notice };
}
const nameOf = (place) => place.name ?? place.name_source;
const children = async (parentId) => (await call(`/v1/geographie?parent_id=${parentId}&per_page=100`)).data;
export async function regions() {
return (await call('/v1/geographie?level=region&per_page=100')).data.map((p) => ({ id: p.id, name: nameOf(p) }));
}
export async function departments(regionId) {
return (await children(regionId)).map((p) => ({ id: p.id, name: nameOf(p) }));
}
// A commune belongs to a department, an arrondissement or a city.
export async function communes(departmentId) {
const list = [];
for (const place of await children(departmentId)) {
if (place.level === 'commune') list.push(place);
else list.push(...(await children(place.id)).filter((p) => p.level === 'commune'));
}
return list.map((p) => ({ id: p.id, name: nameOf(p) })).sort((a, b) => a.name.localeCompare(b.name, 'fr'));
}These functions were tested against the local API: 14 regions, 5 departments for Dakar, 19 communes for the Dakar department.
On the page
Expose these functions on your own routes (for example /api/communes). On
the page, keep native elements: an <input type="tel" autocomplete="tel">,
<select> elements with a <label>, an error message linked to the field
with aria-describedby. Validate again on submit: browser-side checks are
not enough.
The list of places rarely changes. Cache /v1/geographie responses, or
download the lieux.json file from the
Geographic reference dataset.