Facturiers (Biller Directory)
Le service Facturiers permet aux marchands Switag de figurer dans l'annuaire "Paiements" de l'application mobile. Contrairement aux paiements par lien (charges), les clients vous trouvent directement dans l'annuaire, remplissent un formulaire dynamique que vous avez défini, et règlent leur facture de manière autonome.
Concepts clés#
- Biller (Facturier) : Votre fiche d'identité dans l'annuaire (nom, logo, catégorie).
- Produit : Une offre payable (ex: "Facture Mensuelle", "Recharge Prépayée").
- Schéma de formulaire : Objet JSON décrivant les champs à saisir par le client.
- Lookup : Interrogation en temps réel de votre système pour récupérer un montant dû (mode
lookupuniquement). - Quote (Devis) : Verrouillage temporaire du montant entre le lookup et le paiement.
- Fulfillment : Logique post-paiement exécutée côté Switag juste après l'encaissement — activer un abonnement, recharger une carte, notifier votre ERP, etc. Disponible pour tous les modes (
fixed,user,lookup).
Parcours : devenir facturier#
Tout se fait par l'API avec vos clés pk_/sk_ (le scope billers.write est requis pour les écritures). Cinq étapes :
| # | Étape | Appel |
|---|---|---|
| 1 | Créer votre fiche (brouillon) | PUT /v1/biz/biller |
| 2 | Téléverser votre logo | POST /v1/biz/biller/logo |
| 3 | Créer vos produits | POST /v1/biz/biller/products |
| 4 | Tester lookup / fulfillment | POST /v1/biz/biller/products/{slug}/test-lookup · test-fulfillment |
| 5 | Soumettre à publication | POST /v1/biz/biller/submit |
Après soumission, l'équipe Switag examine votre fiche (curation). Suivez l'état via GET /v1/biz/biller → directory_state :
draft → in_review → published (visible dans l'app)
↘ rejected (motif dans reject_reason ; corrigez puis re-soumettez)
published ⇄ suspended (retrait temporaire par Switag)Règles une fois publié : le nom et la catégorie sont figés (anti-usurpation), la description reste modifiable, et un changement de logo repart en approbation (pending: true dans la réponse d'upload) sans toucher au logo affiché tant que Switag n'a pas validé.
1. Configurer votre profil facturier#
Avant d'être visible, vous devez définir votre identité annuaire.
/v1/biz/billerSession ou clé API{
"display_name": "CIE",
"category": "factures",
"short_desc": "Électricité post-payée"
}Upload du Logo#
Pour une meilleure expérience, uploadez un logo carré (min 128x128px, max 512Ko).
/v1/biz/biller/logoSession ou clé APICorps : multipart/form-data avec le champ file.
Soumission à curation#
Une fois votre profil et vos produits configurés, soumettez votre fiche à l'équipe Switag.
/v1/biz/biller/submitSession ou clé API2. Gérer vos produits#
Un facturier peut exposer un ou plusieurs produits.
/v1/biz/biller/productsbillers.writeModes de montant (amount_mode)#
| Mode | Usage |
|---|---|
fixed | Le client choisit parmi des forfaits (packages) définis dans le produit. |
user | Le client saisit un montant libre (borné par min/max dans le schéma). |
lookup | Switag appelle votre système pour obtenir le montant exact à payer. |
Intégration Lookup (mode lookup)#
Le lookup détermine le montant à afficher avant le paiement. Deux stratégies au choix :
Option A — Webhook externe (lookup_integration: "webhook")#
Switag appelle votre serveur HTTPS en temps réel. C'est le comportement par défaut.
{
"lookup_integration": "webhook",
"lookup_url": "https://api.example.com/switag/lookup",
"lookup_secret": "whsec_votre_secret"
}lookup_url: L'URL HTTPS que Switag appellera (obligatoire).lookup_secret: Secret partagé pour vérifier la signatureX-Switag-Signature(écriture seule — jamais renvoyé ;has_secret: trueconfirme qu'il est posé).
Option B — Fonction hébergée (lookup_integration: "code")#
Écrivez directement la logique d'appel dans le BO marchand. Le code tourne dans un sandbox Node.js ≥ 18 sur l'infrastructure Switag — aucun serveur à maintenir.
{
"lookup_integration": "code",
"lookup_code": "async function lookup(args) {\n const r = await fetch(...);\n const d = await r.json();\n return { ok: true, amount: d.amount, currency: 'XOF', label: d.label, biller_ref: d.ref };\n}"
}La fonction reçoit :
// args injectés par le sandbox
{
product: "water-bill", // slug du produit
fields: { contract_number: "0012457788" }, // saisie client
request_id: "lkp_abc123",
ts: 1730000000
}Et doit retourner le même objet qu'un endpoint webhook (voir section 4).
Seul fetch() vers des URL HTTPS est autorisé. Timeout : 10 secondes. Pas d'accès au système de fichiers ni aux modules Node.js natifs.
Intégration Fulfillment (tous les modes)#
Indépendamment du mode de montant, vous pouvez définir une fonction post-paiement exécutée par Switag juste après l'encaissement — avant même que le webhook bill.paid soit envoyé. Exemples d'usage :
- Activer un forfait data ou électrique dans votre CRM.
- Recharger une carte prépayée.
- Notifier un ERP ou un bus d'événements interne.
- Chaîner plusieurs appels API (GET état → POST activation → PUT quota).
{
"fulfillment_mode": "code",
"fulfillment_code": "async function onPaid(ctx) {\n await fetch('https://api.example.com/activate', {\n method: 'POST',\n headers: { Authorization: 'Bearer TOKEN', 'Content-Type': 'application/json' },\n body: JSON.stringify({ ref: ctx.biller_ref, tx: ctx.transaction_id })\n });\n}"
}La fonction onPaid reçoit le contexte de paiement :
{
biller: "cie",
product: "electricity-bill",
amount: 12500,
currency: "XOF",
biller_ref: "INV-2026-03-7781", // référence renvoyée par votre lookup (vide en fixed/user)
form_values: { contract_number: "0012457788" },
transaction_id: "txn_…",
charge_id: "b3f1…",
merchant_id: "9c2e…",
paid_at: 1730000000
}Mettez fulfillment_mode: "none" (défaut) pour désactiver. L'exécution est best-effort : une erreur de fulfillment ne bloque pas le paiement et n'est pas réessayée — le webhook bill.paid est envoyé quoi qu'il arrive.
Lire, mettre à jour, (dés)activer#
/v1/biz/biller/productsClé API/v1/biz/biller/products/{slug}Clé API/v1/biz/biller/products/{slug}billers.write/v1/biz/biller/products/{slug}/deactivatebillers.write/v1/biz/biller/products/{slug}/activatebillers.writeLe PUT remplace la définition payable complète (titre, mode, schéma, lookup, fulfillment). Pour retirer temporairement un produit de l'app sans renvoyer tout le payload, utilisez deactivate / activate.
Erreurs de validation#
Le schéma est validé à l'écriture (POST/PUT) — un schéma cassé n'atteint jamais l'application. Le champ code de l'erreur 400 indique la cause :
| Code | Cause |
|---|---|
title_required | Titre manquant. |
invalid_amount_mode | amount_mode ∉ fixed/user/lookup. |
invalid_schema_json | JSON absent ou malformé. |
packages_required / invalid_package | Mode fixed sans forfait, ou forfait sans label/montant > 0. |
invalid_amount_bounds | Mode user : min > max, ou preset ≤ 0. |
identify_fields_required | Mode lookup sans aucun champ d'identification. |
lookup_url_required / lookup_url_must_be_https | Mode lookup + webhook sans URL ou en HTTP clair. |
lookup_code_required | Mode lookup + lookup_integration: "code" sans code. |
fulfillment_code_required | fulfillment_mode: "code" sans code. |
field_id_label_required / duplicate_field_id | Champ sans id/label, ou id dupliqué. |
invalid_field_type | Type ∉ text/number/phone/select/amount. |
invalid_field_pattern | Regex pattern incompilable. |
select_options_required | Champ select sans options (ou option sans value/label). |
invalid_step_action | action inconnue, ou resolve hors mode lookup. |
slug_taken (409) | Un produit porte déjà ce slug. |
2bis. Tester votre lookup avant publication#
/v1/biz/biller/products/{slug}/test-lookupbillers.writeExécute le vrai appel de lookup — webhook ou fonction hébergée — avec les champs fournis et renvoie la réponse brute. Aucune quote ni paiement n'est créé.
// Requête
{ "fields": { "contract_number": "0012457788" } }
// Réponse — lookup réussi :
{ "ok": true, "amount": 12500, "currency": "XOF", "label": "Facture Mars 2026",
"customer_name": "Awa Kouassi", "biller_ref": "INV-2026-03-7781", "line_items": [...] }
// Réponse — endpoint injoignable / non-2xx (mode webhook) :
{ "ok": false, "transport_error": "biller: lookup returned 500" }
// Réponse — erreur dans le code (mode function) :
{ "ok": false, "transport_error": "sandbox: TypeError: Cannot read properties of undefined" }2ter. Tester votre fulfillment#
/v1/biz/biller/products/{slug}/test-fulfillmentbillers.writeSimule un contexte de paiement et exécute votre fonction onPaid sans déclencher de vrai encaissement. Utile pour valider l'intégration avant la mise en production.
// Requête — les champs sont optionnels, des valeurs de test sont injectées si absentes
{
"biller_ref": "INV-2026-03-7781",
"form_values": { "contract_number": "0012457788" },
"amount": 12500
}
// Réponse — fulfillment réussi :
{ "ok": true, "result": null }
// Réponse — erreur dans le code :
{ "ok": false, "error": "sandbox: fetch: 403 Forbidden from https://api.example.com/activate" }
// Réponse — sandbox indisponible (Node.js absent sur le serveur) :
{ "ok": false, "error": "sandbox: node not found — install Node.js ≥ 18" }La fonction hébergée requiert Node.js ≥ 18 sur l'infrastructure Switag. En cas d'indisponibilité temporaire, test-fulfillment retourne ok: false — le paiement réel n'est pas affecté (le webhook bill.paid est toujours envoyé).
3. Le Schéma de Formulaire#
Le schema définit l'interface utilisateur dans l'application mobile.
{
"steps": [
{
"id": "identify",
"title": "Votre contrat",
"action": "resolve",
"fields": [
{
"id": "contract_number",
"label": "Numéro de contrat",
"type": "text",
"required": true,
"pattern": "^[0-9]{8,12}$",
"placeholder": "0123456789",
"help": "8 à 12 chiffres au dos de votre facture"
}
]
}
]
}Types de champs supportés#
text: Texte libre.number: Clavier numérique.phone: Numéro de téléphone.select: Liste de choix (ajoutezoptions: [{label, value}]).amount: Champ de saisie montant (requis pour le modeuser).
4. Implémenter le Protocole Lookup (mode webhook)#
Quand un client valide un formulaire lookup et que lookup_integration vaut "webhook", Switag appelle votre serveur.
Requête de Switag#
POST {lookup_url} Signature : X-Switag-Signature: t=<unix>,v1=<hmac>
{
"product": "water-bill",
"fields": { "contract_number": "0012457788" },
"request_id": "lkp_abc123",
"ts": 1730000000
}Réponse attendue (200 OK)#
{
"ok": true,
"amount": 12500,
"currency": "XOF",
"label": "Facture Mars 2026",
"customer_name": "Awa Kouassi",
"biller_ref": "INV-2026-03-7781",
"line_items": [
{ "label": "Consommation", "amount": 11000 },
{ "label": "Taxes", "amount": 1500 }
]
}En cas d'erreur (contrat inconnu, service indisponible) :
{ "ok": false, "reason": "Contrat introuvable" }5. Webhook de Paiement#
Après le succès du paiement (PIN saisi par le client), vous recevez un webhook et/ou votre fonction onPaid est exécutée (si fulfillment_mode: "code").
Événement bill.paid#
{
"type": "bill.paid",
"data": {
"biller": "cie",
"product": "water-bill",
"amount": 12500,
"currency": "XOF",
"biller_ref": "INV-2026-03-7781",
"form_values": { "contract_number": "0012457788" },
"charge_id": "b3f1…",
"transaction_id": "b3f1…",
"merchant_id": "9c2e…"
}
}Détails :
biller= le slug de votre fiche ;product= le slug du produit.form_values= les champs saisis par le client ;biller_ref= la référence que votre lookup a renvoyée (modelookup), vide enfixed/user.- Livraison best-effort (pas de réessai) : réconciliez via votre liste de paiements et dédupliquez sur
transaction_id. - Signature : même schéma
X-Switag-Signature: t=<unix>,v1=<hmac>que vos autres webhooks (voir Webhooks).
Webhook vs Fulfillment#
Webhook bill.paid | Fonction onPaid | |
|---|---|---|
| Modes | Tous | Tous |
| Exécution | Après paiement, livraison async | Juste après paiement, dans le sandbox Switag |
| Retry | Non | Non (best-effort) |
| Logique multi-appels | Dans votre backend | Directement dans le code |
| Infra à maintenir | Votre serveur HTTPS | Aucune |
| Recommandé si… | Vous avez déjà un backend | Vous voulez zéro infrastructure |
Les deux peuvent coexister : le webhook bill.paid est toujours envoyé, même si fulfillment_mode: "code" est actif.
Exemples complets#
Facturier mode fixed avec fulfillment (recharge forfait)#
// fulfillment_code
async function onPaid(ctx) {
// ctx.form_values.phone = numéro saisi, ctx.amount = montant payé
const r = await fetch("https://api.operator.com/recharge", {
method: "POST",
headers: {
Authorization: "Bearer sk_live_…",
"Content-Type": "application/json"
},
body: JSON.stringify({
msisdn: ctx.form_values.phone,
amount: ctx.amount,
ref: ctx.transaction_id
})
});
if (!r.ok) throw new Error(`Recharge failed: ${r.status}`);
}Facturier mode lookup avec fonction hébergée#
// lookup_code
async function lookup(args) {
const r = await fetch(`https://api.cie.ci/contracts/${args.fields.contract_number}/balance`, {
headers: { Authorization: "Bearer sk_live_…" }
});
if (!r.ok) return { ok: false, reason: "Contrat introuvable" };
const d = await r.json();
return {
ok: true,
amount: d.amount_due,
currency: "XOF",
label: `Facture ${d.month}`,
customer_name: d.owner_name,
biller_ref: d.invoice_id,
line_items: d.lines.map(l => ({ label: l.description, amount: l.amount }))
};
}Facturier mode lookup avec lookup webhook + fulfillment#
Webhook lookup → votre serveur calcule le montant → paiement → onPaid active le service.
// fulfillment_code
async function onPaid(ctx) {
// ctx.biller_ref = invoice_id renvoyé par votre lookup webhook
await fetch("https://erp.example.com/invoices/" + ctx.biller_ref + "/mark-paid", {
method: "PATCH",
headers: { Authorization: "Bearer …", "Content-Type": "application/json" },
body: JSON.stringify({ transaction_id: ctx.transaction_id, amount: ctx.amount })
});
}Bonnes pratiques#
- Idempotence : Fiez-vous au
transaction_iddu webhook pour éviter les doubles crédits. - Délai : Répondez au lookup (webhook ou code) en moins de 10 secondes.
- Sécurité : Utilisez un
lookup_secretrobuste et différent de votre secret webhook. - Validation : Utilisez les regex
patterndans vos schémas pour réduire les erreurs de saisie client. - Test avant publication : Utilisez
test-lookupettest-fulfillmentdepuis le BO marchand (Studio produits) ou l'API pour valider vos intégrations sans créer de vrai paiement.