Skip to content

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:

JSON
{
  "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 valid and senegalese are true and type is MOBILE (or FIXED_LINE_OR_MOBILE). A landline such as 33 821 00 00 is rejected for an OTP.
  • Store e164: one single form in your database.
  • Do not use operator to pick a payment channel. The prefix does not guarantee the current operator (number portability). The API returns this notice in French.
  • Unreadable input returns 400 with code = "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.

  1. Regions: GET /v1/geographie?level=region&per_page=100 (14 places).
  2. Departments: GET /v1/geographie?parent_id=<region id>&per_page=100.
  3. 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

JavaScript
// 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.

What next?