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ée | Limite | Ré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.
| Code | HTTP | Quand |
|---|---|---|
| unauthorized | 401 | Clé absente, inconnue ou révoquée. |
| forbidden | 403 | La clé n'a pas la permission requise. |
| not_found | 404 | Ressource absente, ou appartenant à une autre organisation. |
| bad_request | 400 | Paramètre manquant, mal formé ou hors bornes. |
| unknown_parameter | 400 | Paramètre hors contrat — il est refusé, jamais ignoré. |
| form_not_published | 422 | Le formulaire est un brouillon ou est archivé. |
| form_closed | 422 | Le formulaire est clos depuis sa date de fin, ou anonyme sans date de fin. |
| form_without_questions | 422 | Le formulaire publié n'a aucune question. |
| too_many_contacts | 422 | Plus de 10 000 liens demandés en un appel. |
| insufficient_balance | 402 | Solde 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_large | 413 | Corps de requête au-delà de 4 Mo. |
| terms_acceptance_required | 403 | L'organisation n'a pas accepté la dernière version des documents légaux. |
| payment_overdue | 403 | Écriture refusée : le compte est suspendu à la suite d'un impayé. Contactez RESILIENCE. |
| rate_limited | 429 | Limite de débit atteinte. |
| internal_error | 500 | Erreur 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 —
_=etutm_*compris :400 unknown_parameter. perhors de 1 à 500 ou non entier,pageinférieur à 1 ou non entier, un paramètre répété ou structuré (page[]=2),frompostérieur àto:400 bad_request.fromoutoqui n'est ni une dateAAAA-MM-JJdu calendrier, ni une date-heureAAAA-MM-JJThh:mm[:ss]suivie ou non deZou±hh:mm(le+s'encode%2Bdans l'adresse) ;windowautre que3,6,12ouall: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,datavide etmeta.totalrempli.
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
switchsans 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_balanceet le blocbalancede 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 deGET /api/v1/forms/{id}/resultsporte, dans ses objetsanswers, un champautre: le tableau des textes que le répondant a saisis sous l'option « Autre » d'une question à choix (cinq au maximum), ounullsi la question n'a pas cette option ou si « Autre » n'a pas été choisi.valuegarde 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.