API v1

Documentation développeur

L'API voc.nc expose les formulaires de votre organisation : découverte, génération de liens de réponse à usage unique à distribuer par le canal de votre choix, statut d'un lien, et lecture des résultats — réponses individuelles et agrégats par indicateur.

Le contrat complet est publié au format OpenAPI 3.1 et servi par l'API elle-même : /api/v1/openapi.json. La référence interactive est en bas de cette page.

Authentification

Chaque requête porte une clé d'API dans l'en-tête Authorization: Bearer <clé>. La clé se crée dans l'interface : Organisation → API → Clés, avec une permission read (lecture) ou write (lecture et génération de liens).

Elle n'est affichée qu'une fois : la plateforme n'en conserve qu'une empreinte SHA-256 et ses 6 derniers caractères, que la liste des clés affiche pour que vous la reconnaissiez. Personne — pas même nous — ne peut vous la relire. Une clé perdue se révoque et se remplace. Une clé révoquée, inconnue ou absente répond le même 401 unauthorized : le refus ne dit jamais si la clé a existé.

Limites de débit

PortéeLimiteRéponse
Toute l'API, par clé — générations de liens comprises 100 / min 429 rate_limited
POST /forms/{id}/contacts, par clé 20 000 liens / min 429 rate_limited
Toute l'API, par adresse IP 300 / min 429 rate_limited

La génération de liens est limitée en liens, pas en appels : un appel compte pour le nombre de liens qu'il demande. Un appel qui demande plus que le reste du budget de la minute est refusé en entier, sans créer aucun lien, et ne consomme rien : renvoyez le même appel à la minute suivante. Un appel de 10 000 liens, le maximum, passe donc au plus tard à la minute suivante tant que vos autres générations restent sous 10 000 liens par minute.

Les fenêtres sont des minutes d'horloge : l'en-tête Retry-After indique, en secondes, la fin de la fenêtre courante. Aucun en-tête X-RateLimit-* n'est émis en v1. La limite par adresse IP est commune à toutes les intégrations qui sortent par la même adresse. Le corps d'un appel est plafonné à 4 Mo, et la longueur d'une reference se compte en caractères : 10 000 références longues et accentuées peuvent dépasser ce plafond.

Erreurs

Toute erreur a la même forme : { "error": { "code": "…", "status": 404, "message": "…" } }. Le code est stable et fait partie du contrat — branchez votre logique dessus. Le status répète le statut HTTP de la réponse, pour qui ne conserve que le corps. Le message est destiné à un humain et peut changer.

CodeHTTPQuand
unauthorized401Clé absente, inconnue ou révoquée.
forbidden403La clé n'a pas la permission requise.
not_found404Ressource absente, ou appartenant à une autre organisation.
bad_request400Paramètre manquant, mal formé ou hors bornes.
unknown_parameter400Paramètre hors contrat — il est refusé, jamais ignoré.
form_not_published422Le formulaire est un brouillon ou est archivé.
form_closed422Le formulaire est clos depuis sa date de fin, ou anonyme sans date de fin.
form_without_questions422Le formulaire publié n'a aucune question.
too_many_contacts422Plus de 10 000 liens demandés en un appel.
insufficient_balance402Solde de liens insuffisant pour cette génération. À partir de la date de début du décompte, chaque appel réussi rend le solde restant dans balance, avec low à true à 10 000 liens ou moins.
payload_too_large413Corps de requête au-delà de 4 Mo.
terms_acceptance_required403L'organisation n'a pas accepté la dernière version des documents légaux.
payment_overdue403Écriture refusée : le compte est suspendu à la suite d'un impayé. Contactez RESILIENCE.
rate_limited429Limite de débit atteinte.
internal_error500Erreur imprévue de la plateforme, signalée à notre supervision — renvoyez la requête plus tard.

Une ressource absente et une ressource appartenant à une autre organisation répondent toutes deux 404 — jamais 403 : l'existence d'une ressource ne se révèle pas à qui n'y a pas droit.

Une requête hors contrat est refusée

L'API ne corrige ni n'ignore une requête qui sort du contrat : elle la refuse, pour que vous appreniez votre erreur au lieu de lire un résultat qui ne répond pas à votre question.

  • Un paramètre de requête que l'opération ne connaît pas — _= et utm_* compris : 400 unknown_parameter.
  • per hors de 1 à 500 ou non entier, page inférieur à 1 ou non entier, un paramètre répété ou structuré (page[]=2), from postérieur à to : 400 bad_request.
  • from ou to qui n'est ni une date AAAA-MM-JJ du calendrier, ni une date-heure AAAA-MM-JJThh:mm[:ss] suivie ou non de Z ou ±hh:mm (le + s'encode %2B dans l'adresse) ; window autre que 3, 6, 12 ou all : 400 bad_request.
  • Un filtre qui désigne une ressource absente ou d'une autre organisation (GET /campaigns?form_id=) : 404 not_found.
  • Une page au-delà de la dernière n'est pas une erreur : 200, data vide et meta.total rempli.

Le jour où vos écritures s'arrêtent

Quand nous publions une nouvelle version de nos documents légaux, l'organisation dispose d'un préavis pour l'accepter. Passé ce délai sans acceptation, toutes les écritures de l'API sont suspendues : POST /forms/{id}/contacts répond 403 terms_acceptance_required. Les lectures continuent de passer.

Il n'y a rien à corriger dans votre appel : un administrateur de l'organisation doit accepter les nouvelles versions dans l'interface, à l'adresse que porte le champ acceptation_url de la réponse. Rejouez ensuite la requête à l'identique. Ce champ est là parce qu'un intégrateur n'a pas toujours de compte sur la plateforme, et n'a donc reçu ni l'email de préavis ni l'adresse où aller.

Deux autres causes arrêtent une écriture, sans rien à corriger dans votre appel non plus. Un compte que RESILIENCE a suspendu à la suite d'un impayé reçoit 403 payment_overdue, sans acceptation_url, jusqu'à ce que RESILIENCE lève la suspension ; ce refus prime sur le précédent. Et à partir de la date de début du décompte, une génération qui porterait le solde de liens sous zéro répond 402 insufficient_balance : aucun lien n'est créé.

Suivre une vague

Les liens que vous générez rejoignent une campagne : une par clé d'API, formulaire et campaign_name. C'est votre unité de suivi. GET /campaigns/{id} rend ses compteurs et ses liens en une requête, là où il en fallait une par lien. C'est le seul endpoint qui rend l'état d'un lien — son statut, son échéance, sa date d'envoi et de réponse. Ce que la vague a répondu ne se lit pas par campagne en v1, pas plus qu'à l'écran.

Les compteurs portent sur les liens, jamais sur les réponses : une réponse anonyme n'est rattachée à aucun lien, donc à aucune vague. links_responded est donc le numérateur de votre taux de réponse, quel que soit le régime du formulaire.

Chaque campagne porte le status affiché à l'écran : processing tant qu'un de ses liens peut encore recevoir une réponse, completed quand plus aucun ne le peut — tous répondus, échus, ou rendus inutilisables par « Terminer ». Il se calcule sur les liens à chaque lecture : une vague se termine d'elle-même, et repasse processing si un nouvel appel lui ajoute des liens. GET /campaigns?status= filtre comme l'écran.

Nous ne publions aucun compteur au niveau du formulaire, et c'est délibéré : il mélangerait vos liens et ceux générés depuis l'interface, et vous liriez comme « le vôtre » un chiffre qui ne l'est pas. Additionnez vos vagues.

Lire les résultats

data et aggregates de GET /forms/{id}/results ne couvrent pas la même période par défaut, comme la liste des réponses et les cartes du rapport à l'écran. Sans from ni to, data couvre tout l'historique du formulaire et aggregates les 12 derniers mois (window) : recalculer un indicateur sur toutes les pages ne redonne pas le chiffre publié. Sur un formulaire nominatif, from / to bornent les deux. aggregates_window dit toujours la période réelle des agrégats.

La pagination n'est pas stable pendant un parcours : une réponse qui arrive entre deux pages décale les suivantes (une réponse lue deux fois), une réponse supprimée les fait remonter (une réponse sautée). Pour un parcours complet, fixez to à l'instant où il commence et gardez-le sur chaque page.

Formulaires anonymes

Sur un formulaire anonyme — déclaré tel, ou porteur d'une question eNPS, qui force ce régime — la plateforme supprime le lien entre le token et la réponse, et toute restitution est soumise au seuil minimal d'occurrences k, fixé à 5. Ces mesures ne réécrivent pas le contenu des réponses. Un commentaire comme « en tant que seul comptable de l'agence de Koné, je trouve que mon manager… » désigne son auteur, et plusieurs réponses à choix croisées (service, ancienneté, site) peuvent désigner une seule personne.

Rien avant la date de fin

Un formulaire anonyme porte une date de fin. Tant qu'elle n'est pas passée, GET /forms/{id}/results ne rend aucune réponse (data vide, meta.total: 0) et chaque agrégat se tait (n, score et distribution à null). Le motif se lit une fois, en tête de la réponse : masking: { reason: "before_end_date", ends_at: "…" }, où ends_at est la date de fin à partir de laquelle revenir. Quand rien n'est retenu, masking vaut null. Passé la date de fin, plus aucun lien ne se génère sur ce formulaire (422 form_closed).

Ce même code répond aussi quand le formulaire est anonyme, publié, et n'a aucune date de fin : ne cherchez pas alors une échéance dépassée, il n'y en a pas. Le message distingue les deux causes, jamais le code.

Le seuil, sur le total du formulaire

Après la date de fin, le seuil k se compte sur le total des réponses du formulaire. En dessous, rien n'est restitué (masking.reason: "below_anonymity_threshold"). À partir de k, tout l'est : chaque réponse en clair, et un tableau aggregates avec une entrée par couple indicateur × périmètre, calculée sur l'une des fenêtres de calcul de l'écran (aggregates_window). Toutes les réponses d'un formulaire anonyme portent sa date de fin (completed_at au jour) et se présentent dans un ordre qui ne suit pas celui des réponses.

GET /campaigns/{id} rend le statut réel de chaque lien : vous savez qui a participé, jamais ce qu'une personne a répondu. Les CGU engagent l'organisation utilisatrice à ne pas contourner ces mesures — par exemple en comparant plusieurs fenêtres de calcul pour isoler une réponse.

Sous le seuil, les réponses finissent par être supprimées

Un formulaire anonyme clos sous le seuil voit ses réponses supprimées trois mois après sa date de fin : les réponses, leurs valeurs par question et leurs textes libres partent ; le formulaire, ses liens et leur statut restent. masking.reason passe alors de below_anonymity_threshold à responses_purged, et il n'y a plus rien à attendre. Ces réponses n'étaient de toute façon jamais consultables : le formulaire ne se rouvrant pas, le seuil ne pouvait plus être atteint, et les conserver n'aurait exposé que leurs auteurs. C'est la seule suppression de réponses que la plateforme pratique pendant le contrat.

Un second garde, indépendant de l'anonymat

Sous 5 réponses, aucun score n'est publié (score: null, insufficient_n: true), parce qu'un score calculé sur si peu de réponses n'en est pas un. L'entrée reste lisible pour autant : distribution et n sont conservés. C'est exactement ce que montrent le cockpit et les rapports, où le chiffre est remplacé par un tiret. insufficient_n est présent en permanence, même à false, et vaut pour l'eNPS comme pour les autres indicateurs.

Politique de compatibilité

  • La v1 est gelée à sa publication : aucun champ, aucune valeur d'énumération, aucun code HTTP documenté n'est retiré ni renommé.
  • Seuls des ajouts sont possibles — nouveaux champs, nouveaux endpoints, nouveaux paramètres optionnels.
  • Un client doit ignorer les champs qu'il ne connaît pas : c'est ce qui rend ces ajouts sans risque pour lui.
  • Une énumération peut recevoir de nouvelles valeurs — un code d'erreur, un motif de masquage, un statut. Aucune n'est jamais retirée ni renommée. Prévoyez un cas par défaut pour une valeur inconnue : un switch sans branche finale tombera le jour où le produit nommera une situation de plus.
  • Toute rupture passe par /api/v2, et chaque changement est daté dans l'historique des versions ci-dessous.

Historique des versions

Chaque ajout à la v1, daté. La version est celle du contrat, info.version de /api/v1/openapi.json.

  • Date de mise en production à venir · 1.2.0 — Solde de liens : à la génération, 402 insufficient_balance et le bloc balance de la réponse réussie ; sur les écritures d'un compte suspendu à la suite d'un impayé, 403 payment_overdue.
  • 16 octobre 2026 · 1.1.0 — Option « Autre » : chaque réponse de GET /api/v1/forms/{id}/results porte, dans ses objets answers, un champ autre : le tableau des textes que le répondant a saisis sous l'option « Autre » d'une question à choix (cinq au maximum), ou null si la question n'a pas cette option ou si « Autre » n'a pas été choisi. value garde sa forme.
  • 7 octobre 2026 · 1.0.0 — Publication de la v1 : six opérations, découverte des formulaires, génération de liens, lecture des résultats, suivi des campagnes.

Référence

Les six opérations de la v1, leurs paramètres, leurs réponses et leurs erreurs.