FlexApp Docs

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 lookup uniquement).
  • 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 :

#ÉtapeAppel
1Créer votre fiche (brouillon)PUT /v1/biz/biller
2Téléverser votre logoPOST /v1/biz/biller/logo
3Créer vos produitsPOST /v1/biz/biller/products
4Tester lookup / fulfillmentPOST /v1/biz/biller/products/{slug}/test-lookup · test-fulfillment
5Soumettre à publicationPOST /v1/biz/biller/submit

Après soumission, l'équipe Switag examine votre fiche (curation). Suivez l'état via GET /v1/biz/billerdirectory_state :

Code
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.

PUT/v1/biz/billerSession ou clé API
JSON
{
  "display_name": "CIE",
  "category": "factures",
  "short_desc": "Électricité post-payée"
}

Pour une meilleure expérience, uploadez un logo carré (min 128x128px, max 512Ko).

POST/v1/biz/biller/logoSession ou clé API

Corps : multipart/form-data avec le champ file.

Soumission à curation#

Une fois votre profil et vos produits configurés, soumettez votre fiche à l'équipe Switag.

POST/v1/biz/biller/submitSession ou clé API

2. Gérer vos produits#

Un facturier peut exposer un ou plusieurs produits.

POST/v1/biz/biller/productsbillers.write

Modes de montant (amount_mode)#

ModeUsage
fixedLe client choisit parmi des forfaits (packages) définis dans le produit.
userLe client saisit un montant libre (borné par min/max dans le schéma).
lookupSwitag 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.

JSON
{
  "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 signature X-Switag-Signature (écriture seule — jamais renvoyé ; has_secret: true confirme 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.

JSON
{
  "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 :

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

Contraintes sandbox

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).
JSON
{
  "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 :

JavaScript
{
  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#

GET/v1/biz/biller/productsClé API
GET/v1/biz/biller/products/{slug}Clé API
PUT/v1/biz/biller/products/{slug}billers.write
POST/v1/biz/biller/products/{slug}/deactivatebillers.write
POST/v1/biz/biller/products/{slug}/activatebillers.write

Le 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 :

CodeCause
title_requiredTitre manquant.
invalid_amount_modeamount_mode ∉ fixed/user/lookup.
invalid_schema_jsonJSON absent ou malformé.
packages_required / invalid_packageMode fixed sans forfait, ou forfait sans label/montant > 0.
invalid_amount_boundsMode user : min > max, ou preset ≤ 0.
identify_fields_requiredMode lookup sans aucun champ d'identification.
lookup_url_required / lookup_url_must_be_httpsMode lookup + webhook sans URL ou en HTTP clair.
lookup_code_requiredMode lookup + lookup_integration: "code" sans code.
fulfillment_code_requiredfulfillment_mode: "code" sans code.
field_id_label_required / duplicate_field_idChamp sans id/label, ou id dupliqué.
invalid_field_typeType ∉ text/number/phone/select/amount.
invalid_field_patternRegex pattern incompilable.
select_options_requiredChamp select sans options (ou option sans value/label).
invalid_step_actionaction inconnue, ou resolve hors mode lookup.
slug_taken (409)Un produit porte déjà ce slug.

2bis. Tester votre lookup avant publication#

POST/v1/biz/biller/products/{slug}/test-lookupbillers.write

Exé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éé.

JSON
// 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#

POST/v1/biz/biller/products/{slug}/test-fulfillmentbillers.write

Simule 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.

JSON
// 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" }
Disponibilité sandbox

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.

JSON
{
  "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 (ajoutez options: [{label, value}]).
  • amount : Champ de saisie montant (requis pour le mode user).

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>

JSON
{
  "product": "water-bill",
  "fields": { "contract_number": "0012457788" },
  "request_id": "lkp_abc123",
  "ts": 1730000000
}

Réponse attendue (200 OK)#

JSON
{
  "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) :

JSON
{ "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#

JSON
{
  "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 (mode lookup), vide en fixed/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.paidFonction onPaid
ModesTousTous
ExécutionAprès paiement, livraison asyncJuste après paiement, dans le sandbox Switag
RetryNonNon (best-effort)
Logique multi-appelsDans votre backendDirectement dans le code
Infra à maintenirVotre serveur HTTPSAucune
Recommandé si…Vous avez déjà un backendVous 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)#

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

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

JavaScript
// 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_id du 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_secret robuste et différent de votre secret webhook.
  • Validation : Utilisez les regex pattern dans vos schémas pour réduire les erreurs de saisie client.
  • Test avant publication : Utilisez test-lookup et test-fulfillment depuis le BO marchand (Studio produits) ou l'API pour valider vos intégrations sans créer de vrai paiement.