Smart-Evolution
← Retour
Développeurs · API

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/api

Introduction

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.

Aucun login humain côté logiciel. Tout est automatique, en HTTPS.

Configurer un poste

Comment un poste s'authentifie et se provisionne de façon transparente — aucun jeton manuel côté utilisateur.

  1. Connexion utilisateur (OIDC). App enregistrée comme client public (is_public: true, sans secret). Flow authorization code + PKCE (S256), Authority https://app.smart-evolution.com (auto-découverte /.well-known). Redirect loopback à port dynamique : enregistrer une fois http://127.0.0.1/callback/ (port ignoré). Scopes openid profile email license.
  2. Au login, l'id_token porte déjà l'identité (email, name), l'organisation (org_id, org_name, tableau orgs avec rôle) et la licence (plan, entitlements, valid_until, licensed) — exploitable hors-ligne, sans appel supplémentaire.
  3. Enrôlement automatique du poste. Si aucune clé machine locale : POST /api/portal/machines/enroll avec Authorization: Bearer et un corps { machine_uid, name }. Stocker la machine_key renvoyée (keystore). status: pending → afficher « en attente d'approbation ».
  4. À l'exécution — deux secrets. X-Machine-Key: dvk_… pour tout ce qui est machine (licences, config, manifeste de plugins, chiffrement) ; Authorization: Bearer pour les appels au nom de l'utilisateur.
  5. 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 :

SecretEn-tête / mécanismeUsage
enr_… jeton d'enrôlementX-Enroll-TokenUne fois, pour inscrire une machine
dvk_… clé machineX-Machine-KeyTous les appels d'un poste (licences, équipement, chiffrement)
Utilisateur du portailCookie 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

POST/api/license/enrollX-Enroll-Token

Inscrit 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).

Requête
{
  "machine_uid": "<empreinte matérielle stable>",
  "name": "PC-USINE-07",
  "location": "Usine Saint-Jean",
  "asset_tag": "INV-1042"
}
Réponse
{
  "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

POST/api/license/enroll-token/checkX-Enroll-Token

Vé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.

Réponse
{ "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

POST/api/license/checkoutX-Machine-Key

Prend un siège dans le pool du produit. Idempotent : relancé sur la même machine, renvoie le même siège.

Requête
{
  "product_code": "mon-produit",
  "user": "marc@acme.com"   // optionnel (mode nominatif)
}
Réponse
{
  "seat_id": "uuid",
  "lease_token": "…",
  "expires_at": "2026-06-24T16:00:00Z",
  "product": "Mon produit",
  "mode": "floating",
  "valid_until": "2027-01-01"
}
404 product_not_found — code produit inconnu403 no_entitlement — pas de licence pour ce produit409 pool_exhausted — plus de siège403 license_expired

Renouveler le bail

POST/api/license/heartbeatX-Machine-Key

Prolonge 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é.

Requête
{ "seat_id": "uuid", "lease_token": "…" }
Réponse
{ "expires_at": "2026-06-24T20:00:00Z", "valid_until": "2027-01-01" }
409 seat_inactive — siège révoqué : arrêter

Libérer la licence

POST/api/license/releaseX-Machine-Key

Rend le siège au pool immédiatement. À appeler à la fermeture du logiciel.

Requête
{ "seat_id": "uuid", "lease_token": "…" }
Réponse
204 No Content

État de licence

GET/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.

Réponse
{ "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

GET/api/org/licensesJeton OIDC utilisateur

Les 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.

Réponse
// 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

GET/api/store/catalogJeton OIDC utilisateur

Tous 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.

Réponse
{ "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

POST/api/store/trialsJeton OIDC utilisateur

Accorde à 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.

Requête
{ "product_code": "evocore.admincrypt" }
Réponse
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

GET/api/device/configX-Machine-Key

Configuration courante de l'équipement (valeurs + schéma typé), telle que réglée dans la plateforme.

Réponse
{ "label": "prod", "values": { "luminosite": 80 },
  "fields": [ { "key": "luminosite", "type": "int" } ] }

Charger le code source

GET/api/device/sourceX-Machine-Key

Code source assigné à l'équipement (ou partagé au client), version courante incluse.

Réponse
[ { "id": "uuid", "title": "Boot", "language": "python", "version": "1.0", "content": "…" } ]

Manifeste de plugins

GET/api/device/pluginsX-Machine-Key

Les 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).

Réponse
{
  "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.

Le serveur ne voit jamais : ta clé privée admin, la clé privée d'org en clair, ni le contenu des fichiers.

Clés actives de l'organisation

GET/api/org/keys/activeX-Machine-Key

Toutes les clés actives — un slot par clé (assisté + hardware).

Réponse
[ { "keyId": "…", "role": "assisted", "algorithm": "RSA-3072-OAEP-SHA256", "publicKeySpki": "…" } ]

Demander une récupération

POST/api/org/recover/requestsX-Machine-Key

Demande de récupération assistée. Le poste envoie une clé publique éphémère (canal de retour).

Requête
{ "keyId": "…", "wrappedDek": "…", "clientPubKey": "…", "reason": "…", "fileRef": "…" }
Réponse
{ "requestId": "…", "status": "pending" }

Récupérer la clé approuvée

GET/api/org/recover/requests/{id}X-Machine-Key

Récupère le DEK une fois approuvé (chiffré pour le poste). Disponible 1 h puis effacé.

Réponse
{ "status": "fulfilled", "dekEncryptedForClient": "…" }

Enregistrer la clé admin

POST/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.

Requête
{ "adminPublicKeySpki": "…" }

Créer une clé d'organisation

POST/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).

Requête
{ "role": "assisted", "makeCurrent": true }
Réponse
{ "keyId": "…", "role": "assisted", "publicKeySpki": "…", "status": "current" }

Demandes en attente (admin)

GET/api/org/{orgId}/recover/requests?status_filter=pendingAdmin · session (2FA)

Les demandes en attente (avec wrappedDek + clientPubKey) pour la console d'approbation.

Réponse
[ { "id": "…", "keyId": "…", "wrappedDek": "…", "clientPubKey": "…", "reason": "…" } ]

Approuver une récupération (admin)

POST/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.

Requête
{ "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.

Le serveur ne voit jamais : le contenu des secrets, les titres (chiffrés), les DEK emballées, ni les clés privées. Uniquement des métadonnées : type, dates, appartenance, key_id.

Publier ma clé

PUT/api/vault/keys/meCookie session

Premiè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).

Requête
{ "public_key_spki": "…", "wrapped_private_key": "…" }
Réponse
204 No Content

Ma clé

GET/api/vault/keys/meCookie session

Récupérée sur chaque appareil ; le client déballe la clé privée localement avec la passphrase. 404 si pas encore configurée.

Réponse
{ "public_key_spki": "…", "wrapped_private_key": "…", "updated_at": "…" }

Annuaire des membres

GET/api/vault/membersCookie session

Les utilisateurs de l'organisation et leur clé PUBLIQUE, pour choisir un destinataire de partage. Aucune clé privée ici.

Réponse
[ { "user_id": "…", "email": "…", "name": "…", "public_key_spki": "…", "has_key": true } ]

Mes coffres

GET/api/vault/vaultsCookie session

Coffres 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).

Réponse
[ { "id": "…", "enc_name": "…", "kind": "shared", "owner_user_id": "…", "created_at": "…",
    "slots": [ { "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_vault_key": "…" } ] } ]

Partager un coffre (slot)

POST/api/vault/vaults/{id}/slotsCookie session

Donne accès à un membre : le client emballe la DEK de coffre sous la clé publique du destinataire. La DEK de coffre chiffre les métadonnées (nom + noms de dossiers/tags). Réservé au propriétaire du coffre ou à un délégué. Un coffre personnel = un coffre à un seul slot (soi-même).

Requête
{ "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_vault_key": "…" }
Réponse
{ "key_id": "…" }

Retirer un membre (slot coffre)

DELETE/api/vault/vaults/{id}/slots/{key_id}Cookie session

Retire un membre du coffre. Sécurité descendante : rotation de la DEK de coffre côté client (ré-emballage des slots restants + re-chiffrement du nom et des dossiers/tags).

Réponse
204 No Content

Synchroniser les items

GET/api/vault/vaults/{id}/items?since={seq}&include_deleted=trueCookie session

Delta 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.

Réponse
{ "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

POST/api/vault/itemsCookie session

Cré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.

Requête
{ "vault_id": "…", "type": "password", "enc_title": "…", "enc_blob": "…",
  "slots": [ { "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_dek": "…" } ] }
Réponse
{ "id": "…", "seq": 42 }
400 base64 invalide / type inconnu403 accès refusé au coffre413 enc_blob > 1 Mo

Modifier un item

PUT/api/vault/items/{id}Cookie session

Remplace 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é.

Requête
{ "enc_title": "…", "enc_blob": "…", "slots": [ … ], "base_seq": 42 }
Réponse
{ "seq": 43 }
409 { detail: { current_seq } } — édition concurrente : re-synchroniser, rejouer l'édition, réessayer

Partager (ajouter un slot)

POST/api/vault/items/{id}/slotsCookie session

Accorde 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é.

Requête
{ "key_id": "…", "kind": "user", "recipient_user_id": "…", "wrapped_dek": "…" }
Réponse
{ "key_id": "…", "seq": 44 }

Révoquer un accès

DELETE/api/vault/items/{id}/slots/{key_id}Cookie session

Retire 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.

Réponse
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

POST/api/portal/auth/request-accessPublic

Auto-demande d'accès (le domaine du courriel doit correspondre à un client connu). Réponse générique.

Requête
{ "email": "jane@acme.com", "name": "Jane Doe" }

Mon profil

GET/api/portal/meCookie session

L'utilisateur portail connecté (client, nom).

Mes permissions

GET/api/portal/access/meCookie session

Modules + grants effectifs (niveau + portée).

Mes projets

GET/api/portal/projectsCookie session

Projets du client (restreints aux projets autorisés).

Mes communications

GET/api/portal/communicationsCookie session

Journal des échanges. Portée ses propres / tous selon la permission.

Ouvrir un incident

POST/api/portal/incidentsCookie session

Ouvre 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.

Requête
{ "title": "…", "severity": "medium", "description": "…", "screenshot_base64": "data:image/png;base64,iVBORw0K…" }

Approuver une soumission

POST/api/portal/soumissions/{id}/approveCookie session

Approuve une soumission (fige le devis → lead Vendu).

Mes documents

GET/api/portal/documentsCookie session

Documents du client. Téléchargement via /documents/{id}/download (URL présignée).

Mes postes / licences

GET/api/portal/machinesCookie session

Postes enrôlés. PATCH pour approuver/révoquer, DELETE pour retirer un poste révoqué.

Enrôler ce poste (sans jeton)

POST/api/portal/machines/enrollCookie session / Bearer OIDC

Auto-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).

Requête
{ "machine_uid": "<empreinte matérielle stable>", "name": "PC-USINE-07" }
Réponse
{ "machine_id": "…", "machine_key": "dvk_…", "status": "active" }   // ou "pending" si approbation requise

Mes téléchargements & versions

GET/api/portal/downloadsCookie session

Produits 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).

Réponse
[ { "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

POST/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).

Requête
{ "version_id": "…" }   // ou { "version_id": null } pour retirer l'épingle

Jeton d'enrôlement

POST/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

GET/api/portal/source-codesCookie session

Code source hébergé du client. POST /{id}/versions pour sauvegarder si accès lecture/écriture.

Approuver une étude de cas

POST/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

POST/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.

Requête
{ "email": "bob@acme.com", "first_name": "Bob" }

Demandes agent (soumettre + suivre)

POST/api/portal/agent-requestsCookie session · module agents

Soumet 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.

Requête
{ "title": "Analyser nos données", "description": "…", "client_ref": "CRM-42", "priority": 0 }
Réponse
{ "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.jsonpas 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.

Codes à usage unique (~60 s), PKCE obligatoire pour les clients publics, refresh tokens rotatifs et révocables. Les services valident hors-ligne via JWKS (signature, iss, aud, exp) — aucun secret partagé.

Découverte OIDC

GET/.well-known/openid-configurationPublic

Document de découverte, à la RACINE du domaine (pas sous /api). Chaque librairie lit cette URL et se configure seule (endpoints, algos, scopes).

Réponse
{ "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)

GET/.well-known/jwks.jsonPublic

Clé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).

Réponse
{ "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "…", "n": "…", "e": "AQAB" } ] }

Autoriser

GET/api/oidc/authorizePublic · navigateur

Début du flow. Query : client_id, redirect_uri (match exact ; exception RFC 8252 : pour une URI loopback http://127.0.0.1 ou http://[::1], le port est ignoré au matching → apps natives à port dynamique, enregistrez http://127.0.0.1/callback/), response_type=code, scope=openid…, state, nonce, code_challenge, code_challenge_method=S256. Sans session portail → redirection vers /login?next=… puis retour. Répond 302 vers redirect_uri?code=…&state=….

Remonter l'usage LLM (NOVUS→SE)

POST/api/llm/usageBearer client_credentials · scope llm.usage

Service-à-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).

Requête
[ { "request_id": "…", "org_id": "<client id>", "model": "claude-…", "cost_cad": 0.42, "tokens_in": 1200, "tokens_out": 800, "sector_id": "…?", "player_id": "…?" } ]
Réponse
{ "received": 3, "recorded": 2, "duplicates": 1 }

Échanger le code / rafraîchir

POST/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).

Requête
grant_type=authorization_code&code=…&redirect_uri=…&client_id=cli_…&client_secret=…&code_verifier=…
Réponse
{ "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)

POST/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.

Requête
POST /api/oidc/device_authorization
client_id=cli_…&scope=openid
Réponse
{ "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)

GET/api/oidc/userinfoBearer access_token

Identité de base à partir de l'access_token.

Réponse
{ "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)

GET/api/oidc/logoutPublic · navigateur

Ferme 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

GET/api/oidc/clientsAdmin · session

Liste des clients OIDC (outils enregistrés). Gérable dans Administration → Applications (OIDC).

Enregistrer une application

POST/api/oidc/clientsAdmin · session

Enregistre un outil. Le client_secret est renvoyé une seule fois (clients confidentiels).

Requête
{ "name": "Portail EvoKPI", "redirect_uris": ["https://…/signin-oidc"], "is_public": false, "product_code": "evokpi" }
Réponse
{ "client_id": "cli_…", "client_secret": "…", "client": { "id": "…", "is_active": true } }

Modifier / secret / supprimer

PATCH/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.

Tout est cadré à l'agent (sous-arbre + initiatives), audité et révocable (kill-switch en cascade). L'avancement d'une tâche liée remonte automatiquement à la tâche CRM. La coordination entre pairs vit sur NovusBus, pas dans l'app.

Mon profil d'agent

GET/api/agent/meX-API-Key (agent)

L'agent courant : secteur, rôle, manager, initiatives.

Réponse
{ "id": "…", "role": "worker", "sector_id": "…", "initiatives": ["…"] }

Fiches produit (éditorial agent)

PATCH/api/agent/products/{id}X-API-Key (agent) · capacité products

Surface é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.

Requête
{ "public_description": "<p>…FR…</p>", "public_description_en": "<p>…EN…</p>" }

Support client (helpdesk agent)

POST/api/agent/support/incidents/{id}/replyX-API-Key (agent) · capacité support

Traitement 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.

Requête
{ "body": "Bonjour, voici la marche à suivre…", "subject": "Re: …" }

Annuaire mesh (NOVUS→SE)

GET/api/agent/directoryBearer client_credentials · scope agents.directory

L'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.

Réponse
[ { "mesh_address": "…uuid…", "name": "…", "description": "…", "novus_ref": "…",
    "sector": "…", "role": "worker", "status": "active",
    "initiatives": [ { "id": "…", "name": "…", "sector_role": "…" } ] } ]

Ma file de tâches

GET/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

PATCH/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.

Requête
{ "status": "in_progress", "lease_id": "ls_…", "note": "…" }

Réserver une tâche (lease)

POST/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.

Réponse
{ "lease_id": "ls_…", "lease_expires_at": "…" }

Prolonger la réservation

POST/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).

Requête
{ "lease_id": "ls_…" }
Réponse
{ "lease_expires_at": "…" }

Drain (arrêt gracieux)

POST/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.

Requête
{ "draining": true }

Déléguer une tâche

POST/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.

Requête
{ "initiative_id": "…", "title": "…", "assigned_agent_id": "…" }

Mon équipe (sous-arbre)

GET/api/agent/teamX-API-Key (agent)

Mes subordonnés, directs et indirects.

Lancer un agent d'appoint

POST/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é.

Requête
{ "name": "Helper", "role": "worker" }
Réponse
{ "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)

GET/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).

Réponse
{ "initiative_id": "…", "name": "task-exchange", "version": 2, "status": "frozen",
  "document": { …opaque… }, "frozen_at": "…",
  "signoffs": [ { "agent_id": "…", "kind": "build", "signed_at": "…" } ] }

Brouillon + signature

POST/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 »).

Requête
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)

POST/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é.

Requête
POST …/{name}/freeze     { "version": 2 }
POST …/{name}/supersede

Invariant : 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

GET/api/sectorsAdmin · session

Liste les secteurs. POST crée {name, description?, lead_agent_id?}, PATCH /{id} met à jour, DELETE /{id} supprime.

Annuaire des agents

GET/api/agentsAdmin · session

Tous les agents avec secteur, rôle, manager, statut, novus_ref et capacités.

Provisionner un agent

POST/api/agentsAdmin · session

Crée le compte + la clé API (renvoyée une seule fois). NOVUS démarre ensuite la VM avec cette clé.

Requête
{ "name": "…", "kind": "claude", "sector_id": "…", "role": "worker", "manager_agent_id": "…" }
Réponse
{ "agent": { "id": "…" }, "api_key": "sek_…" }

Modifier un agent

PATCH/api/agents/{id}Admin · session

Change secteur, rôle, manager, novus_ref, capacités ou statut (active|paused|offline). DELETE /{id} retire le compte + la clé.

Requête
{ "role": "lead", "sector_id": "…" }

Kill-switch (agent + sous-arbre)

POST/api/agents/{id}/disableAdmin · session

Dé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

GET/api/initiativesAdmin · session

Liste les initiatives. POST crée, PATCH /{id} met à jour (DRI, statut, repo, lien client + projet CRM), DELETE /{id} supprime.

Requête
{ "name": "…", "coordinator_agent_id": "…", "client_id": "…", "crm_project_id": "…" }

Membres d'une initiative

POST/api/initiatives/{id}/membersAdmin · session

Ajoute un agent à la matrice de l'initiative. GET liste les membres, DELETE /members/{member_id} le retire.

Requête
{ "agent_id": "…", "sector_role": "…" }

Tâches d'agent

GET/api/agent-tasksAdmin · session

Filtres : ?initiative_id=, ?assigned_agent_id=, ?status_filter=. Triées par priorité.

Créer une tâche

POST/api/agent-tasksAdmin · session

Crée une tâche dans une initiative. Statut initial assigned si assignée, sinon queued. crm_task_id optionnel pour le pont CRM.

Requête
{ "initiative_id": "…", "title": "…", "assigned_agent_id": "…", "crm_task_id": "…" }

Mettre à jour une tâche

PATCH/api/agent-tasks/{id}Admin · session

Change 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.

Requête
{ "status": "in_progress", "note": "…" }

Historique d'une tâche

GET/api/agent-tasks/{id}/eventsAdmin · session

La timeline des changements de statut (from_status → to_status, note, horodatage).

Journal d'audit

GET/api/agent-auditAdmin · session

Les actions gouvernance (spawn, kill-switch, délégation…) : acteur, action, cible, détail, horodatage. ?limit= (max 500).

Réponse
[{ "actor_agent_id": "…", "action": "agent.disabled", "target": "…", "ts": "…" }]

Codes d'erreur

HTTPdetailRéaction
401clé invalidere-vérifier / re-enrôler
403Machine révoquéearrêter ; contacter l'admin
403license_expiredbloquer ; prévenir l'utilisateur
404product_not_foundcode produit inconnu
403no_entitlementproduit sans licence pour ce client
409pool_exhaustedréessayer / file d'attente
409seat_inactivere-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) ───┘
Tolérance hors-ligne : tant que now < expires_at, le logiciel continue de fonctionner sans réseau.