Home › Developer portal › Authentication
Document menu← Home
Developer portal
  1. Quick start
  2. Authentication
  3. REST API reference
  4. Webhooks guide
  5. Exports guide
  6. ERP integration guide
  7. Bank integration guide
  8. Tauraco Collateral documentation
  9. MCP documentation
  10. Code samples
  11. Error handling
  12. Sandbox environments
  13. Versions and changelog
  14. FAQ
  15. Developer support
Developers

Authentication

The public API and the MCP server use OAuth 2.0 (with the OAuth 2.1 rules: mandatory PKCE S256, no implicit flow, rotating refresh tokens). The authorization server is Tauraco's own; its issuer is https://api.tauraco.io, including under the *.tauraco.ai names, online since 2026-11-06.

Endpoints

EndpointMethodStandardRole
/oauth/tokenPOSTRFC 6749token issuance and refresh
/oauth/authorizeGETRFC 6749, PKCE S256start of the delegated flow, then the consent page
/oauth/revokePOSTRFC 7009token revocation
/oauth/introspectPOSTRFC 7662reserved to Tauraco internal clients
/oauth/registerPOSTRFC 7591dynamic registration of MCP clients, when open
/.well-known/oauth-authorization-serverGETRFC 8414authorization server metadata
/.well-known/jwks.jsonGETRFC 7517public signing keys
/.well-known/oauth-protected-resource/public/v1GETRFC 9728protected resource: public API
/.well-known/oauth-protected-resource/mcpGETRFC 9728protected resource: MCP server

All are served under https://api.tauraco.io and are online since 2026-11-05; dynamic registration is open. A closed module would answer 404.

Client credentials (server to server)

For software acting on behalf of the company, with no user in front of the screen: accounting software, ERP, integrator, financial institution.

  • Client authentication with HTTP Basic (client_id:client_secret) or in the body (client_secret_post).
  • grant_type=client_credentials, scope (space-separated scopes), resource=https://api.tauraco.io/public/v1, and tenant when several companies have authorised the application.
  • Response: access_token, token_type Bearer, expires_in 600, effective scope. No refresh token.
  • The grant for this flow is given by a company administrator in the Connected apps console, never through the consent page.
python/client_credentials.pyDownload the file
"""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 and PKCE (delegated access)

For an application acting on behalf of a user: an AI assistant connected through MCP, an interactive third-party application.

  1. The client redirects to /oauth/authorize with response_type=code, client_id, redirect_uri (exact match with a declared address), code_challenge and code_challenge_method=S256, state, scope and resource.
  2. The user signs in to Tauraco, picks the company among those where they hold an active role, sees the redirect host in clear and accepts or denies the scopes.
  3. The client receives code, state and iss (RFC 9207), then exchanges the code (valid for 5 minutes, single use) for a 60-minute access token and a 30-day refresh token.
  4. Every refresh returns a new refresh token. Reusing a refresh token that was already exchanged revokes the whole token family and the grant.

Delegated access carries the user's rights in the chosen company, never more: if the user loses their role, their account or access to the company, the application loses access on the next call.

Access token

A JWT signed with RS256 (RFC 9068), header typ at+jwt, kid published in the JWKS. It holds iss, aud (tauraco-public-api or tauraco-mcp), sub, client_id, tenant_id, grant_id, scope, jti, iat and exp; never a role, an e-mail address or a name. Treat it as opaque: its shape may change.

A token is only valid for its audience: a public API token is refused by the MCP server, and vice versa. Tauraco application session tokens are never accepted by the public API.

Revocation and lifecycle

On every call, Tauraco re-reads the application (active), the grant (active, not expired), the bearer user (active, not deleted) and the company (active). A suspended application, a revoked grant or a delegating user without an active role give 401 grant_revoked from the next request on. Rotating a secret invalidates the old one immediately.

Scopes

Write scopes imply the matching read (invoices:write gives invoices:read, exports:write gives exports:read, bank:info-requests:write gives bank:requests:read, bank:reconciliation:write gives bank:repayments:read). A scope is checked on top of the application's rights: it never grants more than a user of the company could see.

ScopeAccessClients
company:readcompany profile, calling applicationpartner, MCP
customers:readcustomerspartner, MCP
customers:writecreate customerspartner
suppliers:readsuppliers (without bank details)partner, MCP
invoices:readcustomer invoices and their linespartner, MCP
invoices:writecreate customer invoicespartner
bills:readsupplier billspartner, MCP
accounting:readchart of accounts, journals, entries, trial balance, general ledgerpartner, MCP
payments:readpayments receivedpartner, MCP
payments:writerecord paymentspartner
treasury:readcash movementspartner, MCP
purchases:readpurchase orderspartner, MCP
products:readproducts and servicespartner, MCP
inventory:readinventory items, movements and balancespartner, MCP
tenders:readrelayed public tenderspartner, MCP
markets:readreserved, no resource in v1partner, MCP
financing:readthe company's financing requestspartner, MCP
documents:readdocument contents, on top of the parent's scopepartner, MCP
guarantees:readreserved, no resource in v1partner, MCP
collateral:readCollateral deposits and security interestspartner, MCP
exports:readexports and their filespartner, MCP
exports:writerequest an exportpartner, MCP
webhooks:managemanage webhook endpointspartner
mcp:readconnect to the MCP server (required by every /mcp request)MCP
bank:inbox:readthe institution's inboxbank
bank:requests:readreceived financing requestsbank
bank:documents:readrequest documentsbank
bank:info-requests:writerequests for additional informationbank
bank:collateral:readsecurity interests linked to requestsbank
bank:repayments:readinstalments and repaymentsbank
bank:reconciliation:writereconcile repaymentsbank

Tauraco "internal" clients receive the same scopes as a partner. A bank client only receives bank:* scopes.

OAuth errors

The /oauth/* endpoints answer in the RFC 6749 format ({"error":"invalid_grant","error_description":"…"}): invalid_request, invalid_client (401, with WWW-Authenticate: Basic), invalid_grant, unauthorized_client, unsupported_grant_type, unsupported_response_type, invalid_scope, invalid_target (RFC 8707), access_denied (consent denied), temporarily_unavailable (token endpoint rate limit or service unavailable); dynamic registration adds invalid_redirect_uri and invalid_client_metadata (RFC 7591). Public API errors are described in Error handling.

Good practice

  • Request only the scopes you need; the administrator sees them all before authorising.
  • Cache the token until expires_in minus a margin; do not request a token per call (the /oauth/token endpoint has its own rate limit).
  • Never log a secret or a token; log client_id and X-Request-Id.