Développeurs & agents
API, webhooks et serveur MCP
Une clé, un en-tête, et les liens, l'attribution et les ventes deviennent des appels HTTP. L'API v1, le serveur MCP et la description OpenAPI sortent du même registre : une seule vérité à apprendre, et elle ne peut pas diverger d'elle-même.
Votre première requête
201
curl -X POST https://subtraq.co/api/v1/links { "id": "clx8f2h4k0001a1b2c3d4e5f6", "slug": "b7kQm2x", "label": "Pub Meta — mars", "destination": "https://exemple.fr/offre", "parentId": null, "status": "active", "utmSource": "meta", "utmMedium": "cpc", "utmCampaign": "mars", "utmTerm": null, "utmContent": null, "createdAt": "2026-09-11T09:14:02.181Z", "space": "maison-lartigue", "domain": "subtraq.co", "shortUrl": "https://subtraq.co/b7kQm2x" }
Un lien créé par l'API redirige dans la seconde : le carnet de la périphérie est réécrit avant que la réponse parte. Et shortUrl vous est rendue toute faite — ne l'assemblez jamais vous-même, le domaine du lien n'est pas toujours le nôtre.
L'authentification
Authentification par clé d'API
Chaque appel porte Authorization: Bearer suivi de la clé de l'agence, de la forme stq_sk_…. Elle se crée dans Réglages → Clés API, s'affiche une seule fois, et n'est jamais stockée en clair : seule son empreinte SHA-256 est gardée. La révocation est immédiate et vaut pour l'API comme pour le serveur MCP.
Sans en-tête : 401 missing_token. Clé inconnue ou révoquée : 401 invalid_token. Clé sans le droit demandé : 403 missing_scope — jamais un résultat partiel qu'il faudrait deviner.
curl -X POST https://subtraq.co/api/v1/links \
-H "Authorization: Bearer stq_sk_VOTRE_CLE_SECRETE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lien-mars-01" \
-d '{
"space": "maison-lartigue",
"destination": "https://exemple.fr/offre",
"label": "Pub Meta — mars",
"utmSource": "meta",
"utmMedium": "cpc",
"utmCampaign": "mars"
}'links:read
Lister les espaces et les liens, détailler un lien avec sa destination finale et ses clics sur trente jours.
links:write
Créer un espace, un lien parent ou un placement, modifier une destination ou un libellé, archiver.
analytics:read
Lire les clics, les prospects, les ventes, le revenu attribué et non attribué, placement par placement.
events:write
Enregistrer une vente. Le seul droit qui touche à de l'argent, et il ne s'exerce que depuis un serveur.
Une clé peut aussi porter *, qui ouvre les quatre — à réserver à vos propres scripts.
Le contrat
Quatre règles communes aux routes
Les montants sont en centimes
4 300 € s'écrit 430000. Aucun flottant ne circule sur de l'argent : amount est un entier, entre 0 et 1 000 000 000. Subtraq ne convertit aucune devise — si un espace en contient plusieurs, mixedCurrencies vaut true et le détail est dans byCurrency.
Les listes se parcourent au curseur
limit va de 1 à 100, cinquante par défaut, et toute autre valeur est refusée. Suivez nextCursor jusqu'à null. Le curseur est opaque : il se transmet, il ne se fabrique pas.
Les écritures acceptent Idempotency-Key
Un rejeu rend la réponse d'origine, avec l'en-tête x-subtraq-idempotent-replay: true. Seuls les succès sont mémorisés : un appel qui a échoué peut être retenté. Pour une vente, c'est invoiceId qui joue ce rôle, et il est obligatoire.
Une seule forme d'erreur
Toujours un objet error avec un code et un message, plus un details quand un paramètre est invalide. Branchez votre code sur code, qui est stable ; montrez message à la personne.
{
"error": {
"code": "slug_taken",
"message": "Le raccourci « promo-mars » est déjà pris."
}
}Les codes les plus fréquents : slug_taken et slug_reserved à la création d'un lien, space_not_found quand l'espace n'appartient pas à la clé, space_limit_reached en 402 quand la formule est pleine. Ce dernier n'est pas une panne : ne le retentez pas en boucle.
Importés du registre, pas d'une plaquette
Les 8 points d'entrée de l'API v1
Chaque ligne ci-dessous est lue du même registre que le serveur expose réellement. Les réponses portent l'en-tête x-subtraq-api-version.
GET /api/v1/spaces
links:read
Liste les espaces clients de l'agence. Un espace = un client final, une marque ou un projet ; tout le reste (liens, clics, ventes) vit dedans.
POST /api/v1/spaces
links:write
Crée un espace client. Refusé avec le code `space_limit_reached` si la formule est pleine.
GET /api/v1/links
links:read
Liste les liens. Un lien PARENT porte la destination ; un PLACEMENT (sublink) porte les UTM d'une publication précise et hérite du reste.
POST /api/v1/links
links:write
Crée un lien court. Sans `parentId`, c'est un lien parent et `destination` est obligatoire. Avec `parentId`, c'est un placement : il hérite de la destination du parent et porte ses propres UTM. Renvoie `shortUrl`, prêt à publier.
GET /api/v1/links/{id}
links:read
Détaille un lien : sa destination FINALE (UTM comprises, telle que la reçoit le site d'arrivée) et ses clics sur 30 jours.
PATCH /api/v1/links/{id}
links:write
Modifie la destination ou le libellé d'un lien, ou l'archive. Un lien n'est JAMAIS supprimé : archivé, il cesse de rediriger mais son historique reste.
GET /api/v1/analytics
analytics:read
Les chiffres d'un espace : clics, prospects, ventes, revenu ATTRIBUÉ et revenu NON attribué, plus le détail placement par placement. Montants en CENTIMES. Subtraq ne convertit pas les devises : si `mixedCurrencies` est vrai, les totaux ne concernent que `currency`. `model` choisit la lecture de l'attribution ; la réponse rappelle toujours lequel a servi.
POST /api/v1/sales
events:write
Enregistre une VENTE et la rattache au placement d'origine de la personne. Montant en CENTIMES. `invoiceId` rend l'appel idempotent : le rejouer ne facture jamais deux fois. Une vente ne peut PAS être envoyée depuis un navigateur.
La description complète, en OpenAPI 3.1, est générée depuis ce même registre : /api/v1/openapi.json.
Sortant
Webhooks signés à chaque conversion
Dès qu'un prospect ou une vente est enregistré, Subtraq poste ce JSON à l'adresse que vous avez donnée. Un webhook vaut pour un seul espace client ou pour toute l'agence, et vous choisissez ce qu'il reçoit : lead, sale, ou les deux. C'est exactement ce qu'attend un déclencheur « recevoir un webhook », chez Zapier comme chez Make ou n8n — rien à installer de notre côté.
Un placement à null n'est pas un oubli : c'est une conversion que Subtraq n'a pas su rattacher, et il le dit plutôt que d'inventer une origine.
L'adresse doit être en https et publique : les adresses locales et les plages privées sont refusées, sans quoi un webhook deviendrait une sonde braquée sur notre propre réseau. Deux tentatives, cinq secondes chacune, puis l'échec est consigné à côté du webhook. Un envoi raté ne fait jamais échouer la conversion.
{
"event": "sale",
"at": "2026-09-11T09:14:02.181Z",
"space": { "slug": "maison-lartigue", "name": "Maison Lartigue" },
"person": { "email": "[email protected]", "externalId": null },
"placement": { "slug": "b7kQm2x", "label": "Pub Meta — mars" },
"amountMinor": 430000,
"currency": "EUR",
"eventId": "clx9a1b2c0002d3e4f5g6h7i8"
}Dans les deux sens
Signé à l’aller. Vérifié au retour.
La signature de nos envois
L'en-tête X-Subtraq-Signature porte t=HORODATAGE,v1=SIGNATURE, où la signature est le HMAC-SHA256 de HORODATAGE.CORPS calculé avec le secret affiché à la création du webhook. L'horodatage entre dans le calcul : sans lui, une signature capturée resterait valable pour toujours. Comparez à durée constante, et refusez au-delà de cinq minutes.
Le webhook Stripe de votre client
Votre client colle une adresse dans son tableau de bord Stripe et ses ventes arrivent, sans une ligne de code. La signature Stripe est obligatoire : la clé de l'adresse ne donne aucun droit, c'est elle seule qui autorise. Trois natures d'événement sont exploitées — checkout.session.completed, invoice.payment_succeeded et invoice.paid. Le reste reçoit un 200 et passe : une erreur ferait retenter Stripe sans fin.
X-Subtraq-Signature: t=1757580842,v1=9f2c…e41 User-Agent: Subtraq-Webhook/1.0 Content-Type: application/json
Un même paiement Stripe peut arriver par deux natures d'événement : la déduplication porte donc sur l'identifiant du paiement, la seule clé qui tienne de l'une à l'autre. Les montants de Stripe sont déjà dans la plus petite unité de la devise, comme chez nous — aucune conversion, donc aucune occasion de se tromper d'un facteur cent.
Côté navigateur
Une balise. Elle se règle toute seule.
Une seule balise, posée sur le site de votre client. Elle ramasse st_id dans l'URL d'arrivée, le range, et tient en plus la liste ordonnée des clics suivants — le parcours, jusqu'à vingt. Le premier clic garde sa place quoi qu'il arrive.
<script src="https://subtraq.co/subtraq.js" async
data-key="stq_pk_VOTRE_CLE_PUBLIABLE"
data-endpoint="https://subtraq.co"></script>Trois appels suffisent ensuite : lead(email) quand une personne laisse son adresse, identify(externalId) quand elle a un compte chez vous, track(nom) pour un événement libre. Et decorate(url) recolle l'identifiant de clic sur un lien sortant, pour suivre quelqu'un d'un domaine à l'autre.
document.querySelector('#mon-formulaire')
.addEventListener('submit', function () {
window.subtraq.lead(document.querySelector('[name="email"]').value);
});Ces appels visent /api/t/lead et /api/t/event, authentifiés par la clé publiable stq_pk_… — celle qui peut se lire dans le HTML, parce qu'elle n'ouvre aucune lecture. Aucun montant n'est accepté ici : une somme écrite depuis un navigateur est une somme que n'importe qui peut écrire. Vous restreignez les domaines autorisés depuis l'écran d'installation ; hors de la liste, la réponse est 403 origin_not_allowed. Aucune empreinte d'appareil, aucun cookie tiers.
Servis par l'API elle-même
Trois documents. Écrits par le code.
Un fichier posé dans un dépôt vieillit en silence. Ces trois-là sont régénérés à chaque appel depuis le registre : ils ne peuvent pas mentir sur ce que le serveur fait aujourd'hui.
/api/v1/openapi.json
La description OpenAPI 3.1 : chemins, paramètres, corps, réponses, et le droit requis par opération. De quoi générer un client ou nourrir un outil de test.
/api/v1/agent-skill.md
Le contrat d'utilisation écrit pour un agent : le modèle en trois phrases, le parcours le plus courant, les règles à connaître et le tableau de chaque paramètre.
/api/mcp
Le serveur MCP, en JSON-RPC 2.0 sur HTTP : initialize, tools/list, tools/call, avec la même clé Bearer. Un GET renvoie la carte de visite du serveur.
Le serveur MCP n'a pas sa propre implémentation : il appelle les mêmes gestionnaires de routes que l'API. Deux implémentations auraient divergé au premier correctif, et un agent aurait obtenu un résultat différent selon la porte empruntée. Claude, Cursor ou Zed : la même clé, le même résultat.
Questions des développeurs
Faut-il installer un SDK ?
Non. L'API est du HTTP et du JSON : curl, fetch, requests, n'importe quel client fait l'affaire. Un fichier JavaScript existe pour le navigateur, subtraq.js, mais il ne sert qu'au suivi des prospects sur le site de votre client — l'API, elle, s'appelle depuis n'importe où.
Comment éviter de créer deux fois le même objet ?
Posez un en-tête Idempotency-Key sur vos écritures. Un rejeu vous rend la réponse d'origine, accompagnée de x-subtraq-idempotent-replay: true. Pour une vente, c'est le numéro de facture, invoiceId, qui joue ce rôle, et il est obligatoire : rejouer un webhook ne facture jamais deux fois.
Une clé peut-elle voir les données d'une autre agence ?
Non. Chaque appel résout l'espace demandé à l'intérieur de l'agence de la clé. Un espace qui appartient à quelqu'un d'autre est simplement introuvable : la réponse porte un 404, jamais un résultat qui laisserait croire que l'espace est vide.
Peut-on envoyer une vente depuis le navigateur ?
Non, c'est refusé par construction : les routes de suivi côté navigateur n'acceptent aucun montant. Une somme écrite depuis une page est une somme que n'importe qui peut écrire, et la clé secrète ne doit jamais atteindre un navigateur.
Que se passe-t-il si un webhook n'arrive pas ?
Deux tentatives sont faites, cinq secondes chacune, puis l'échec est consigné à côté du webhook avec son statut, son message, sa date et le nombre d'échecs consécutifs. Il n'y a pas de file d'attente : un destinataire indisponible plusieurs minutes perd les envois de cette période. La conversion, elle, reste enregistrée.
Un lien peut-il être supprimé par l'API ?
Non : on archive. Un lien archivé cesse de rediriger, mais son historique de clics, de prospects et de ventes reste lisible. Une suppression effacerait des chiffres déjà présentés à un client.
Créez votre clé d'API
Un compte, une clé, un curl : le 201 tombe et le lien redirige déjà. Le reste — webhooks, Stripe, MCP — se branche quand vous en avez besoin, pas avant.
Créer un compte gratuit