Qu’est-ce que l’ATSS
L’ATSS (Afro.tools Spec Specification) définit la structure, les champs obligatoires et les règles de chaque spec de ce registre.
Une spec est une description structurée et lisible par les machines d’une capacité d’API. Ce n’est pas un wrapper. Ce n’est pas un SDK. C’est une description déclarative.
Note de terminologie : dans ce dépôt, « spec » désigne une description d’API ATSS (
schema.json+canonical_example.ts). À ne pas confondre avec les « skills » de Claude Code (fichiers SKILL.md dansplugin/skills/), qui sont des fichiers d’instructions pour les agents IA.
Structure des dossiers
specs/{category}/{provider_slug}/
├── provider.json ← provider-level metadata
└── {capability}/
├── schema.json ← capability spec
└── canonical_example.ts ← working TypeScript example
category — payment, communications, identity, logistics ou language (extensible à d’autres catégories).
communications couvre les fournisseurs qui envoient des SMS, WhatsApp, USSD, voix ou e-mail —
les canaux précis vont dans le champ channels de provider.json, pas dans le nom de la catégorie.
provider_slug — minuscules, sans espaces, sans tirets (ex. paycard, wave, nimbasms). Unique toutes catégories confondues : le registre, le MCP et le site cherchent les fournisseurs par leur slug seul, donc un fournisseur proposant des produits dans plusieurs catégories utilise un slug par produit. validate.js rejette un slug réutilisé dans une autre catégorie.
capability — verbe en snake_case (ex. create_payment, verify_payment, send_otp)
Chaque dossier de capacité doit contenir exactement ces deux fichiers. Rien d’autre.
provider.json : champs obligatoires
Un provider.json par dossier de fournisseur. Il porte les métadonnées du fournisseur, migrées depuis schema.json.
{
"slug": "paycard",
"name": "Paycard",
"category": "payment",
"country_code": ["GN"],
"description": "Paycard is a Guinean mobile money payment gateway that lets you accept payments via MTN, Orange, and Cellcom mobile wallets.",
"example_prompt": "Integrate Paycard to accept mobile money payments in Guinea. Create a payment, extract the operation_reference, and verify the transaction server-side before fulfilling the order."
}
| Champ | Type | Règle |
|---|---|---|
| slug | string | Minuscules, sans espaces, sans tirets — correspond au nom du dossier |
| name | string | Nom d’affichage du fournisseur |
| category | string | "payment", "communications", "identity", "logistics" ou "language" — chaque catégorie a un plugin/skills/{category}/SKILL.md correspondant |
| channels | string[] | Obligatoire quand category vaut "communications", facultatif sinon. Tableau non vide, valeurs parmi sms, whatsapp, ussd, voice, email. Liste les canaux décrits dans la documentation publique du fournisseur — pas seulement ceux qui ont déjà une spec |
| country_code | string[] | Codes ISO 3166-1 alpha-2 — tableau non vide |
| description | string | Description du fournisseur en une phrase, pour l’affichage et le contexte MCP |
| example_prompt | string | Prompt d’usage de bout en bout pour les agents IA — décrit un scénario d’intégration complet |
schema.json : champs obligatoires
{
"spec_version": "1.0",
"provider_api_version": "2024-01-01",
"capability": "create_payment",
"capability_type": "synchronous",
"status": "ready",
"currency": ["GNF"],
"sandbox": false,
"docs_url": "https://paycard.com/docs/create-payment",
"docs_public": false,
"auth": {
"type": "api_key",
"location": "body",
"field": "c",
"format": "{token}",
"env_var": "PAYCARD_API_KEY"
},
"endpoint": {
"method": "POST",
"url": "https://mapaycard.com/epay/create/"
},
"example_prompt": "Initiate a Paycard mobile money payment for a given amount in GNF, extract the operation_reference from the response, and store it immediately before redirecting the user.",
"input_schema": {},
"response_schema": {},
"error_schema": {},
"gotchas": [
"Always verify payment status server-side before fulfilling an order."
]
}
Règles des champs
| Champ | Type | Règle |
|---|---|---|
| spec_version | string | "1.0", ou "1.1" quand la spec utilise un bloc ajouté en 1.1 (voir § Blocs ajoutés dans l’ATSS 1.1) |
| provider_api_version | string | YYYY-MM-DD si le fournisseur n’a pas de numéro de version |
| capability | string | snake_case — correspond au nom du dossier |
| capability_type | enum | synchronous, asynchronous, webhook ou sdk |
| status | enum | unverified, draft, ready, verified, deprecated, archived |
| currency | string[] | Codes ISO 4217. Obligatoire quand la category du fournisseur est "payment" (et doit correspondre à son country_code — voir COUNTRY_CURRENCY_MAP dans scripts/validate.js ; un code d’avant redénomination que le fournisseur utilise encore, comme SLL, est accepté via LEGACY_CURRENCY_CODES) ; facultatif sinon — l’omettre pour identity, communications, etc. |
| sandbox | boolean | true si le fournisseur a un environnement sandbox |
| docs_url | string | Peut valoir "" — le fournisseur n’a pas d’URL de documentation publique |
| docs_public | boolean | false si le fournisseur ne publie pas sa documentation d’API publiquement |
| auth | object | Descripteur d’authentification — voir les variantes d’auth ci-dessous |
| endpoint | object | { method, url } |
| example_prompt | string | Obligatoire pour ready/verified — décrit l’usage de cette capacité précise pour les agents IA |
| input_schema | object | JSON Schema décrivant le corps de la requête. Les paramètres de chemin, de requête et d’en-tête vont dans parameters (1.1) |
| response_schema | object | JSON Schema décrivant la réponse en cas de succès |
| error_schema | object | JSON Schema décrivant la réponse en cas d’erreur |
| gotchas | string[] | OBLIGATOIRE — au moins 1 entrée, sans exception |
Champs absents de schema.json (migrés vers provider.json) : provider_slug, provider_name, category, country_code.
evidence : obligatoire seulement quand status vaut "unverified"
"evidence": {
"source_url": "https://provider.com/docs/create-payment",
"source_reference": "Section 'Create a payment'",
"extracted_at": "2026-09-15T10:00:00Z",
"snapshot_hash": "a1b2c3…",
"spans": ["POST /v1/payments", "Authorization: Bearer {api_key}"]
}
Chaque entrée de spans[] doit apparaître littéralement (espaces et casse normalisés) dans le document source situé à source_url. C’est ce qui permet à un relecteur de vérifier qu’une spec unverified n’a pas été hallucinée, sans relire toute la documentation du fournisseur depuis zéro.
Blocs ajoutés dans l’ATSS 1.1
Facultatifs, vérifiés avec atss.schema.json par npm run validate. Une spec
qui en utilise un met spec_version: "1.1" ; une spec 1.0 reste valide telle quelle.
"verification": { // required when status is "verified", forbidden otherwise
"method": "test_live", // test_live | app | community_report
"environment": "sandbox", // sandbox | production
"at": "2026-10-24", // date of the test
"ref": "https://…", // the run, the app, or the report issue — required for community_report
"spec_hash": "sha256:…" // what was tested: schema.json without this block + canonical_example.ts
},
"open_questions": [ // what the provider's docs do not answer — never filled with a guess
"Does the payment URL embed the public or the secret key?"
]
| Champ | Règle |
|---|---|
verification.method | test_live — npm run test:live a réussi ; app — une intégration qui fonctionne (afrotools/examples ou une app en usage) ; community_report — un rapport d’intégration qu’un mainteneur a accepté |
verification.environment | Où cela a été testé. sandbox prouve la forme des requêtes et des réponses, pas le comportement en production (prérequis, limites, argent réel) |
verification.ref | URL de l’exécution, du code de l’app ou du rapport. Pour community_report, l’issue d’afrotools/afrotools |
verification.spec_hash | validate le recalcule : une spec vérifiée puis modifiée échoue jusqu’à ce qu’elle soit vérifiée de nouveau, ou qu’un mainteneur ayant contrôlé la modification mette le hash à jour (le message d’erreur affiche la nouvelle valeur) |
open_questions | Chaînes non vides. Un agent doit en informer l’utilisateur avant de s’appuyer sur la spec |
Auth à plusieurs clés — des champs ajoutés à auth, à côté des champs 1.0 (qui restent) :
"auth": {
"type": "bearer", "location": "header", "field": "Authorization", "format": "Bearer {access_token}", "env_var": "DJOMY_CLIENT_ID",
"credentials": [ // every secret the user must provide
{ "env_var": "DJOMY_CLIENT_ID", "description": "clientId (API key), provided in the Djomy merchant space" },
{ "env_var": "DJOMY_CLIENT_SECRET", "description": "clientSecret (secret key), provided in the Djomy merchant space" }
],
"signing": { // produces {signature}
"algorithm": "HMAC-SHA256", "key": "{DJOMY_CLIENT_SECRET}", "message": "{DJOMY_CLIENT_ID}", "encoding": "hex"
},
"token_exchange": { // produces {access_token}
"method": "POST", "path": "/v1/auth",
"headers": [{ "in": "header", "name": "X-API-KEY", "value": "{DJOMY_CLIENT_ID}:{signature}" }],
"token_field": "data.accessToken", // dotted path in the response body
"expires_in_s": 3600
},
"apply": [ // what every request of this capability sends
{ "in": "header", "name": "X-API-KEY", "value": "{DJOMY_CLIENT_ID}:{signature}" },
{ "in": "header", "name": "Authorization", "value": "Bearer {access_token}" }
]
}
Signature de webhook — uniquement sur une spec capability_type: "webhook" :
"webhook": {
"signature": {
"header": "X-Webhook-Signature",
"format": "v1:{signature}", // how the header carries it
"algorithm": "HMAC-SHA256", "key": "{DJOMY_CLIENT_SECRET}", "message": "{raw_body}", "encoding": "hex"
}
}
Paramètres hors du corps — input_schema est le corps de la requête ; ce qui voyage dans le chemin de l’URL,
la chaîne de requête ou un en-tête va dans parameters :
"endpoint": { "method": "POST", "url": "https://api.example.com/v1/payouts/{batch_id}/items" },
"parameters": [ // illustrative provider
{ "name": "batch_id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Payout batch id" },
{ "name": "dry_run", "in": "query", "schema": { "type": "boolean" } },
{ "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string" } }
]
Condition de succès — quand une réponse 2xx ne suffit pas (par ex. HTTP 200 avec {"code": 1} pour un
paiement échoué), le champ de la réponse qui indique que l’appel a réussi :
"success_condition": { "field": "code", "equals": 0 } // or "in": ["SUCCESS", "PENDING"]
Environnements — dans provider.json, l’URL de base de chaque environnement que le fournisseur documente :
"environments": { "sandbox": { "base_url": "https://sandbox-api.djomy.africa" } }
Dépréciation et retrait — voir § Politique de compatibilité :
"deprecation": { // schema.json: required when status is "deprecated", forbidden otherwise
"reason": "The provider retires this endpoint on 2027-01-31 (changelog link).",
"replaced_by": "create_charge", // optional: a capability of the same provider
"removed_after": "2027-01-31" // optional: the registry removes the capability after this date
},
"removed_capabilities": [ // provider.json: one tombstone per removed capability
{ "capability": "create_bill", "removed_in": "2027-02-03", "reason": "The provider shut the endpoint down on 2027-01-31.", "alternatives": ["create_charge"] }
]
| Règle | |
|---|---|
| Placeholders | {ENV_VAR} est un identifiant de connexion déclaré ; {signature} requiert auth.signing (ou est le résultat de webhook.signature) ; {access_token} requiert auth.token_exchange ; {raw_body} est le corps de la requête exactement tel que reçu et {header:Name} la valeur brute d’un en-tête de requête — les deux uniquement dans webhook.signature.message, où un {ENV_VAR} peut aussi être un identifiant qui vous est propre et que canonical_example.ts lit (ex. {header:Response-Timestamp}{SMILEID_PARTNER_ID}sid_request) ; ailleurs, {ENV_VAR} est un identifiant de connexion déclaré. validate rejette un placeholder qui ne se résout en rien |
auth.credentials | Chaque variable d’environnement une seule fois ; interdit avec auth.type: "none" ; inclut l’auth.env_var de la 1.0. Dès qu’il est présent, chaque lecture de process.env dans canonical_example.ts doit y être déclarée (ou être le secret webhook.signature.key, ou une variable d’environnement signée dans webhook.signature.message) : une variable non déclarée fait échouer la validation, au lieu de l’avertissement de la 1.0 |
auth.apply | in : header, query, body ou path — une clé qui voyage dans le chemin de l’URL, où name est le {placeholder} de endpoint.url qu’elle remplit (ex. /epay/{c}/{ref}/status de Paycard) |
auth.token_exchange | path (sur l’URL de base de l’environnement) ou url (un serveur de jetons sur un autre hôte, ex. https://idp.flutterwave.com/…/token) — exactement un des deux. body (paires nom/valeur) avec son content_type (application/x-www-form-urlencoded) pour une requête de jeton encodée en formulaire ; headers ne contient que des en-têtes (in: "header") |
webhook.signature.key | Exactement un {ENV_VAR} — le secret, jamais une valeur littérale. Il déclare cette variable d’environnement pour la spec |
algorithm, encoding | HMAC-SHA256 ; hex ou base64 — les seules valeurs utilisées par les specs jusqu’ici ; les autres sont ajoutées avec la première spec qui en a besoin |
parameters | in : path, query ou header ; name une seule fois par in (les noms d’en-tête ignorent la casse), jamais un nom que auth.apply envoie déjà ; schema est un JSON Schema. Un paramètre path est required: true et remplit un {placeholder} de endpoint.url ; dès que parameters est présent, chaque {placeholder} de l’URL est rempli par un paramètre de chemin ou par auth.apply ({UPPER_SNAKE_CASE} reste une variable d’environnement) ; un paramètre n’est jamais aussi une propriété d’input_schema, et un GET/DELETE avec parameters n’a pas de corps. validate avertit pour une spec 1.1 dont l’URL a des placeholders et pas de parameters. Pas sur un webhook : son URL est la vôtre |
success_condition | field est un chemin pointé (data.status, à travers les éléments de tableau) que response_schema décrit, quand il décrit quelque chose ; exactement un de equals (une chaîne, un nombre, un booléen ou null) ou in (une liste de ces valeurs), du type du champ (0, et non "0", pour un code entier). Seulement ce que la documentation du fournisseur énonce — sinon, une question ouverte. Pas sur un webhook |
| Idempotence | Convention, signalée par un avertissement de validate : un POST d’un fournisseur de paiement qui déplace de l’argent (paiement, checkout, versement, transfert, débit, remboursement, cash-in/out, confirmation d’OTP — pas une vérification de statut ni une création de bénéficiaire) indique si un nouvel essai est sans danger — son en-tête d’idempotence dans parameters (ou son champ dans input_schema), le comportement dans gotchas, ou une entrée d’open_questions quand la documentation ne dit rien. Un versement réessayé sans cela peut payer deux fois |
environments | sandbox et/ou production, chacun avec une base_url de la forme https://host[:port] (sans chemin, requête ni fragment). L’endpoint.url de chaque spec du fournisseur qui n’est pas un webhook doit commencer par l’une d’elles. Un environnement que le fournisseur ne documente pas est omis — jamais deviné — et son absence va dans les open_questions des specs (une convention, non vérifiée) |
deprecation | reason dit ce que le fournisseur a annoncé, avec sa date quand il en a donné une. replaced_by nomme un autre dossier de capacité du même fournisseur ; removed_after est une date (YYYY-MM-DD) |
removed_capabilities | capability une seule fois par liste, sans dossier restant dans le répertoire du fournisseur ; removed_in est la date du retrait ; reason une chaîne non vide ; alternatives (facultatif) des dossiers de capacité du même fournisseur |
Définitions de capability_type
synchronous— requête/réponse, résultat disponible immédiatement dans la réponse HTTPasynchronous— le fournisseur traite en arrière-plan ; on obtient le résultat en interrogeant périodiquement un endpoint séparéwebhook— le fournisseur envoie un HTTP POST à votre endpoint quand un événement survientsdk— capacité livrée par un script SDK côté client (pas d’endpoint HTTP direct).endpoint.urlcontient l’URL CDN du script SDK ;endpoint.methodvautnull. Lecanonical_example.tsexporte un helper de configuration côté serveur ; l’appel réel du SDK est montré dans le bloc de commentaire d’usage. L’identifiant de connexion va dansauth.location: "sdk_param"(l’objet de configuration du SDK).
canonical_example.ts : règles
Règles obligatoires
- TypeScript strict — doit compiler avec
tsc --noEmit, zéro erreur fetchnatif uniquement — pas d’axios, pas de node-fetch, pas de bibliothèque HTTP externe- Tous les identifiants de connexion via
process.env— vérifiés en haut du fichier, avec une erreur levée s’ils manquent - Interfaces TypeScript explicites pour les types d’entrée, de réponse et d’erreur
- Exporter la fonction principale
- Inclure un exemple d’usage dans un bloc de commentaire en bas
- En-tête JSDoc : fournisseur, capacité, version de l’ATSS, capability_type
- L’expéditeur d’un message (numéro, sender ID, nom d’expéditeur) vient de
process.env.{PROVIDER}_{CHANNEL}_SENDERdans l’exemple d’usage (ex.INFOBIP_WHATSAPP_SENDER), déclaré dansauth.credentials, jamais une valeur littérale : il change entre un expéditeur de test et celui du compte lui-même
Modèle
/**
* @provider Paycard
* @capability create_payment
* @atss 1.0
* @capability_type synchronous
*/
const PAYCARD_API_KEY = process.env.PAYCARD_API_KEY;
if (!PAYCARD_API_KEY) throw new Error("Missing env: PAYCARD_API_KEY");
interface CreatePaymentInput {
amount: number;
currency: string;
reference: string;
callback_url: string;
}
interface CreatePaymentResponse {
id: string;
payment_url: string;
status: string;
}
interface PaycardError {
code: string;
message: string;
}
export async function createPayment(
input: CreatePaymentInput
): Promise<CreatePaymentResponse> {
const response = await fetch("https://mapaycard.com/epay/create/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ...input, c: PAYCARD_API_KEY }),
});
if (!response.ok) {
const error: PaycardError = await response.json();
throw new Error(`Paycard error ${response.status}: ${error.message}`);
}
return response.json() as Promise<CreatePaymentResponse>;
}
/*
Usage example:
const payment = await createPayment({
amount: 100000,
currency: "GNF",
reference: "order_123",
callback_url: "https://myapp.com/callback",
});
// Store payment.operation_reference immediately
// Always verify payment status server-side before fulfilling the order
*/
Ce qui ne doit jamais apparaître dans canonical_example.ts
- Tout import npm (
axios,node-fetch, etc.) —fetchnatif uniquement require()— modules ES uniquement- Des clés d’API ou des secrets codés en dur
- Le type
anysans commentaire expliquant pourquoi
Cycle de vie du statut
unverified → draft → ready → verified → deprecated → archived
unverified : critères
-
schema.jsonpassenpm run validate(structure + bloc evidence) - Bloc
evidenceprésent avecsource_url,source_reference,extracted_at,snapshot_hash, et au moins une entréespans[]ancrée dans la source - Généré automatiquement par le pipeline de discovery (
core/tools/discovery) — les contributeurs n’écrivent pas à la main de specsunverified - Pas encore relu par un humain au-delà des contrôles automatiques — un mainteneur qui relit la PR la promeut en
draft(demande plus de travail) ou enready(semble correcte) en modifiantstatuset en retirant ou en gardantevidencecomme documentation
ready : critères
-
schema.jsonpassenpm run validate -
canonical_example.tscompile avectsc --noEmit -
gotchas[]contient au moins 1 entrée précise et actionnable -
example_promptn’est pas vide -
provider.jsonexiste dans le dossier parent du fournisseur - Le champ
statusest àready
Une spec ready est visible dans le serveur MCP.
verified : critères
- Tous les critères
readysont remplis - L’un des suivants :
npm run test:live -- --provider <slug>réussit (code de sortie 0) —method: "test_live"; son--reportdonne lespec_hashde chaque capacité- Une intégration qui fonctionne :
afrotools/examples, ou une app en usage —method: "app" - Un rapport d’intégration qu’un mainteneur a accepté (voir
CONTRIBUTING.md§ Report an integration) —method: "community_report"
- Le bloc
verificationconsigne la méthode, l’environnement, la date etspec_hash - Un mainteneur a passé le statut à
verified
verified dit que la spec a correspondu à l’API une fois, dans verification.environment. Une spec vérifiée en
sandbox n’a pas été essayée avec des identifiants de production. test:live cible toujours et uniquement un sandbox —
il refuse de s’exécuter contre un fournisseur sans sandbox (trafic de production réel, aucun filet de sécurité
automatique) ; voir CONTRIBUTING.md § Live verification.
AI Ready
AI Ready est une promesse de tout le registre, pas un badge par fournisseur : chaque fournisseur publié
a des specs ATSS à partir desquelles un agent peut réaliser l’intégration sans avoir à analyser des pages de documentation. Cela ne veut pas dire
qu’une spec est sans erreur — une spec peut être fausse tant qu’elle n’est pas testée, ce que le champ status indique à
l’agent. Ne jamais décrire AI Ready comme « vérifié », « testé » ou « prêt pour la production ».
Politique de compatibilité
Des agents maintiennent des intégrations générées à partir de ces specs. Un changement qui rend une telle intégration fausse sans prévenir son agent est cassant (breaking) :
- retirer une capacité : supprimer son dossier, ou repasser une spec servie (
unverified,ready,verified,deprecated) àdraftou àarchived, que le MCP ne sert pas ; - retirer ou renommer un champ de
response_schema; - retirer ou renommer un champ d’
input_schemaou deparameters, y compris un changement de casse ou un déplacement vers un autre endroit du corps. Un seul déplacement n’est pas cassant : une propriété de premier niveau d’input_schemaqui devient un paramètre du même nom, ou l’inverse (la migration ATSS 1.1) — l’agent lit le nouvel emplacement dans la spec. Seuls les noms d’en-tête ignorent la casse.
Changer le type d’un champ, rendre obligatoire une entrée facultative ou restreindre un enum casse aussi les intégrations ; le relecteur en juge. Ajouter une capacité, un champ facultatif, un gotcha ou une question ouverte, ou corriger une description, n’est pas cassant.
scripts/spec-diff.js compare les specs d’une PR à main et liste les capacités retirées et les
champs retirés ou renommés (un renommage apparaît comme un retrait), et, à titre d’information, les champs de requête
qui ont bougé. La CI échoue quand elle trouve un retrait, sauf si la PR porte le label breaking : un mainteneur
le pose une fois le retrait voulu, puis relance le job.
Quand le fournisseur déprécie une capacité (annonce sa fin, ou un remplaçant) :
- Passez
statusà"deprecated"avec un blocdeprecation(spec_version: "1.1") :reason, etreplaced_by/removed_afterquand ils sont connus. Le MCP continue de servir la spec, avec un avertissement qui nommereplaced_by. - Une fois que le fournisseur l’a arrêtée (ou après
removed_after), supprimez le dossier et ajoutez un tombstone (une entrée qui garde la trace de la capacité retirée) àremoved_capabilitiesdansprovider.json.get_specrépond alors par une erreur portantremoved_in,reasonetalternatives, au lieu d’un simple « introuvable ». Cette PR a besoin du labelbreaking.
Quand le registre retire une capacité que le fournisseur propose toujours (une réécriture, une spec qui n’aurait
pas dû être écrite) : supprimez le dossier, avec le label breaking, sans statut deprecated
ni tombstone — get_spec répond avec les capacités restantes du fournisseur. Exemple : les 25
capacités Flutterwave que afrotools/afrotools#94 a retirées.
Gotchas : guide d’écriture
Bien :
"Always verify payment status server-side before fulfilling an order.
The callback URL alone is not sufficient — it can be forged."
Mal :
"Read the docs carefully."
Les gotchas doivent être : précis, actionnables, fondés sur une vraie expérience d’intégration.
Index des fournisseurs
Voir le tableau généré automatiquement dans README.md — il est régénéré par la CI
(scripts/generate-readme.js) à chaque fusion dans main et n’est jamais désynchronisé.
Ce fichier gardait autrefois une seconde copie de ce tableau, tenue à la main ; elle a divergé
de la réalité en quelques semaines et ne vaut pas la peine d’être réintroduite ici.