Documentation développeur

Encaissez et versez par Mobile Money depuis vos serveurs

Une API REST en JSON, des écritures signées en HMAC-SHA256, une idempotence native et des notifications de statut signées. Lisez cette page en entier avant d'écrire votre première requête.

URL de base

https://tamoapi.tasa.pro/api/v1

Format

JSON UTF-8

Montants

Entiers, de 10 à 500 000

Limites

120 lectures et 30 écritures par minute

Démarrage

Trois façons de vous authentifier

Les lectures portent un jeton. Toute écriture (POST, PUT, PATCH, DELETE) est en plus signée avec votre api_secret.

Token API permanent

X-API-Token: tok_…

Recommandé pour l'intégration de serveur à serveur. Généré dans le dashboard, affiché une seule fois avec l'api_secret.

Token d'accès

Authorization: Bearer tat_…

Valable 60 minutes, de portée all, collection ou distribution. Obtenu avec Basic base64(merchant_code:api_secret). Les écritures restent signées.

Signature v2

X-Signature-Version: 2

Obligatoire sur chaque écriture, avec X-Timestamp, X-Nonce et Idempotency-Key. La clé est api_secret, jamais api_token.

  1. 01

    Créez un compte

    Inscription en ligne, puis validation par l'équipe TASA.

  2. 02

    Générez vos clés

    Dashboard, Clés API. Notez api_token et api_secret : ils ne s'affichent qu'une fois.

  3. 03

    Déclarez votre IP

    Obligatoire pour verser. Déclarez l'adresse publique sortante de vos serveurs.

  4. 04

    Renseignez l'URL de rappel

    Elle reçoit les changements de statut. Utilisez https://.

Signature des requêtes

Signez les octets exacts que vous envoyez

X-Signature = hex( HMAC-SHA256( METHOD + "\n" + URI + "\n" + X-Timestamp + "\n" + X-Nonce + "\n" + Idempotency-Key + "\n" + corps_brut, api_secret ) )

  • URI : chemin complet et chaîne de requête, sans le domaine (/api/v1/collections).
  • corps_brut : sérialisez une fois, signez, envoyez la même chaîne.
  • Idempotency-Key : la valeur envoyée, ou une ligne vide si l'en-tête est absent.
  • X-Timestamp : secondes Unix, tolérance de 300 s. X-Nonce : unique, 16 caractères au moins.
  • Les lectures (GET) ne sont pas signées.
Chaîne signée
POST
/api/v1/collections
1767225600
9f86d081884c7d659a2feaa0c55ad015
collecte-CMD-10045
{"product_id":"f415c255-821d-4314-8628-61beea638215","operator_code":"MTN_CG","amount":5000,"phone_number":"+242060000000","merchant_reference":"CMD-10045"}

Idempotence

Une Idempotency-Key par opération métier, dérivée de votre référence (collecte-CMD-10045), 190 caractères au plus. Même clé à chaque nouvelle tentative : la transaction existante est renvoyée en 200, rien n'est créé. Même clé pour une opération différente : 409. Délai dépassé ou 5xx : l'issue est inconnue, relancez avec la même clé.

Exemple

Client de référence et première collecte

Une fonction qui authentifie, signe et envoie. Identifiants lus dans TAMO_API_TOKEN et TAMO_API_SECRET.

// Node.js 18+ (fetch et crypto intégrés). Identifiants lus dans l'environnement.
const crypto = require('node:crypto');
const TAMO_BASE_URL = 'https://tamoapi.tasa.pro/api/v1';

async function tamoRequest(method, path, body = null, idempotencyKey = '') {
  const url = new URL(TAMO_BASE_URL + path);
  const raw = body === null ? '' : JSON.stringify(body); // sérialisé UNE fois
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'X-API-Token': process.env.TAMO_API_TOKEN,
  };

  // Toute écriture est signée, sur les octets EXACTS envoyés.
  if (method !== 'GET') {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = crypto.randomBytes(16).toString('hex');
    const canonical = [method, url.pathname + url.search, timestamp, nonce, idempotencyKey, raw].join('\n');
    headers['X-Signature-Version'] = '2';
    headers['X-Timestamp'] = timestamp;
    headers['X-Nonce'] = nonce;
    headers['X-Signature'] = crypto
      .createHmac('sha256', process.env.TAMO_API_SECRET)
      .update(canonical, 'utf8')
      .digest('hex');
    if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
  }

  // Une erreur réseau rend l'issue INCONNUE : relancez avec la MÊME clé.
  const res = await fetch(url, { method, headers, body: method === 'GET' ? undefined : raw });
  return { status: res.status, body: await res.json() };
}

// Encaisser (Cash In) : 201 créée, 200 rejeu de la même clé
const collecte = await tamoRequest('POST', '/collections', {
  product_id: 'f415c255-821d-4314-8628-61beea638215',
  operator_code: 'MTN_CG',
  amount: 5000,
  phone_number: '+242060000000',
  merchant_reference: 'CMD-10045',
  payer_name: 'Client Exemple',
  description: 'Commande CMD-10045',
}, 'collecte-CMD-10045');

console.log(collecte.status, collecte.body.data?.reference ?? collecte.body.message);

// Suivre une transaction
const statut = await tamoRequest('GET', '/collections/status/' + encodeURIComponent(collecte.body.data.reference));
console.log(statut.body.data.status);
POST /api/v1/collections, 201 créée, 200 rejeu
Réponse 201
{
  "message": "Transaction initiée avec succès.",
  "data": {
    "id": "dca9d0a4-fa57-4fcd-b772-3c6ce403369d",
    "reference": "TAMOC-OP7Y-261002-M8XFKU",
    "merchant_reference": "CMD-10045",
    "type": "collection",
    "status": "pending",
    "under_review": false,
    "operator": { "code": "MTN_CG", "name": "Opérateur" },
    "currency": "XAF",
    "amounts": { "requested": 5000, "fee": 100, "net": 4900 },
    "solde": { "sens": "credit", "montant": 4900, "applique": false },
    "expires_at": "2026-10-02T10:58:49.000000Z"
  }
}

Référence

Endpoints

Toutes les routes sont relatives à https://tamoapi.tasa.pro/api/v1.

Authentification

  • POST

    /token

    Token d'accès de portée all (60 min) Basic

  • POST

    /collection/token

    Token limité aux encaissements Basic

  • POST

    /distribution/token

    Token limité aux versements Basic

  • POST

    /credentials/token

    Renouveler api_token (api_secret inchangé) Bearer

  • POST

    /credentials/secret

    Renouveler api_secret Bearer

Collection (Cash In)

  • POST

    /collections

    Initier une collecte Token + signature

  • GET

    /collections/status/{reference}

    Statut par référence TAMO Token

  • GET

    /collections/{id}

    Détail par identifiant Token

Disbursement (Cash Out)

  • POST

    /distributions

    Initier un versement Token + signature

  • GET

    /distributions/status/{reference}

    Statut par référence TAMO Token

  • GET

    /distributions/{id}

    Détail par identifiant Token

Compte

  • GET

    /balances

    Soldes par opérateur, devise et produit Token

  • GET

    /transactions

    Historique paginé, base de la réconciliation Token

  • GET

    /products

    Produits actifs : valeurs de product_id Token

  • GET

    /operators

    Opérateurs utilisables : valeurs de operator_code Token

  • POST

    /sms

    Envoyer un SMS (service SMS activé) Token + signature

Cash In : POST /collections

Le client paie depuis son Mobile Money. Statut pending jusqu'à sa validation ou 30 minutes.

product_id*
Produit actif de votre compte (GET /products).
operator_code*
Opérateur ouvert à la collecte pour ce produit.
amount*
Entier, unité de la devise. De 10 à 500 000.
phone_number*
Numéro du payeur, 8 à 15 chiffres, indicatif compris.
merchant_reference
Votre référence (100 car. max), renvoyée dans les notifications.
payer_name
Nom du payeur (150 car. max).
payer_email
Email du payeur (150 car. max).
description
Libellé (500 car. max).
metadata
Données libres, renvoyées telles quelles.

* obligatoire

Cash Out : POST /distributions

Vous versez sur le Mobile Money du bénéficiaire. Exige une IP déclarée et un solde suffisant.

product_id*
Produit actif ouvert à la distribution.
operator_code*
Opérateur du bénéficiaire.
amount*
De 10 à 500 000. Montant reçu ; les frais s'y ajoutent sur votre solde.
phone_number*
Numéro du bénéficiaire, 8 à 15 chiffres, indicatif compris.
merchant_reference
Votre référence de versement (100 car. max).
recipient_name
Nom du bénéficiaire (150 car. max).
description
Motif (500 car. max).
metadata
Données libres.

* obligatoire

Cycle de vie

Statuts d'une transaction

pendingen cours

Attendre la notification (validation du client, traitement opérateur).

processingen cours

Attendre.

successdéfinitif

Livrer ou clôturer.

faileddéfinitif

Proposer une nouvelle opération (nouvelle clé). En distribution, le montant est restitué.

expireddéfinitif

Collecte non validée en 30 minutes.

cancelleddéfinitif

Mettre à jour votre comptabilité.

refundeddéfinitif

Mettre à jour votre comptabilité.

under_review: true: l'opérateur n'a pas donné de réponse définitive. La transaction reste pending, les fonds restent bloqués, nos équipes vérifient. Ne relancez pas : le statut définitif arrivera par notification.

Notifications

Webhooks signés

À chaque changement de statut, la plateforme envoie un POST à l'URL de rappel de votre produit ou à vos webhooks déclarés. Vérifiez X-Webhook-Signature sur le corps brut avant tout traitement.

Événements

  • collection.pending
  • collection.success
  • collection.failed
  • collection.expired
  • distribution.pending
  • distribution.success
  • distribution.failed
  • webhook.test
  • En-têtes : X-Webhook-Event, X-Webhook-Signature, X-Webhook-Attempt, et X-Webhook-Test pour webhook.test.
  • Secret : propre à chaque destination, affiché à la création d'un webhook ou par le test de l'URL de rappel d'un produit.
  • Réponse : 2xx en moins de 30 secondes. Sur 5xx, 408, 425 ou 429, jusqu'à 4 tentatives (après 1, 5 et 15 minutes). Les autres 4xx abandonnent.
  • Doublons : une notification peut arriver plusieurs fois ou dans le désordre. Dédupliquez sur data.reference + event et ne revenez jamais d'un statut définitif à un statut en cours.
Notification reçue
{
  "event": "collection.success",
  "timestamp": "2026-10-02T10:29:12.000000Z",
  "data": {
    "reference": "TAMOC-OP7Y-261002-M8XFKU",
    "merchant_reference": "CMD-10045",
    "type": "collection",
    "status": "success",
    "amount": 5000,
    "fee": 100,
    "net_amount": 4900,
    "currency": "XAF",
    "phone_number": "+242060000000",
    "operator": "MTN_CG",
    "completed_at": "2026-10-02T10:29:12.000000Z"
  }
}
webhook.js
// Node.js + Express. express.raw : la signature porte sur les octets BRUTS.
const crypto = require('node:crypto');
const express = require('express');
const app = express();

app.post('/webhooks/tamo', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.TAMO_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');
  const received = String(req.get('X-Webhook-Signature') || '');

  // Comparaison à temps constant, AVANT tout traitement.
  const valid = received.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // 1. Dédupliquer sur event.data.reference + event.event
  // 2. Mettre en file, puis répondre 2xx rapidement
  // 3. Ne jamais revenir d'un statut définitif à un statut en cours
  res.sendStatus(200);
});

app.listen(3000);

Erreurs

Codes de réponse

Toute erreur renvoie un JSON avec message, souvent complété par errors, action ou causes. Chaque réponse porte un en-tête X-Request-Id à communiquer au support.

HTTPCauseRéaction de votre code
200Rejeu d'une Idempotency-Key (en-tête Idempotent-Replay: true).Traiter comme la transaction d'origine.
400Idempotency-Key absente ou trop longue ; X-Signature-Version inconnue.Corriger la requête.
401Authentification absente ou révoquée ; signature absente, invalide ou v1 sur une route d'argent ; X-Timestamp hors fenêtre ; X-Nonce déjà utilisé.Vérifier les identifiants, synchroniser l'horloge, un nonce neuf à chaque envoi. Ne pas réessayer à l'identique.
403IP non déclarée ou liste blanche vide ; token hors de sa portée ; compte suspendu.Déclarer l'IP dans le portail, utiliser un token de bonne portée.
404Produit inconnu ou non actif ; transaction inconnue.Relire GET /products.
409Idempotency-Key réutilisée avec un autre montant, numéro, produit ou opérateur.Une clé par opération.
422Champ invalide, montant hors bornes, opérateur non ouvert à ce service, ou solde de distribution insuffisant.Corriger ; approvisionner le solde. Ne pas réessayer à l'identique.
429Limite de débit atteinte.Attendre Retry-After secondes.
5xxIncident, ou réponse perdue : l'issue est inconnue.Relancer avec la même Idempotency-Key, ou consulter le statut.

Mise en production

Avant de passer en production

  • Identifiants en coffre ; horloge synchronisée (NTP).
  • Écritures signées en v2 ; corps envoyé identique au corps signé.
  • Idempotency-Key dérivée de la référence et réutilisée à chaque nouvelle tentative.
  • IP de production déclarées ; URL de rappel en HTTPS, signature vérifiée, réponse 2xx rapide, traitement dédupliqué.
  • under_review géré sans relance ; réconciliation quotidienne avec GET /transactions.
  • Premier encaissement et premier versement de faible montant validés de bout en bout.

Prêt à envoyer votre première requête ?

Créez un compte, générez vos clés et suivez l'état de votre intégration depuis le dashboard.