Documentation
API de veille et webhooks
Poussez vos clients et fournisseurs depuis votre ERP, lisez ce que la veille détecte dans les sources officielles, et recevez chaque événement sur votre serveur. Une API REST en JSON, authentifiée par jeton, sans SDK à installer.
Démarrer en quatre étapes
- Créez un compte TierSafe (gratuit).
- Dans l'espace Sentinelle, page API, créez un jeton : il commence par ts_ et n'est montré qu'une fois.
- Poussez un premier lot de tiers (POST /companies) : le premier passage du moteur est fait aussitôt, sur toutes les sources.
- Abonné, lisez les événements (GET /events) ou déclarez une adresse de webhook pour les recevoir dès leur détection.
Sans abonnement, l'API sert à préparer le portefeuille : la poussée et la liste des fiches fonctionnent, l'état de veille de chaque fiche reste masqué et le fil des événements, le détail d'un événement, l'acquittement et les webhooks demandent un abonnement actif.
La spécification OpenAPI 3.1 de l'API et des webhooks s'importe dans Postman, Insomnia ou un générateur de client : télécharger la spécification (JSON)
Authentification
Chaque requête porte le jeton dans l'en-tête Authorization, en schéma Bearer. L'API est sans session : aucun cookie n'est envoyé ni attendu.
curl -H "Authorization: Bearer ts_..." https://tiersafe.eu/api/v1/monitoring/subscription
- Jusqu'à 5 jetons actifs par compte, un par application. Seule l'empreinte du jeton est conservée : perdu, il se révoque et se remplace.
- Un jeton absent, inconnu ou révoqué reçoit une réponse 401 avec le code invalid_token, jamais une redirection vers la page de connexion.
- La cadence est limitée par jeton : 120 requêtes par minute. Au-delà, la réponse est 429 avec le code rate_limited et un en-tête Retry-After en secondes.
Conventions
- Chemins relatifs à la base :
https://tiersafe.eu/api/v1/monitoring - Corps de requête et de réponse en JSON, encodé en UTF-8.
- Dates et heures au format ISO 8601 avec fuseau (2026-09-28T03:10:00+02:00), dates seules en AAAA-MM-JJ.
- Listes paginées par limit (100 par défaut, 500 au plus) et offset ; la réponse porte le total.
- Toute erreur porte un code machine stable (« code ») et un message lisible (« detail », en français). Programmez sur le code, jamais sur le message.
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"code": "subscription_required", "detail": "Le fil des événements demande un abonnement de veille actif."}
Portefeuille de tiers
Le portefeuille est la liste des tiers que vous faites surveiller. Chaque fiche porte votre référence client, unique dans le portefeuille, et au moins un identifiant : SIREN, SIRET ou numéro de TVA.
GET /subscription
État de l'abonnement et du portefeuille : quota du palier, fiches suivies, fin de la couverture, rotation du mois et solde de crédits. À interroger avant une poussée importante.
{
"subscribed": true,
"quota": 2000,
"followed": 1843,
"over_quota": 0,
"coverage_end": "2027-09-30T00:00:00+02:00",
"rotation": {"allowance": 657, "used": 12},
"credit_balance": 450
}
POST /companies
Pousse un lot de fiches en une requête, avec les mêmes règles que l'import de fichier de l'espace Sentinelle.
- Champs d'une fiche : reference, name, siren, siret, vat, platform (plateforme agréée de réception), routing (identifiant de routage), et facultatifs address, postal_code, city, iban, bic, email (une adresse de contact). Tous textuels ; une référence vide prend l'identifiant le plus précis. Une valeur inexploitable à côté d'un identifiant valide (SIRET ou TVA de clé fausse, e-mail mal formé) n'empêche pas le suivi : elle est écartée et signalée en anomalie.
- Mode « merge » (par défaut) : ajoute les références inconnues et met à jour les autres ; rien n'est retiré.
- Mode « sync » : le lot décrit le portefeuille entier, les références absentes sont retirées. Chaque ligne doit alors être exploitable : une ligne illisible vaudrait retrait de sa fiche, le lot est donc refusé en entier (422 sync_invalid_lines, lignes en cause listées dans « invalid »).
- Paramètre dry_run=1 : renvoie l'aperçu du lot (ajouts, modifications, retraits, lignes ignorées, crédits de rotation) sans rien écrire.
curl -X POST "https://tiersafe.eu/api/v1/monitoring/companies?dry_run=1" \
-H "Authorization: Bearer ts_..." -H "Content-Type: application/json" \
-d '{"mode": "merge", "companies": [
{"reference": "C0001", "name": "DANONE", "siren": "552032534"},
{"reference": "C0002", "siret": "55203253400646", "address": "17 boulevard Haussmann", "postal_code": "75009", "city": "Paris", "iban": "FR7630006000011234567890189", "email": "comptabilite@example.com"},
{"reference": "F0107", "name": "Muster GmbH", "vat": "DE123456789"}
]}'
La réponse détaille ce qui a été appliqué et ce qui a été ignoré : lignes sans identifiant exploitable (« invalid », avec leur rang dans le lot et le motif : field, no_identifier, siren, siret, vat) et références en double (« duplicates »).
{
"applied": true,
"refused": null,
"summary": {
"refused": null,
"active_before": 1843,
"active_after": 1845,
"limit": 10000,
"quota": 2000,
"over_quota": 0,
"subscribed": true,
"rotation": {"allowance": 657, "used": 12, "additions": 2, "extra_credits": 0},
"balance": 450,
"counts": {"added": 2, "modified": 1, "removed": 0}
},
"invalid": [{"index": 4, "reason": "siren", "reference": "C0004"}],
"duplicates": []
}
Statuts : 200 appliqué, ou rien à changer (la requête est rejouable) ; 402 solde de crédits insuffisant pour la rotation, rien n'est écrit ; 409 rotation dépassée sans abonnement ou portefeuille trop grand ; 413 lot trop grand ; 422 lot de synchronisation invalide ; 400 corps illisible.
GET /companies?limit=100&offset=0
Fiches actives, dans l'ordre d'ajout. Abonné, chaque fiche porte son niveau d'alerte (none, watch, critical), son dernier passage et ses événements ouverts ; sans abonnement, ces trois champs valent null.
{
"companies": [{
"uuid": "0192a0b1c2d34e5f8a6b7c8d9e0f1a2c",
"reference": "C0001",
"name": "DANONE",
"siren": "552032534",
"siret": null,
"vat": null,
"platform": null,
"routing": null,
"address": "17 boulevard Haussmann",
"postal_code": "75009",
"city": "Paris",
"iban": null,
"bic": null,
"email": "comptabilite@example.com",
"alert_level": "none",
"awaiting_baseline": false,
"open_events": 0,
"added_at": "2026-09-01T10:12:00+02:00",
"checked_at": "2026-09-28T03:10:00+02:00",
"anomalies": [{"code": "vat_missing", "severity": "review", "params": {"expected": "FR27552032534"}}]
}],
"total": 1845,
"limit": 100,
"offset": 0
}
GET /companies/{reference}
Une fiche active par sa référence, dans la même forme qu'un élément de la liste (champ « company »), état de veille masqué sans abonnement. Réponse 404 unknown_reference si aucune fiche active ne porte cette référence.
curl -H "Authorization: Bearer ts_..." https://tiersafe.eu/api/v1/monitoring/companies/C0001
Chaque fiche lue porte ses anomalies (anomalies, null sans abonnement) : les contrôles de la vérification de fichier, faits quand la fiche entre ou change, et les conflits avec d'autres fiches du portefeuille. Une anomalie a un code stable, une gravité (blocking, review ou info) et les valeurs de son message (params, jamais d'IBAN). Codes : siren_invalid, siret_invalid, siret_other_siren, vat_invalid, vat_other_siren, vat_wrong_key, vat_missing, name_inconsistent, name_to_check, name_differs, email_invalid, email_public_provider, email_domain_unknown, email_dmarc_missing, email_dmarc_monitoring, email_spf_permissive, email_spf_missing, email_spf_invalid, postal_code_suspect, routing_invalid, routing_other_siren, iban_length, iban_checksum, iban_format, bic_format, iban_bic_country, bank_vat_country, bic_unknown, address_conflict, address_department, address_postal_code, address_city, address_number, address_not_found, address_imprecise, address_suggestion, conflict_siret_names, conflict_siret_addresses, conflict_iban, conflict_vat, siren_unknown, siret_unknown, replacement_siret, takeover, address_sirene_mismatch, platform_mismatch, directory_absent, directory_inactive, routing_not_in_directory, einvoicing_excluded.
DELETE /companies/{reference}
Retire une fiche par sa référence : elle cesse d'être surveillée et reste comptée dans la rotation du mois si elle y était entrée. Réponse 404 unknown_reference si aucune fiche active ne porte cette référence.
Événements
Un événement est un écart daté constaté sur une fiche : ce qui a changé, selon quelle source, publié quand, comparé à quelle version de la source. Il n'est jamais recalculé. Ces points d'entrée demandent un abonnement actif.
GET /events?all=0&limit=100&offset=0
Fil des événements du plus récent au plus ancien : les événements non acquittés par défaut, tous avec all=1.
GET /events/{uuid}
Un événement par son identifiant, avec le détail propre à son type (payload) : c'est ce que lit un client prévenu par webhook.
{
"event": {
"uuid": "0192a0b1c2d34e5f8a6b7c8d9e0f1a2b",
"type": "proceeding",
"severity": "critical",
"source": "bodacc",
"company_reference": "C0001",
"company_name": "SARL EXEMPLE",
"payload": {"family": "Procédures collectives", "nature": "Jugement d'ouverture d'une procédure de redressement judiciaire"},
"source_date": "2026-09-25",
"source_version": "2026-09-28T02:40:00+02:00",
"detected_at": "2026-09-28T03:10:00+02:00",
"acknowledged_at": null,
"acknowledgement": null
}
}
POST /events/{uuid}/ack
Acquitte un événement : « seen » (vu, la fiche reste à traiter, par défaut) ou « accepted » (décision prise de garder la fiche en l'état). Idempotent : un événement déjà acquitté le reste.
curl -X POST https://tiersafe.eu/api/v1/monitoring/events/0192a0b1c2d34e5f8a6b7c8d9e0f1a2b/ack \
-H "Authorization: Bearer ts_..." -H "Content-Type: application/json" \
-d '{"ack": "accepted"}'
Types d'événements :
| type | Signification | severity |
|---|---|---|
proceeding |
Procédure collective | critical |
proceeding_closed |
Procédure close (passif éteint) | good |
ceased |
Cessation | critical |
reactivated |
Réactivation | good |
establishment_closed |
Établissement fermé | critical |
establishment_reopened |
Établissement rouvert | good |
name_changed |
Changement de dénomination | info |
head_office_moved |
Transfert de siège | info |
sirene_missing |
Absente du répertoire | info |
vat_invalid |
TVA devenue invalide | critical |
vat_valid |
TVA de nouveau active | good |
platform_removed |
Plateforme radiée | critical |
platform_registered |
Plateforme immatriculée | good |
peppol_reachable |
Devenu joignable (Peppol) | good |
peppol_operator |
Changement d'opérateur Peppol | info |
management_changed |
Changement de dirigeant | info / critical |
registry_changed |
Modification au registre (siège, dénomination, activité, dissolution) | info / critical |
directory_reachable |
Joignable (annuaire) | good |
directory_unreachable |
Plus joignable (annuaire) | info |
directory_address_changed |
Adresse de facturation changée (annuaire) | info |
platform_mismatch |
Plateforme désignée par Peppol différente de l'annoncée | info |
address_mismatch |
Adresse saisie hors de la commune du répertoire | info |
Gravité : critical pour un risque avéré (il part aussi en alerte immédiate par e-mail), info pour un changement à connaître, good pour une bonne nouvelle. De nouveaux types peuvent apparaître : ignorez ceux que vous ne connaissez pas.
Codes d'erreur
| code | HTTP | Signification |
|---|---|---|
invalid_token |
401 | Jeton absent, inconnu ou révoqué. |
rate_limited |
429 | Cadence dépassée pour ce jeton ; réessayez après le délai de l'en-tête Retry-After. |
invalid_json |
400 | Corps de requête illisible : du JSON est attendu. |
invalid_payload |
400 | Forme invalide : « companies » doit être une liste et « mode » valoir merge ou sync. |
empty_payload |
400 | Le lot ne contient aucune fiche. |
too_many_entries |
413 | Plus de 10 000 fiches dans le lot. |
payload_too_large |
413 | Corps de plus de 4 Mo. |
sync_invalid_lines |
422 | Lot de synchronisation contenant une ligne illisible : rien n'est écrit, les lignes sont listées dans « invalid ». |
unknown_reference |
404 | Aucune fiche active ne porte cette référence dans votre portefeuille. |
unknown_event |
404 | Aucun événement de votre portefeuille ne porte cet identifiant. |
invalid_ack |
400 | Valeur d'acquittement autre que seen ou accepted. |
subscription_required |
403 | Point d'entrée réservé aux abonnés actifs. |
email_unconfirmed |
403 | Adresse e-mail du compte non confirmée : le portefeuille ne change pas avant la confirmation. |
free_daily_limit |
429 | Portefeuille sans abonnement déjà modifié 5 fois en 24 heures (imports, ajouts, lots poussés) ; réessayez plus tard, ou abonnez-vous pour lever la limite. |
Une poussée refusée pour une raison métier le dit dans « refused » : rotation (rotation du mois dépassée sans abonnement), credits (solde insuffisant), too_large (portefeuille au-delà de 10 000 fiches), empty (rien à changer, réponse 200).
Webhooks
Abonné, déclarez jusqu'à 3 adresses HTTPS dans l'espace Sentinelle : chaque événement détecté y est envoyé en POST, signé, dès le passage du moteur qui l'a trouvé.
POST /tiersafe/webhook HTTP/1.1
Host: erp.example.com
Content-Type: application/json
User-Agent: TierSafe-Webhooks/1.0
TierSafe-Event: monitoring.event.detected
TierSafe-Delivery: 0192a0b1c2d34e5f8a6b7c8d9e0f1a2d
TierSafe-Signature: t=1790563800,v1=6d2b0f9c...a41e
{"id": "0192a0b1c2d34e5f8a6b7c8d9e0f1a2d", "type": "monitoring.event.detected", "created_at": "2026-09-28T03:12:00+02:00",
"data": {"event": {"uuid": "0192a0b1c2d34e5f8a6b7c8d9e0f1a2b", "type": "proceeding", "severity": "critical", "source": "bodacc",
"company_reference": "C0001", "source_date": "2026-09-25", "detected_at": "2026-09-28T03:10:00+02:00",
"url": "https://tiersafe.eu/api/v1/monitoring/events/0192a0b1c2d34e5f8a6b7c8d9e0f1a2b"}}}
- Le corps est mince par construction : identifiant, type, gravité, source, votre référence client et les dates, jamais la dénomination ni le détail. Relisez le détail par GET /events/{uuid} avec votre jeton : un envoi égaré ne divulgue rien de votre portefeuille.
- Types d'envoi (en-tête TierSafe-Event et champ « type ») : monitoring.event.detected, et webhook.test pour le bouton de test.
- Livraison au moins une fois : un même envoi peut arriver deux fois (réponse perdue, nouvelle tentative). Écartez les doublons grâce à son identifiant, dans le champ « id » et l'en-tête TierSafe-Delivery. Un événement n'est mis en file qu'une fois par adresse ; pour un traitement idempotent, appuyez-vous aussi sur data.event.uuid.
- L'ordre d'arrivée n'est pas garanti : triez sur detected_at si l'ordre compte pour vous.
Vérifier la signature
L'en-tête TierSafe-Signature porte un horodatage (t) et une ou deux signatures (v1) : HMAC-SHA256, avec votre secret, de la chaîne « horodatage.corps » où le corps est l'octet près tel que reçu. Acceptez l'envoi si l'une des signatures correspond, en comparaison à temps constant, et si l'horodatage est à moins de 300 secondes de votre horloge.
PHP
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_TIERSAFE_SIGNATURE'] ?? '';
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
if ('t' === $key) { $timestamp = (int) $value; }
if ('v1' === $key) { $signatures[] = $value; }
}
$expected = hash_hmac('sha256', $timestamp.'.'.$body, getenv('TIERSAFE_WEBHOOK_SECRET'));
$valid = null !== $timestamp && abs(time() - $timestamp) <= 300
&& [] !== array_filter($signatures, fn ($s) => hash_equals($expected, $s));
http_response_code($valid ? 204 : 400);
Node.js
const crypto = require('node:crypto');
function isValid(rawBody, header, secret) {
const pairs = header.split(',').map((part) => part.trim().split('='));
const timestamp = Number(pairs.find(([key]) => key === 't')?.[1]);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
return pairs.filter(([key]) => key === 'v1').some(([, value]) => {
const given = Buffer.from(value ?? '', 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
Python
import hashlib, hmac, time
def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
items = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
timestamps = [v for k, v in items if k == "t"]
if not timestamps or not timestamps[0].isdigit() or abs(time.time() - int(timestamps[0])) > 300:
return False
signed = timestamps[0].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in items if k == "v1")
Réponse, nouvelles tentatives, désactivation
- Répondez 2xx en moins de dix secondes ; le corps de votre réponse est ignoré. Traitez l'événement après avoir répondu si le traitement est long. Une redirection n'est jamais suivie et compte comme un échec.
- Sans réponse 2xx, l'envoi est retenté au bout de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h puis 24 h : neuf tentatives sur environ trois jours.
- Après la dernière tentative, l'adresse est désactivée et vous êtes prévenu par e-mail. Les événements restent lisibles par l'API ; réactivée, l'adresse reçoit les événements des sept derniers jours restés en attente.
- Le secret se remplace depuis l'espace Sentinelle : pendant 24 heures, chaque envoi porte deux signatures (nouveau et ancien secret), le temps de déployer le nouveau sans perdre un envoi.
- Le bouton « Envoyer un test » expédie un envoi webhook.test et affiche aussitôt la réponse de votre serveur ; les derniers envois et leur résultat restent consultables 30 jours.
Adresses acceptées
HTTPS seulement, sur le port 443 ou 8443, sans identifiant ni mot de passe dans l'URL (l'authenticité se vérifie par la signature). Le nom doit résoudre vers des adresses IP publiques uniquement : une adresse privée, locale ou réservée est refusée à l'enregistrement comme à chaque envoi, et la connexion se fait vers l'adresse contrôlée.
Limites et versions
- Portefeuille et lot : 10 000 fiches au plus ; corps de requête : 4 Mo au plus.
- Cadence : 120 requêtes par minute et par jeton. Une synchronisation complète tient en une requête.
- Rotation : un quart du palier par mois (25 %) plus les places libres ; au-delà, chaque nouvelle entrée coûte un crédit (annoncé par dry_run, refusé en 402 si le solde manque). Sans abonnement, la poussée n'est pas limitée par la rotation.
- Version : v1. Les ajouts compatibles (nouveau champ, nouveau type d'événement) arrivent sans changer de version : ignorez ce que vous ne connaissez pas. Un changement incompatible passerait par une nouvelle version (/api/v2), la v1 restant servie pendant la transition.
Une question d'intégration, un besoin au-delà de 10 000 fiches ? Écrivez-nous.