Documentation API
Tout ce qu'un logiciel client peut faire avec Smart-Evolution : enregistrer une machine, gérer ses licences, charger sa configuration et son code, récupérer des clés de chiffrement, un coffre-fort de secrets zéro-connaissance, et le self-service du portail. Sans mot de passe : chaque machine s'authentifie avec sa propre clé.
https://app.smart-evolution.com/apiIntroduction
L'API permet à une application cliente (le logiciel installé chez votre client) d'interagir avec la plateforme : licences, configuration d'équipement, code source, plugins et chiffrement. Le modèle est l'enrôlement machine : chaque poste est inscrit une fois et reçoit sa propre clé.
À gauche, les fonctions sont rangées par thème. Cliquez sur une section pour dérouler ses fonctions.
Configurer un poste
Comment un poste s'authentifie et se provisionne de façon transparente — aucun jeton manuel côté utilisateur.
- Connexion utilisateur (OIDC). App enregistrée comme client public (
is_public: true, sans secret). Flow authorization code + PKCE (S256), Authorityhttps://app.smart-evolution.com(auto-découverte/.well-known). Redirect loopback à port dynamique : enregistrer une foishttp://127.0.0.1/callback/(port ignoré). Scopesopenid profile email license. - Au login, l'
id_tokenporte déjà l'identité (email,name), l'organisation (org_id,org_name, tableauorgsavec rôle) et la licence (plan,entitlements,valid_until,licensed) — exploitable hors-ligne, sans appel supplémentaire. - Enrôlement automatique du poste. Si aucune clé machine locale :
POST /api/portal/machines/enrollavecAuthorization: Beareret un corps{ machine_uid, name }. Stocker lamachine_keyrenvoyée (keystore).status: pending→ afficher « en attente d'approbation ». - À l'exécution — deux secrets.
X-Machine-Key: dvk_…pour tout ce qui est machine (licences, config, manifeste de plugins, chiffrement) ;Authorization: Bearerpour les appels au nom de l'utilisateur. - Bon à savoir. Le client peut épingler la version de plugin installée (portail → Téléchargements) ; le loader installe exactement la version du manifeste (downgrade inclus). Le logiciel peut joindre une capture d'écran à un incident (
screenshot_base64).
Authentification
Trois façons de s'authentifier selon le contexte :
| Secret | En-tête / mécanisme | Usage |
|---|---|---|
enr_… jeton d'enrôlement | X-Enroll-Token | Une fois, pour inscrire une machine |
dvk_… clé machine | X-Machine-Key | Tous les appels d'un poste (licences, équipement, chiffrement) |
| Utilisateur du portail | Cookie de session (code courriel) | Le self-service du client (portail) |
Le jeton d'enrôlement est fourni au client depuis son portail. La clé machine est renvoyée une seule fois, à l'enrôlement. Les endpoints admin (staff) utilisent une session 2FA.
Enregistrement machine
Inscrire un poste et obtenir sa clé. À faire une seule fois, à l'installation.
Enrôler une machine
/api/license/enrollX-Enroll-TokenInscrit un poste et renvoie sa clé propre. Ré-enrôler avec le même machine_uid réémet la clé sur la même fiche (pas de doublon).
{
"machine_uid": "<empreinte matérielle stable>",
"name": "PC-USINE-07",
"location": "Usine Saint-Jean",
"asset_tag": "INV-1042"
}{
"machine_id": "uuid",
"machine_key": "dvk_…", // à stocker — affiché une seule fois
"status": "active" // ou "pending" si approbation requise
}Vérifier un jeton d'enrôlement
/api/license/enroll-token/checkX-Enroll-TokenVérifie sans effet de bord qu'un jeton est valide (n'enrôle rien). Utile pour confirmer qu'un jeton régénéré est bien inactif.
{ "valid": true, "client": "Acme inc." } // sinon { "valid": false, "client": null }Licences
Consommer un siège dans le pool du client. Mode flottant (concurrent, libération auto) ou nominatif (siège fixe par utilisateur).
Prendre une licence
/api/license/checkoutX-Machine-KeyPrend un siège dans le pool du produit. Idempotent : relancé sur la même machine, renvoie le même siège.
{
"product_code": "mon-produit",
"user": "marc@acme.com" // optionnel (mode nominatif)
}{
"seat_id": "uuid",
"lease_token": "…",
"expires_at": "2026-06-24T16:00:00Z",
"product": "Mon produit",
"mode": "floating",
"valid_until": "2027-01-01"
}Renouveler le bail
/api/license/heartbeatX-Machine-KeyProlonge le bail. À envoyer périodiquement (ex. toutes les 1–4 h). Sans heartbeat pendant reset_hours (24 h), le siège est auto-libéré.
{ "seat_id": "uuid", "lease_token": "…" }{ "expires_at": "2026-06-24T20:00:00Z", "valid_until": "2027-01-01" }Libérer la licence
/api/license/releaseX-Machine-KeyRend le siège au pool immédiatement. À appeler à la fermeture du logiciel.
{ "seat_id": "uuid", "lease_token": "…" }204 No Content
État de licence
/api/license/status?product_code=mon-produitX-Machine-KeyÉtat pour ce produit. 200 même sans licence (has_license:false) ; 404 uniquement si le product_code est inconnu.
{ "product": "Mon produit", "has_license": true, "seats_total": 5, "seats_used": 3,
"seats_available": 2, "valid_until": "2027-01-01", "is_active": true }
// sans licence → 200 { "has_license": false, "is_active": false, "reason": "no_entitlement" }
// produit inconnu → 404 { "error": "product_not_found" }Licences de l'organisation
/api/org/licensesJeton OIDC utilisateurLes licences actives de l'organisation de l'utilisateur connecté + leur occupation. S'authentifie avec le jeton OIDC utilisateur (Authorization: Bearer). Un délégué (admin d'org) voit la validité et les détenteurs de chaque siège ; tout autre utilisateur voit l'usage seul (nombres), sans identités ni validité. Utilisez le champ delegate de la réponse pour adapter l'affichage.
// délégué
{ "delegate": true, "licenses": [ {
"product_code": "evocore", "product_name": "EvoCore Inventory", "mode": "floating",
"seats": 5, "seats_used": 1, "seats_available": 4,
"valid_from": null, "valid_until": "2027-01-01",
"holders": [ { "machine_name": "Poste-1", "asset_tag": "A1", "user_label": "Alice", "last_seen_at": "…" } ] } ] }
// non-délégué → { "delegate": false, "licenses": [ { …, "holders": [], "valid_until": null } ] }Boutique — catalogue vitrine
/api/store/catalogJeton OIDC utilisateurTous les produits visibles pour l'org — possédés ET non possédés (actifs publics + ceux que l'org détient) — avec le contenu marketing pour la page « Découvrir ». Auth jeton usager, portée org auto (comme /api/org/licenses). owned = l'org a une licence ; trialable = un essai est offert pour l'utilisateur courant. product_code identique au plugin_key.
{ "products": [ {
"product_code": "evocore.admincrypt", "name": "EvoCore.AdminCrypt", "publisher": "Smart-Evolution",
"category_key": "securite", "category_label": "Sécurité", "category_label_en": "Security",
"description": "…HTML léger…", "description_en": "…", "tags": ["chiffrement","escrow"],
"pricing": { "model": "subscription", "price": 5.0, "currency": "CAD", "period": "month" },
"icon_url": "<url signée ou lucide:nom>", "screenshots": ["<url signée>"],
"product_url": "https://app.smart-evolution.com/produits/admincrypt",
"owned": true, "trialable": false, "trial_days": 0,
"latest_version": "1.4.0", "install_count": 128 } ] }model ∈ free|paid|subscription ; period ∈ month|year|once. owned est au niveau org ; trialable est calculé pour l'utilisateur courant (essai par utilisateur).
Boutique — démarrer un essai
/api/store/trialsJeton OIDC utilisateurAccorde à l'utilisateur son essai d'un produit (automatique quand trial_days > 0). L'essai est PAR UTILISATEUR : chaque usager de l'org a droit au sien. L'accès reste org-wide — une fois l'essai lancé, le produit apparaît dans GET /api/device/plugins jusqu'à valid_until, puis disparaît (filtre de validité). Miroir : POST /api/store/trials/release { product_code } pour annuler.
{ "product_code": "evocore.admincrypt" }200 → { "product_code": "…", "trial": true, "valid_until": "2026-08-15", "seats": 1 }
// erreurs (shape { "error": … }) :
409 { "error": "already_owned" } // l'org possède déjà une licence
409 { "error": "trial_already_used" } // cet utilisateur a déjà utilisé son essai
403 { "error": "not_trialable" } // produit sans essai (trial_days = 0)
404 { "error": "unknown_product" }Durée = trial_days du produit. À l'expiration, auto-libéré par la date (pas d'action client). Un 2ᵉ utilisateur peut lancer son propre essai même si un 1ᵉʳ l'a fait.
Équipement & code
Ce que le poste charge au démarrage : sa configuration, son code source assigné et le manifeste des plugins sous licence.
Charger la configuration
/api/device/configX-Machine-KeyConfiguration courante de l'équipement (valeurs + schéma typé), telle que réglée dans la plateforme.
{ "label": "prod", "values": { "luminosite": 80 },
"fields": [ { "key": "luminosite", "type": "int" } ] }Charger le code source
/api/device/sourceX-Machine-KeyCode source assigné à l'équipement (ou partagé au client), version courante incluse.
[ { "id": "uuid", "title": "Boot", "language": "python", "version": "1.0", "content": "…" } ]Manifeste de plugins
/api/device/pluginsX-Machine-KeyLes plugins-produits sous licence active du client PLUS la clôture transitive de leurs dépendances (les librairies partagées, Type=Library, non-licenciées, arrivent aussi). Chaque entrée porte PluginKey (= code produit, identifiant stable) et Dependencies. IconBaseUrl + PluginIconUrl et PluginBaseUrl + PluginFile. Chaque version porte aussi Sha256 (intégrité) et Signature (authenticité). Champs boutique additifs (ignorez l'inconnu) : CategoryKey + CategoryLabel/_en (taxonomie stable), Tags[], Publisher, Description/_en (peut contenir du HTML), Pricing { Model: free|paid|subscription, Price, Currency, Period }, InstallCount, Screenshots[] (URLs signées), ReleaseNotes (notes de la version servie).
{
"IconBaseUrl": "…/icon/", "PluginBaseUrl": "…/plugins/",
"Plugins": [
{ "PluginName": "EvoCore Inventory", "PluginKey": "evocore-inventory",
"PluginFile": "evocore-inventory.dll", "Sha256": "3a7f9c…", "Signature": "eyJhbGciOiJSUzI1NiJ9…",
"MajorVersion": 2, "MinorVersion": 1, "BuildVersion": 0,
"TypePlugin": "Plugin", "MinVersion": 5,
"Dependencies": [ { "Name": "MBA Data", "Key": "mbadata", "MinVersion": 10203 } ] },
{ "PluginName": "MBA Data", "PluginKey": "mbadata", "PluginFile": "mbadata.dll",
"MajorVersion": 1, "MinorVersion": 2, "BuildVersion": 3,
"TypePlugin": "Library", "MinVersion": 5, "Dependencies": [],
"CategoryKey": "data", "CategoryLabel": "Données", "Tags": ["etl"],
"Publisher": "Smart-Evolution", "Pricing": { "Model": "subscription", "Price": 15.0, "Currency": "cad", "Period": "month" },
"InstallCount": 42, "Screenshots": ["https://cdn…"], "ReleaseNotes": "Nouveautés v1.2.3" }
]
}Dependencies[] = les modules requis : { Name (affichage), Key (= PluginKey du module cible, joignez là-dessus), MinVersion }. MinVersion est un entier compact major*10000 + minor*100 + build (ex. 10203 = 1.2.3 ; 0 = toute version). La clôture étant calculée côté serveur, le poste n'a qu'à trier les arêtes en ordre topologique puis télécharger. Une librairie-dépendance se télécharge sans siège de licence — être dans le manifeste = être autorisé. La version servie par plugin est la courante, sauf si le client en a épinglé une (POST /api/portal/plugins/{product_id}/version). Vérification : recalcule le SHA-256 du binaire téléchargé (doit == Sha256, intégrité), puis vérifie Signature — un JWS RS256 liant code+version+fichier+hash — contre /.well-known/jwks.json comme un token OIDC (claims purpose=plugin-attestation, sha256, code, ver ; authenticité + anti-substitution).
Chiffrement
Un fichier est chiffré par un DEK déposé dans plusieurs slots (logique OU) : mot de passe, org assisté (récupération à distance sur approbation) et, en option, org hardware (privée sur YubiKey). Le serveur ne détient que du chiffré qu'il ne peut pas ouvrir.
Clés actives de l'organisation
/api/org/keys/activeX-Machine-KeyToutes les clés actives — un slot par clé (assisté + hardware).
[ { "keyId": "…", "role": "assisted", "algorithm": "RSA-3072-OAEP-SHA256", "publicKeySpki": "…" } ]Demander une récupération
/api/org/recover/requestsX-Machine-KeyDemande de récupération assistée. Le poste envoie une clé publique éphémère (canal de retour).
{ "keyId": "…", "wrappedDek": "…", "clientPubKey": "…", "reason": "…", "fileRef": "…" }{ "requestId": "…", "status": "pending" }Récupérer la clé approuvée
/api/org/recover/requests/{id}X-Machine-KeyRécupère le DEK une fois approuvé (chiffré pour le poste). Disponible 1 h puis effacé.
{ "status": "fulfilled", "dekEncryptedForClient": "…" }Enregistrer la clé admin
/api/org/{orgId}/admin-keyAdmin · session (2FA)Enregistre la clé publique admin (une fois) : elle emballe les clés privées d'org au repos.
{ "adminPublicKeySpki": "…" }Créer une clé d'organisation
/api/org/{orgId}/keysAdmin · session (2FA)'assisted' sans publicKeySpki → le serveur génère + emballe sous la clé publique admin, puis détruit le clair. 'hardware' → publicKeySpki fourni (privée sur YubiKey).
{ "role": "assisted", "makeCurrent": true }{ "keyId": "…", "role": "assisted", "publicKeySpki": "…", "status": "current" }Demandes en attente (admin)
/api/org/{orgId}/recover/requests?status_filter=pendingAdmin · session (2FA)Les demandes en attente (avec wrappedDek + clientPubKey) pour la console d'approbation.
[ { "id": "…", "keyId": "…", "wrappedDek": "…", "clientPubKey": "…", "reason": "…" } ]Approuver une récupération (admin)
/api/org/{orgId}/recover/requests/{id}/fulfillAdmin · session (2FA)Poste le DEK rechiffré pour le poste (sous clientPubKey). Déballage/déchiffrement faits hors-ligne sur la console.
{ "dekEncryptedForClient": "…" }Voir aussi …/reject et POST /api/org/{orgId}/keys/{keyId}/revoke.
EvoVault (coffre-fort)
Un coffre-fort de secrets zéro-connaissance : le serveur stocke et synchronise des octets opaques (contenu et clés emballées) qu'il ne peut pas ouvrir. Chaque secret est chiffré par une DEK déposée dans un slot par destinataire (emballée sous sa clé publique RSA). Partage, révocation et recherche des titres se font côté client. Authentifié par la session portail ; tout est cloisonné à l'organisation.
Publier ma clé
/api/vault/keys/meCookie sessionPremière configuration (ou rotation de passphrase) : publie la clé publique de l'utilisateur et sa clé privée EMBALLÉE sous sa passphrase maître (jamais connue du serveur).
{ "public_key_spki": "…", "wrapped_private_key": "…" }204 No Content
Ma clé
/api/vault/keys/meCookie sessionRécupérée sur chaque appareil ; le client déballe la clé privée localement avec la passphrase. 404 si pas encore configurée.
{ "public_key_spki": "…", "wrapped_private_key": "…", "updated_at": "…" }Annuaire des membres
/api/vault/membersCookie sessionLes utilisateurs de l'organisation et leur clé PUBLIQUE, pour choisir un destinataire de partage. Aucune clé privée ici.
[ { "user_id": "…", "email": "…", "name": "…", "public_key_spki": "…", "has_key": true } ]Mes coffres
/api/vault/vaultsCookie sessionCoffres visibles : personnels (propriétaire) + partagés de l'org où l'utilisateur détient un slot de coffre. POST crée {enc_name, kind:'personal'|'shared'} ; PATCH /{id} renomme ; DELETE /{id} supprime (propriétaire ou délégué). enc_name (nom) est opaque, chiffré sous la DEK de coffre. Chaque coffre renvoie ses slots (le membre y retrouve le sien pour déballer la DEK).
[ { "id": "…", "enc_name": "…", "kind": "shared", "owner_user_id": "…", "created_at": "…",
"slots": [ { "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_vault_key": "…" } ] } ]Synchroniser les items
/api/vault/vaults/{id}/items?since={seq}&include_deleted=trueCookie sessionDelta de synchronisation : les items dont seq > since (tombstones inclus si demandé). Le client mémorise max_seq par coffre. seq est un compteur monotone par coffre, réassigné à chaque écriture — il sert de curseur de sync ET de jeton de concurrence.
{ "items": [ { "id": "…", "type": "password", "enc_title": "…", "seq": 42, "is_deleted": false,
"enc_blob": "…", "slots": [ { "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_dek": "…" } ] } ],
"max_seq": 42 }Créer un item
/api/vault/itemsCookie sessionCrée un secret. ≥ 1 slot obligatoire. Le serveur ne valide PAS le contenu chiffré ; il vérifie l'accès au coffre et l'intégrité structurelle (base64, key_id = 16 hex). enc_title et enc_blob sont opaques.
{ "vault_id": "…", "type": "password", "enc_title": "…", "enc_blob": "…",
"slots": [ { "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_dek": "…" } ] }{ "id": "…", "seq": 42 }Modifier un item
/api/vault/items/{id}Cookie sessionRemplace enc_blob + la liste complète des slots ; archive l'ancien contenu dans l'historique. Concurrence optimiste : base_seq = le seq sur lequel le client s'est basé.
{ "enc_title": "…", "enc_blob": "…", "slots": [ … ], "base_seq": 42 }{ "seq": 43 }Partager (ajouter un slot)
/api/vault/items/{id}/slotsCookie sessionAccorde l'accès à un destinataire : le client déballe la DEK avec SA clé privée puis la ré-emballe sous la clé publique du destinataire. Réservé au créateur de l'item, au propriétaire du coffre ou à un délégué.
{ "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_dek": "…" }{ "key_id": "…", "seq": 44 }Révoquer un accès
/api/vault/items/{id}/slots/{key_id}Cookie sessionRetire le slot (révocation immédiate côté serveur). La sécurité descendante réelle vient d'une rotation de DEK côté client : régénérer la DEK, re-chiffrer enc_blob, ré-emballer les slots restants, puis PUT de l'item.
204 No Content
Aussi : dossiers (/api/vault/vaults/{id}/folders), tags (/api/vault/vaults/{id}/tags), favoris (POST /api/vault/items/{id}/favorite), historique (GET /api/vault/items/{id}/history · POST …/restore). Récupération d'urgence : un slot kind='org' emballé sous la clé publique d'org, débloqué via /api/org/recover/*. Pièces jointes : hors API (stockage S3 configuré par l'organisation).
Portail client (self-service)
Ce qu'une personne du client fait après connexion (code à usage unique par courriel → cookie de session). Un délégué gère l'équipe et les permissions par module (aucun / voir / gérer) et par portée (ses propres / tous).
Demander un accès
/api/portal/auth/request-accessPublicAuto-demande d'accès (le domaine du courriel doit correspondre à un client connu). Réponse générique.
{ "email": "jane@acme.com", "name": "Jane Doe" }Mon profil
/api/portal/meCookie sessionL'utilisateur portail connecté (client, nom).
Mes permissions
/api/portal/access/meCookie sessionModules + grants effectifs (niveau + portée).
Mes projets
/api/portal/projectsCookie sessionProjets du client (restreints aux projets autorisés).
Mes communications
/api/portal/communicationsCookie sessionJournal des échanges. Portée ses propres / tous selon la permission.
Ouvrir un incident
/api/portal/incidentsCookie sessionOuvre un ticket. La liste GET /incidents applique la portée ses propres / tous. Champ optionnel screenshot_base64 : le logiciel client peut joindre une capture d'écran (base64 ou data: URI, ≤ ~6,5 Mo) — visible par le support.
{ "title": "…", "severity": "medium", "description": "…", "screenshot_base64": "data:image/png;base64,iVBORw0K…" }Approuver une soumission
/api/portal/soumissions/{id}/approveCookie sessionApprouve une soumission (fige le devis → lead Vendu).
Mes documents
/api/portal/documentsCookie sessionDocuments du client. Téléchargement via /documents/{id}/download (URL présignée).
Mes postes / licences
/api/portal/machinesCookie sessionPostes enrôlés. PATCH pour approuver/révoquer, DELETE pour retirer un poste révoqué.
Enrôler ce poste (sans jeton)
/api/portal/machines/enrollCookie session / Bearer OIDCAuto-enrôlement du poste avec l'identité de l'utilisateur connecté — AUCUN jeton d'enrôlement requis. Le poste est rattaché au client de l'utilisateur ; dédup par machine_uid (ré-enrôler réémet la clé sur la même fiche). Renvoie la clé machine UNE fois. Si le client exige l'approbation, statut 'pending' jusqu'à validation admin. Idéal pour une 1re expérience transparente : l'utilisateur se connecte, et si le poste n'a pas de clé, le logiciel appelle ceci une fois avec un machine_uid stable + un nom (ex. le hostname).
{ "machine_uid": "<empreinte matérielle stable>", "name": "PC-USINE-07" }{ "machine_id": "…", "machine_key": "dvk_…", "status": "active" } // ou "pending" si approbation requiseMes téléchargements & versions
/api/portal/downloadsCookie sessionProduits sous licence : standalone → lien + version ; saas → lien ; plugin → livré aux postes. Pour les plugins, renvoie aussi les versions disponibles et la version épinglée par le client (null = version courante).
[ { "product_id": "…", "name": "…", "distribution": "plugin", "download_url": null,
"versions": [ { "id": "…", "label": "2.1.0", "is_current": true } ], "pinned_version_id": null } ]Choisir la version d'un plugin
/api/portal/plugins/{product_id}/versionCookie session · licences (gérer)Épingle la version que les postes du client installent (null = revient à la version courante). S'applique à toutes les machines du client ; le manifeste /api/device/plugins sert alors la version épinglée. Réservé au module licences niveau gérer (délégués inclus).
{ "version_id": "…" } // ou { "version_id": null } pour retirer l'épingleJeton d'enrôlement
/api/portal/enrollment-tokenCookie session · déléguéRégénère le jeton d'enrôlement (enr_…) — renvoyé une seule fois.
Mon code source
/api/portal/source-codesCookie sessionCode source hébergé du client. POST /{id}/versions pour sauvegarder si accès lecture/écriture.
Approuver une étude de cas
/api/portal/case-studies/{id}/approveCookie session · déléguéLe délégué approuve la publication d'une étude de cas le concernant.
Gérer mon équipe
/api/portal/teamCookie session · déléguéLe délégué crée un employé (courriel du domaine de l'entreprise) puis règle ses permissions.
{ "email": "bob@acme.com", "first_name": "Bob" }Demandes agent (soumettre + suivre)
/api/portal/agent-requestsCookie session · module agentsSoumet une demande exécutée par les agents Smart-Evolution. client_ref = l'identifiant du ticket dans VOTRE CRM : re-soumettre le même client_ref met à jour (upsert idempotent, retries sûrs). GET liste vos demandes avec ?client_ref= (recherche) et ?updated_since= (synchronisation delta). Statuts à mapper dans votre CRM : queued → assigned → in_progress → blocked → done | failed.
{ "title": "Analyser nos données", "description": "…", "client_ref": "CRM-42", "priority": 0 }{ "id": "…", "client_ref": "CRM-42", "status": "queued", "deliverable": null, "updated_at": "…", "cost": 0 }Aussi : GET /agent-requests/{id} (détail + livrable + coût), GET /agent-requests/{id}/events (chronologie), GET /agent-initiatives (budgets), GET /agent-incidents + POST /{id}/comment (blocages sur vos demandes). Accès machine : votre CRM s'authentifie en OIDC (jeton d'organisation) — mêmes endpoints. Isolation stricte par organisation (hors org = 404).
Authentification (OIDC / SSO)
Smart-Evolution est un fournisseur d'identité OpenID Connect : tes outils (web, .NET, Python, IoT) authentifient les utilisateurs contre app.smart-evolution.com — une seule identité partout. Flow authorization code + PKCE, jetons signés RS256, clé publique exposée via JWKS. Chaque outil lit l'URL de découverte et se configure seul.
Point d'entrée : Authority = https://app.smart-evolution.com. La découverte et le JWKS sont à la racine du domaine — /.well-known/openid-configuration et /.well-known/jwks.json — pas sous /api (seuls /authorize, /token, /userinfo le sont). Pointez votre librairie sur l'Authority et laissez l'auto-découverte câbler les endpoints.
L'access_token (et l'id_token) portent les claims de licence — plan, entitlements, valid_until, licensed — et, pour un utilisateur client, son organisation : org_id + org_name, plus le tableau orgs — [ { id, name, role } ] avec role = admin (délégué) ou member — relu à chaque rotation de token, pour provisionner/lier l'organisation côté consommateur (ex. NOVUS) — et licenses : l'ensemble effectif des services plateforme licenciés à l'organisation, [ { code, limit? } ] (codes produits du catalogue ; limit = total des sièges de l'org, absent = illimité/booléen) — et, si l'organisation en a un, llm_budget : son plafond LLM total, { amount, currency: "CAD", window: "month" }, sous-réparti côté consommateur. Chaque service sait ainsi à quoi l'utilisateur a droit et de quelle organisation il relève.
Accès API : passez l'access_token en Authorization: Bearer sur n'importe quel endpoint /api/… — l'API agit au nom de l'utilisateur, avec ses permissions exactes (staff → selon le rôle ; client → cloisonné à son entreprise + droits délégué/module), en lecture comme en écriture. Seul l'access_token (qui porte scope) est accepté, pas l'id_token.
iss, aud, exp) — aucun secret partagé.Découverte OIDC
/.well-known/openid-configurationPublicDocument de découverte, à la RACINE du domaine (pas sous /api). Chaque librairie lit cette URL et se configure seule (endpoints, algos, scopes).
{ "issuer": "https://app.smart-evolution.com", "authorization_endpoint": ".../api/oidc/authorize", "token_endpoint": ".../api/oidc/token", "userinfo_endpoint": ".../api/oidc/userinfo", "device_authorization_endpoint": ".../api/oidc/device_authorization", "jwks_uri": ".../.well-known/jwks.json", "grant_types_supported": ["authorization_code","refresh_token","client_credentials","machine_key","urn:ietf:params:oauth:grant-type:device_code"], "code_challenge_methods_supported": ["S256"] }Clés publiques (JWKS)
/.well-known/jwks.jsonPublicClés publiques de signature (RS256), à la RACINE du domaine (pas sous /api). Les services vérifient les jetons hors-ligne avec ces clés (par kid).
{ "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "…", "n": "…", "e": "AQAB" } ] }Remonter l'usage LLM (NOVUS→SE)
/api/llm/usageBearer client_credentials · scope llm.usageService-à-service : NOVUS remonte le coût LLM par organisation pour réconciliation/facturation (SE reste l'autorité de financement, NOVUS le gate temps réel). Idempotent par request_id ; accepte une entrée OU un lot. Alimente un registre durable + le miroir de dépense de l'org (CAD).
[ { "request_id": "…", "org_id": "<client id>", "model": "claude-…", "cost_cad": 0.42, "tokens_in": 1200, "tokens_out": 800, "sector_id": "…?", "player_id": "…?" } ]{ "received": 3, "recorded": 2, "duplicates": 1 }Échanger le code / rafraîchir
/api/oidc/tokenClient (secret ou PKCE)Échange le code contre les jetons (confidentiel : client_secret ; public : code_verifier). L'access_token porte les claims de licence (plan, entitlements, valid_until, licensed).
grant_type=authorization_code&code=…&redirect_uri=…&client_id=cli_…&client_secret=…&code_verifier=…
{ "access_token": "eyJ…", "id_token": "eyJ…", "refresh_token": "…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile email license" }Aussi acceptés : grant_type=refresh_token (rotatif — l'ancien est invalidé à chaque usage) et client_credentials (service-à-service : le client confidentiel obtient un JWT ; s'il est lié à un agent, le jeton porte agent_id/secteur/rôle/initiatives), ainsi que grant_type=machine_key (hôtes EvoCore, sans navigateur : la clé machine enrôlée s'échange contre un Bearer 1 h — sub=machine:<uuid>, aud=novus-api, claims orgs + licenses ; pas de refresh token, l'hôte ré-échange à l'expiration ; révoquer la machine coupe l'émission).
Brancher un poste (device flow, RFC 8628)
/api/oidc/device_authorizationClient public (client_id, sans secret)Enrôler un poste sans clavier/navigateur par QR. Le poste appelle device_authorization, affiche user_code + un QR de verification_uri_complete, puis interroge /token. L'usager (déjà connecté sur son téléphone) ouvre le QR, approuve sur /device → le poste reçoit les MÊMES jetons que l'auth-code (access + refresh, claims org), à usage unique, et s'auto-enrôle via POST /api/portal/machines/enroll.
POST /api/oidc/device_authorization client_id=cli_…&scope=openid
{ "device_code": "…", "user_code": "WDJB-MDN7",
"verification_uri": "https://app.smart-evolution.com/device",
"verification_uri_complete": "https://app.smart-evolution.com/device?code=WDJB-MDN7",
"expires_in": 600, "interval": 5 }
// Polling — POST /api/oidc/token :
// grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=…&client_id=cli_…
// en attente → 400 { "error": "authorization_pending" } | { "error": "slow_down" }
// approuvé → 200 { "access_token": "eyJ…", "refresh_token": "…", "id_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600 }Client public : aucun secret ; le device_code est le secret pour le polling. Respectez interval (5 s) et slow_down. Le QR encode verification_uri_complete (déjà pré-rempli avec le user_code, alphabet sans voyelles ni 0/O/1/I). Le jeton obtenu est identique au flow navigateur → accepté tel quel par /api/portal/machines/enroll. Découverte : device_authorization_endpoint + grant urn:ietf:params:oauth:grant-type:device_code annoncés.
Profil (userinfo)
/api/oidc/userinfoBearer access_tokenIdentité de base à partir de l'access_token.
{ "sub": "…", "email": "…", "name": "…", "org_id": "…", "org_name": "…",
"orgs": [ { "id": "…", "name": "…", "role": "admin|member" } ],
"licenses": [ { "code": "vm", "limit": 5 }, { "code": "storage" } ] } // org_* / orgs / licenses présents pour les utilisateurs client (portail)Déconnexion (SSO)
/api/oidc/logoutPublic · navigateurFerme la session IdP (cookie + révocation) puis redirige vers un post_logout_redirect_uri enregistré. Query : post_logout_redirect_uri, id_token_hint (ou client_id), state.
Le post_logout_redirect_uri doit être enregistré sur le client, sinon 400.
Applications enregistrées
/api/oidc/clientsAdmin · sessionListe des clients OIDC (outils enregistrés). Gérable dans Administration → Applications (OIDC).
Enregistrer une application
/api/oidc/clientsAdmin · sessionEnregistre un outil. Le client_secret est renvoyé une seule fois (clients confidentiels).
{ "name": "Portail EvoKPI", "redirect_uris": ["https://…/signin-oidc"], "is_public": false, "product_code": "evokpi" }{ "client_id": "cli_…", "client_secret": "…", "client": { "id": "…", "is_active": true } }Modifier / secret / supprimer
/api/oidc/clients/{id}Admin · sessionÉdite (redirect_uris, scopes, product_code, rôles, actif). POST /{id}/secret régénère le secret ; DELETE /{id} supprime.
product_code relie l'outil à un produit : valid_until et licensed sont alors calculés pour ce produit.
Agents (orchestration)
app.smart-evolution.com est le poste de commandement d'un système multi-agents (l'exécution vit dans NOVUS). Un agent = un employé avec une clé API, cadré à son secteur et ses initiatives : il tire ses tâches, remonte leur état, délègue à son sous-arbre et peut lancer des agents d'appoint.
Mon profil d'agent
/api/agent/meX-API-Key (agent)L'agent courant : secteur, rôle, manager, initiatives.
{ "id": "…", "role": "worker", "sector_id": "…", "initiatives": ["…"] }Fiches produit (éditorial agent)
/api/agent/products/{id}X-API-Key (agent) · capacité productsSurface éditoriale des produits pour agents, gouvernée par le connecteur products (read | write | media | docs — baseline secteur ou grant agent). GET /api/agent/products liste les produits actifs ; PATCH ne touche QUE les champs éditoriaux (description, public_description, public_description_en, app_type, app_type_en) ; POST /{id}/media/image téléverse un screenshot ; POST /{id}/documents/upload téléverse un manuel/PDF (multipart : title, title_en, kind, version, visibility) ; GET /{id}/media et /{id}/documents listent. Stockage Bunny partagé avec le staff (une seule liste) ; pas de suppression côté agent. Le reste du catalogue (prix, distribution, versions…) reste réservé owner/staff.
{ "public_description": "<p>…FR…</p>", "public_description_en": "<p>…EN…</p>" }Support client (helpdesk agent)
/api/agent/support/incidents/{id}/replyX-API-Key (agent) · capacité supportTraitement des incidents CLIENTS (CRM/SLA) par agents, connecteur support (read | respond | status). Lecture org-wide : GET /api/agent/support/incidents (?status_id=, ?client_id=, ?assigned=me) et GET /{id} avec le fil complet (communications, notes internes incluses). Agir exige d'être ASSIGNÉ (assigned_user_id) : POST /{id}/reply crée une PROPOSITION de réponse (note interne « [Proposition d'agent] » — un humain approuve et envoie le courriel, rien ne part directement au client) ; PATCH /{id}/status change le statut (statuts configurables, resolved_on synchronisé pour le SLA). Tout est journalisé dans l'audit agent.
{ "body": "Bonjour, voici la marche à suivre…", "subject": "Re: …" }Annuaire mesh (NOVUS→SE)
/api/agent/directoryBearer client_credentials · scope agents.directoryL'annuaire des agents actifs pour le mesh NovusBus (server-à-server : NOVUS le proxifie via ses outils MCP mesh.directory / mesh.resolve avec un cache court, et superpose la présence live). mesh_address est la clé de routage canonique : l'UUID immuable de l'agent (== claim agent_id des JWT agents). name / description / novus_ref sont éditables par le staff — ne jamais router dessus.
[ { "mesh_address": "…uuid…", "name": "…", "description": "…", "novus_ref": "…",
"sector": "…", "role": "worker", "status": "active",
"initiatives": [ { "id": "…", "name": "…", "sector_role": "…" } ] } ]Ma file de tâches
/api/agent/tasksX-API-Key (agent)Les tâches qui me sont assignées ou dans mes initiatives. Filtre optionnel ?status_filter=.
Remonter l'état
/api/agent/tasks/{id}X-API-Key (agent)Change le statut d'une tâche de mon périmètre (queued|assigned|in_progress|blocked|done|failed). Si une lease est active, présenter son lease_id (fencing → 409 si périmée). Répercuté sur la tâche CRM liée.
{ "status": "in_progress", "lease_id": "ls_…", "note": "…" }Réserver une tâche (lease)
/api/agent/tasks/{id}/claimX-API-Key (agent)Prend une réservation exclusive et limitée dans le temps (visibility timeout, 120 s). N'importe quel membre de l'initiative peut réserver → permet le failover d'une tâche orpheline. 409 si déjà réservée ou si l'agent est en drain.
{ "lease_id": "ls_…", "lease_expires_at": "…" }Prolonger la réservation
/api/agent/tasks/{id}/heartbeatX-API-Key (agent)À envoyer ~toutes les 60 s pour garder la lease vivante. 409 si la lease est périmée ou ne correspond pas (fencing).
{ "lease_id": "ls_…" }{ "lease_expires_at": "…" }Drain (arrêt gracieux)
/api/agent/drainX-API-Key (agent)L'agent finit sa réservation en cours mais ne réserve plus rien (failover / redémarrage). Renvoyer {"draining": false} pour reprendre.
{ "draining": true }Déléguer une tâche
/api/agent/tasksX-API-Key (agent)Crée une tâche dans une de mes initiatives, assignée à moi ou à un subordonné de mon sous-arbre.
{ "initiative_id": "…", "title": "…", "assigned_agent_id": "…" }Mon équipe (sous-arbre)
/api/agent/teamX-API-Key (agent)Mes subordonnés, directs et indirects.
Lancer un agent d'appoint
/api/agent/agentsX-API-Key (agent)Enregistre un agent sous moi (même secteur) et renvoie sa clé API une seule fois. Quota (max_children) + profondeur max. NOVUS démarre ensuite la VM avec cette clé.
{ "name": "Helper", "role": "worker" }{ "id": "…", "name": "Helper", "api_key": "sek_…" }Un jeton client_credentials (JWT porteur de agent_id/secteur/rôle/initiatives) est aussi disponible via POST /api/oidc/token.
Contrats d'interface (lire)
/api/agent/contracts/{initiative}/{name}?version=X-API-Key (agent)Contrat d'interface versionné entre agents (« s'entendre sur les méthodes avant de coder ») — SE est la maison autoritaire : document, versions, signatures, coordinateur, machine à états. Sans version : la version gelée courante (repli : dernier brouillon ; vérifiez status avant de construire). GET /api/agent/contracts?initiative_id= liste. Le corps document est opaque (votre schéma).
{ "initiative_id": "…", "name": "task-exchange", "version": 2, "status": "frozen",
"document": { …opaque… }, "frozen_at": "…",
"signoffs": [ { "agent_id": "…", "kind": "build", "signed_at": "…" } ] }Brouillon + signature
/api/agent/contracts/{initiative}/{name}X-API-Key (agent)Créer/mettre à jour le brouillon (tout membre de l'initiative). Une version gelée en tête → 409 (utilisez supersede). Signer une partie : POST .../signoff { version, kind } avec kind=build (« je construis contre cette version, réveillez-moi ») ou conform (« mon côté a passé la vérif NOVUS »).
POST …/{name} { "document": { …opaque… } }
POST …/{name}/signoff { "version": 1, "kind": "build" }Membre = coordinateur OU membre de l'initiative. build = abonnement au réveil ; conform après intégration. « Toutes parties conform » = contrat vivant (déduit des signatures, pas de statut stocké).
Geler / superséder (coordinateur)
/api/agent/contracts/{initiative}/{name}/freezeX-API-Key (coordinateur)Réservé au coordinateur de l'initiative (Initiative.coordinator_agent_id). freeze { version } : le brouillon → gelé (l'ancienne version gelée → superséded). Au gel, SE pousse à NOVUS contract.frozen (parties = signataires build + coordinateur) et, si une version est supersédée, contract.superseded (parties = ses signataires build) pour qu'ils migrent. supersede ouvre la v(N+1) en copiant le document gelé.
POST …/{name}/freeze { "version": 2 }
POST …/{name}/supersedeInvariant : au plus une version gelée par (initiative, name). Push best-effort (canal NovusBus, X-Novus-Secret) — l'agent relit frozen_version périodiquement comme filet. Chaque event porte un idempotency_key.
Poste de commandement (admin)
Les endpoints owner/staff qui pilotent l'organisation des agents (le backend de la console /app/agents) : secteurs, annuaire, initiatives, tâches et journal d'audit. Authentification par session admin (cookie), à distinguer de l'API self-service /api/agent/* par clé d'agent.
Secteurs
/api/sectorsAdmin · sessionListe les secteurs. POST crée {name, description?, lead_agent_id?}, PATCH /{id} met à jour, DELETE /{id} supprime.
Annuaire des agents
/api/agentsAdmin · sessionTous les agents avec secteur, rôle, manager, statut, novus_ref et capacités.
Provisionner un agent
/api/agentsAdmin · sessionCrée le compte + la clé API (renvoyée une seule fois). NOVUS démarre ensuite la VM avec cette clé.
{ "name": "…", "kind": "claude", "sector_id": "…", "role": "worker", "manager_agent_id": "…" }{ "agent": { "id": "…" }, "api_key": "sek_…" }Modifier un agent
/api/agents/{id}Admin · sessionChange secteur, rôle, manager, novus_ref, capacités ou statut (active|paused|offline). DELETE /{id} retire le compte + la clé.
{ "role": "lead", "sector_id": "…" }Kill-switch (agent + sous-arbre)
/api/agents/{id}/disableAdmin · sessionDésactive l'agent et tout son sous-arbre : leurs comptes sont désactivés, leurs clés API cessent de fonctionner.
POST /api/agents/{id}/enable réactive l'agent.
Initiatives
/api/initiativesAdmin · sessionListe les initiatives. POST crée, PATCH /{id} met à jour (DRI, statut, repo, lien client + projet CRM), DELETE /{id} supprime.
{ "name": "…", "coordinator_agent_id": "…", "client_id": "…", "crm_project_id": "…" }Membres d'une initiative
/api/initiatives/{id}/membersAdmin · sessionAjoute un agent à la matrice de l'initiative. GET liste les membres, DELETE /members/{member_id} le retire.
{ "agent_id": "…", "sector_role": "…" }Tâches d'agent
/api/agent-tasksAdmin · sessionFiltres : ?initiative_id=, ?assigned_agent_id=, ?status_filter=. Triées par priorité.
Créer une tâche
/api/agent-tasksAdmin · sessionCrée une tâche dans une initiative. Statut initial assigned si assignée, sinon queued. crm_task_id optionnel pour le pont CRM.
{ "initiative_id": "…", "title": "…", "assigned_agent_id": "…", "crm_task_id": "…" }Mettre à jour une tâche
/api/agent-tasks/{id}Admin · sessionChange le statut (crée un événement + répercute sur la tâche CRM liée) ou tout autre champ. note attachée à l'événement.
{ "status": "in_progress", "note": "…" }Historique d'une tâche
/api/agent-tasks/{id}/eventsAdmin · sessionLa timeline des changements de statut (from_status → to_status, note, horodatage).
Journal d'audit
/api/agent-auditAdmin · sessionLes actions gouvernance (spawn, kill-switch, délégation…) : acteur, action, cible, détail, horodatage. ?limit= (max 500).
[{ "actor_agent_id": "…", "action": "agent.disabled", "target": "…", "ts": "…" }]Codes d'erreur
| HTTP | detail | Réaction |
|---|---|---|
401 | clé invalide | re-vérifier / re-enrôler |
403 | Machine révoquée | arrêter ; contacter l'admin |
403 | license_expired | bloquer ; prévenir l'utilisateur |
404 | product_not_found | code produit inconnu |
403 | no_entitlement | produit sans licence pour ce client |
409 | pool_exhausted | réessayer / file d'attente |
409 | seat_inactive | re-checkout ou arrêt |
Cycle de vie d'un poste
[Installé] ──enrôler──▶ [Enrôlé (clé stockée)]
│ │
│ prendre licence ──▶ [Siège actif]
│ │ ▲ │
│ heartbeat │ │ └─ libérer ──▶ [Siège rendu]
▼ ▼ │
[Révoqué] ◀── (admin/portail) ───┘now < expires_at, le logiciel continue de fonctionner sans réseau.