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.

Créer un compte et une clé

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.

La même chose, en MCP, pour un agent

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.

Ce que le produit garde, et ce qu'il ne garde pas

Le contrat

Quatre règles communes aux routes

1

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.

2

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.

3

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.

4

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.

Ce que chaque formule autorise

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.

Les questions qu'on nous pose le plus

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.

Le guide pas à pas côté Zapier

{
  "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.

Ce que nous envoyons

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.

Ce que nous recevons

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.

Brancher Stripe, écran par écran

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.

Les outils déjà branchés, sans écrire de code

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.

Ce qu'un agent fait vraiment de ces outils

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