Accueil › Portail développeurs › Authentification
Menu du document← Accueil
Portail développeurs
  1. Démarrage rapide
  2. Authentification
  3. Référence API REST
  4. Guide des webhooks
  5. Guide des exports
  6. Guide d'intégration ERP
  7. Guide d'intégration bancaire
  8. Documentation Tauraco Collateral
  9. Documentation MCP
  10. Exemples de code
  11. Gestion des erreurs
  12. Environnements sandbox
  13. Versions et changelog
  14. FAQ
  15. Support développeurs
Développeurs

Authentification

L'API publique et le serveur MCP utilisent OAuth 2.0 (avec les règles d'OAuth 2.1 : PKCE S256 obligatoire, aucun flux implicite, rotation des jetons de rafraîchissement). Le serveur d'autorisation est celui de Tauraco ; son émetteur est https://api.tauraco.io, y compris sous les noms *.tauraco.ai, en ligne depuis le 2026-11-06.

Points de terminaison

PointMéthodeNormeRôle
/oauth/tokenPOSTRFC 6749émission et rafraîchissement des jetons
/oauth/authorizeGETRFC 6749, PKCE S256début du flux délégué, puis page de consentement
/oauth/revokePOSTRFC 7009révocation d'un jeton
/oauth/introspectPOSTRFC 7662réservé aux clients internes de Tauraco
/oauth/registerPOSTRFC 7591enregistrement dynamique des clients MCP, s'il est ouvert
/.well-known/oauth-authorization-serverGETRFC 8414métadonnées du serveur d'autorisation
/.well-known/jwks.jsonGETRFC 7517clés publiques de signature
/.well-known/oauth-protected-resource/public/v1GETRFC 9728ressource protégée : API publique
/.well-known/oauth-protected-resource/mcpGETRFC 9728ressource protégée : serveur MCP

Tous sont servis sous https://api.tauraco.io et sont en ligne depuis le 2026-11-05 ; l'enregistrement dynamique est ouvert. Un module fermé répondrait 404.

Client credentials (serveur à serveur)

Pour un logiciel qui agit au nom de l'entreprise, sans utilisateur devant l'écran : logiciel comptable, ERP, intégrateur, établissement financier.

  • Authentification du client par HTTP Basic (client_id:client_secret) ou dans le corps (client_secret_post).
  • grant_type=client_credentials, scope (scopes séparés par des espaces), resource=https://api.tauraco.io/public/v1, et tenant si plusieurs entreprises ont autorisé l'application.
  • Réponse : access_token, token_type Bearer, expires_in 600, scope effectif. Pas de jeton de rafraîchissement.
  • L'octroi de ce flux est donné par un administrateur de l'entreprise dans la console Applications connectées, jamais par la page de consentement.
python/client_credentials.pyTélécharger le fichier
"""Obtenir un jeton ``client_credentials`` et lire les factures, page par page.

Get a ``client_credentials`` token and read invoices page by page.

Bibliothèque standard seule (Python 3.9+). Variables d'environnement :
``TAURACO_CLIENT_ID`` (``tc_live_…``), ``TAURACO_CLIENT_SECRET`` (``tcs_…``, affiché une
seule fois à la création de l'application), et facultativement ``TAURACO_TENANT``
(obligatoire si plusieurs entreprises ont autorisé l'application).

Les adresses par défaut sont les surfaces prévues sous ``api.tauraco.io`` ; elles ne
répondent qu'une fois l'API publique ouverte (voir la page « Versions et changelog »).
"""
from __future__ import annotations

import base64
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Callable, Dict, Iterator, Optional, Tuple

API_BASE = os.environ.get("TAURACO_API_BASE", "https://api.tauraco.io/public/v1")
TOKEN_URL = os.environ.get("TAURACO_TOKEN_URL", "https://api.tauraco.io/oauth/token")

#: Transport : (méthode, url, en-têtes, corps) -> (statut, en-têtes, corps). Remplaçable en test.
Transport = Callable[[str, str, Dict[str, str], Optional[bytes]], Tuple[int, Dict[str, str], bytes]]


class TauracoError(RuntimeError):
    """Erreur de l'API : corps ``application/problem+json`` (RFC 9457) ou erreur OAuth."""

    def __init__(self, status: int, body: dict):
        self.status = status
        self.code = body.get("code") or body.get("error") or "unknown"
        self.request_id = body.get("request_id")
        super().__init__(f"{status} {self.code}: {body.get('detail') or body.get('error_description') or ''}")


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    """Aucune redirection suivie : urllib renverrait sinon l'en-tête ``Authorization`` (jeton)
    à l'hôte de destination. L'API Tauraco ne redirige jamais ; un 3xx est rendu tel quel."""

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


_OPENER = urllib.request.build_opener(_NoRedirect)


def http_transport(method: str, url: str, headers: Dict[str, str],
                   body: Optional[bytes]) -> Tuple[int, Dict[str, str], bytes]:
    request = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with _OPENER.open(request, timeout=30) as response:  # noqa: S310 (URL maîtrisée)
            return response.status, dict(response.headers.items()), response.read()
    except urllib.error.HTTPError as err:
        return err.code, dict(err.headers.items()), err.read()


def get_token(client_id: str, client_secret: str, scopes: str, *, tenant: Optional[str] = None,
              resource: str = API_BASE, transport: Transport = http_transport) -> dict:
    """``POST /oauth/token`` (``grant_type=client_credentials``), authentification HTTP Basic."""
    form = {"grant_type": "client_credentials", "scope": scopes, "resource": resource}
    if tenant:
        form["tenant"] = tenant
    basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode("ascii")
    status, _headers, raw = transport("POST", TOKEN_URL, {
        "Authorization": f"Basic {basic}",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json",
    }, urllib.parse.urlencode(form).encode())
    payload = json.loads(raw or b"{}")
    if status != 200:
        raise TauracoError(status, payload)
    return payload  # {"access_token", "token_type": "Bearer", "expires_in": 600, "scope"}


def iterate(path: str, token: str, params: Optional[Dict[str, str]] = None, *,
            transport: Transport = http_transport, max_retries: int = 3) -> Iterator[dict]:
    """Toutes les lignes d'une liste paginée par curseur (``data``, ``next_cursor``, ``has_more``)."""
    query = dict(params or {})
    retries = 0
    while True:
        url = f"{API_BASE}{path}?{urllib.parse.urlencode(query)}" if query else f"{API_BASE}{path}"
        status, headers, raw = transport("GET", url, {"Authorization": f"Bearer {token}",
                                                      "Accept": "application/json"}, None)
        if status == 429 and retries < max_retries:
            retries += 1
            time.sleep(int(headers.get("Retry-After") or headers.get("retry-after") or 1))
            continue
        payload = json.loads(raw or b"{}")
        if status != 200:
            raise TauracoError(status, payload)
        retries = 0
        yield from payload.get("data", [])
        if not payload.get("has_more") or not payload.get("next_cursor"):
            return
        query["cursor"] = payload["next_cursor"]


def main() -> None:
    token = get_token(os.environ["TAURACO_CLIENT_ID"], os.environ["TAURACO_CLIENT_SECRET"],
                      "invoices:read", tenant=os.environ.get("TAURACO_TENANT"))["access_token"]
    for invoice in iterate("/invoices", token, {"status": "PAID", "limit": "100"}):
        # Montants : chaînes décimales en unités majeures ("1180000" XOF), jamais des flottants.
        print(invoice["number"], invoice.get("customer_name"), invoice["total"], invoice["currency"])


if __name__ == "__main__":
    main()

Authorization code et PKCE (accès délégué)

Pour une application qui agit au nom d'un utilisateur : un assistant IA connecté par MCP, une application tierce interactive.

  1. Le client redirige vers /oauth/authorize avec response_type=code, client_id, redirect_uri (égalité exacte avec une adresse déclarée), code_challenge et code_challenge_method=S256, state, scope et resource.
  2. L'utilisateur se connecte à Tauraco, choisit l'entreprise parmi celles où il a un rôle actif, voit l'hôte de redirection en clair et accepte ou refuse les scopes.
  3. Le client reçoit code, state et iss (RFC 9207), puis échange le code (valable 5 minutes, à usage unique) contre un jeton d'accès de 60 minutes et un jeton de rafraîchissement de 30 jours.
  4. Chaque rafraîchissement rend un nouveau jeton de rafraîchissement. Réutiliser un jeton de rafraîchissement déjà échangé révoque toute la famille de jetons et l'octroi.

Un accès délégué porte les droits de l'utilisateur dans l'entreprise choisie, jamais plus : si l'utilisateur perd son rôle, son compte ou l'accès à l'entreprise, l'application perd l'accès à l'appel suivant.

Jeton d'accès

Jeton JWT signé RS256 (RFC 9068), en-tête typ at+jwt, clé kid publiée dans le JWKS. Il contient iss, aud (tauraco-public-api ou tauraco-mcp), sub, client_id, tenant_id, grant_id, scope, jti, iat et exp ; jamais de rôle, d'adresse e-mail ni de nom. Traitez-le comme opaque : sa forme peut évoluer.

Un jeton n'est valable que pour son audience : un jeton de l'API publique est refusé par le serveur MCP, et inversement. Les jetons de session de l'application Tauraco ne sont jamais acceptés par l'API publique.

Révocation et cycle de vie

À chaque appel, Tauraco relit l'application (active), l'octroi (actif, non expiré), l'utilisateur porteur (actif, non supprimé) et l'entreprise (active). Une application suspendue, un octroi révoqué ou un utilisateur délégant sans rôle actif donnent 401 grant_revoked dès la requête suivante. La rotation d'un secret invalide l'ancien immédiatement.

Scopes

Les scopes d'écriture impliquent la lecture correspondante (invoices:write donne invoices:read, exports:write donne exports:read, bank:info-requests:write donne bank:requests:read, bank:reconciliation:write donne bank:repayments:read). Un scope est vérifié en plus des droits de l'application : il ne donne jamais plus que ce qu'un utilisateur de l'entreprise pourrait voir.

ScopeAccèsClients
company:readprofil de l'entreprise, application appelantepartenaire, MCP
customers:readclientspartenaire, MCP
customers:writecréation de clientspartenaire
suppliers:readfournisseurs (sans coordonnées bancaires)partenaire, MCP
invoices:readfactures clients et leurs lignespartenaire, MCP
invoices:writecréation de factures clientspartenaire
bills:readfactures fournisseurspartenaire, MCP
accounting:readplan de comptes, journaux, écritures, balance, grand livrepartenaire, MCP
payments:readencaissementspartenaire, MCP
payments:writeenregistrement d'encaissementspartenaire
treasury:readmouvements de trésoreriepartenaire, MCP
purchases:readbons de commandepartenaire, MCP
products:readproduits et servicespartenaire, MCP
inventory:readarticles, mouvements et soldes de stockpartenaire, MCP
tenders:readavis de marchés publics relayéspartenaire, MCP
markets:readréservé, aucune ressource en v1partenaire, MCP
financing:readdemandes de financement de l'entreprisepartenaire, MCP
documents:readcontenu des pièces, en plus du scope du parentpartenaire, MCP
guarantees:readréservé, aucune ressource en v1partenaire, MCP
collateral:readdépôts et sûretés Collateralpartenaire, MCP
exports:readexports et leurs fichierspartenaire, MCP
exports:writedemande d'exportpartenaire, MCP
webhooks:managegestion des endpoints de webhookspartenaire
mcp:readconnexion au serveur MCP (exigé par toute requête /mcp)MCP
bank:inbox:readboîte de réception de l'établissementbanque
bank:requests:readdossiers de financement reçusbanque
bank:documents:readpièces des dossiersbanque
bank:info-requests:writedemandes de complémentsbanque
bank:collateral:readsûretés liées aux dossiersbanque
bank:repayments:readéchéances et remboursementsbanque
bank:reconciliation:writerapprochement des remboursementsbanque

Les clients « internes » de Tauraco reçoivent les mêmes scopes qu'un partenaire. Un client banque ne reçoit que des scopes bank:*.

Erreurs OAuth

Les points /oauth/* répondent au format RFC 6749 ({"error":"invalid_grant","error_description":"…"}) : invalid_request, invalid_client (401, avec WWW-Authenticate: Basic), invalid_grant, unauthorized_client, unsupported_grant_type, unsupported_response_type, invalid_scope, invalid_target (RFC 8707), access_denied (consentement refusé), temporarily_unavailable (débit du point de jeton ou service indisponible) ; l'enregistrement dynamique ajoute invalid_redirect_uri et invalid_client_metadata (RFC 7591). Les erreurs de l'API publique sont décrites dans Gestion des erreurs.

Bonnes pratiques

  • Ne demandez que les scopes nécessaires ; l'administrateur les voit tous avant d'autoriser.
  • Mettez le jeton en cache jusqu'à expires_in moins une marge ; ne demandez pas un jeton par requête (le point /oauth/token a son propre débit).
  • Ne journalisez jamais un secret ni un jeton ; journalisez client_id et X-Request-Id.