Trame · Documentation

Endpoints REST#

Toutes les routes ci-dessous sont relatives à la base de l’API de votre espace (voir Vue d’ensemble) et authentifiées par l’en-tête X-Api-Key (voir Authentification et scopes). Une description machine équivalente est publiée en OpenAPI 3.1.

Base de l’API :

https://<base-api>/public

Le chemin d’une route se concatène à cette base : GET /v1/conversations s’appelle donc https://<base-api>/public/v1/conversations. Le segment qui suit le domaine fait partie de la base : une URL qui l’omet ne joint pas l’API et retombe sur l’application web. Les exemples ci-dessous portent toujours l’URL complète.

Index des routes (37)#

MéthodeCheminScopeEffet
GET/v1/conversationsread:conversationsListe les conversations de la boîte demandée, de la plus récente à la plus ancienne.
GET/v1/conversations/{id}read:conversationsDétail d’une conversation (fil complet avec ses messages).
POST/v1/conversations/{id}/assignwrite:conversationsAssigne la conversation à une personne, avec un statut et une équipe optionnels.
POST/v1/conversations/{id}/commentwrite:conversationsAjoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant.
POST/v1/conversations/{id}/labelswrite:conversationsAjoute et/ou retire des étiquettes sur le fil.
POST/v1/conversations/{id}/archivewrite:conversationsArchive la conversation : elle quitte la boîte de réception sans être supprimée.
POST/v1/conversations/{id}/reopenwrite:conversationsRemet la conversation dans la boîte de réception (opération inverse de l’archivage).
POST/v1/conversations/{id}/snoozewrite:conversationsReporte la conversation : elle est mise de côté puis revient à l’échéance indiquée.
GET/v1/searchread:conversationsRecherche dans les conversations d’une boîte, via le moteur de recherche de l’espace.
POST/v1/conversations/{id}/sendmail:sendEnvoie une réponse dans un fil existant, au nom de la boîte indiquée.
POST/v1/sendmail:sendEnvoie un nouveau message au nom de la boîte indiquée.
GET/v1/draftsdrafts:writeListe les brouillons de la boîte.
POST/v1/draftsdrafts:writeCrée un brouillon dans la boîte.
PATCH/v1/drafts/{id}drafts:writeMet à jour un brouillon existant.
DELETE/v1/drafts/{id}drafts:writeSupprime un brouillon.
GET/v1/contactsread:contactsListe les contacts des carnets accessibles à la clé.
GET/v1/contacts/{id}read:contactsDétail d’un contact.
POST/v1/contactswrite:contactsCrée un contact dans un carnet.
PATCH/v1/contacts/{id}write:contactsMet à jour un contact.
DELETE/v1/contacts/{id}write:contactsSupprime un contact.
GET/v1/calendarsread:calendarListe les calendriers accessibles à la clé.
POST/v1/calendarswrite:calendarCrée un calendrier.
DELETE/v1/calendars/{id}write:calendarSupprime un calendrier et tous ses événements.
GET/v1/calendar-eventsread:calendarListe les événements des calendriers accessibles, sur une fenêtre de temps.
POST/v1/calendar-eventswrite:calendarCrée un événement dans un calendrier.
GET/v1/calendar-events/{id}read:calendarDétail d’un événement.
PATCH/v1/calendar-events/{id}write:calendarMet à jour un événement.
DELETE/v1/calendar-events/{id}write:calendarSupprime un événement.
GET/v1/tasksread:tasksListe les tâches de l’espace.
POST/v1/taskswrite:tasksCrée une tâche.
GET/v1/tasks/{id}read:tasksDétail d’une tâche.
PATCH/v1/tasks/{id}write:tasksMet à jour une tâche.
DELETE/v1/tasks/{id}write:tasksSupprime une tâche.
GET/v1/signaturesread:conversationsListe les signatures configurées pour l’espace.
GET/v1/analytics/summaryread:analyticsRésumé analytique de l’espace (volumes).
GET/v1/webhooks/testwrite:conversationsÉmet un événement de test vers les souscriptions webhook de l’espace.
POST/mcpmcpPoint d’entrée JSON-RPC 2.0 du serveur MCP (agents IA).

Accès rapide : Conversations · Envoi de message · Brouillons · Contacts · Agenda · Tâches · Signatures et analytics · Webhooks · MCP

Limites de fréquence#

PortéePlafond par clé et par instance
Toutes les routes (par défaut)120 requêtes/min
Routes d’envoi (mail:send)10 requêtes/min, compteur indépendant

Fenêtre glissante de 60 secondes, comptée par clé et par famille de scope : saturer l’envoi ne bloque jamais vos routes de lecture.

Ces plafonds sont appliqués par instance de service. Le service pouvant être répliqué sous charge, le débit agrégé observable peut dépasser ces valeurs : traitez-les comme un garde-fou, pas comme une garantie contractuelle. Un plafond global partagé est prévu ; en attendant, dimensionnez vos intégrations sur ces valeurs.

Plafonds des routes d’envoi#

Ces bornes s’appliquent à la charge utile de POST /v1/send et POST /v1/conversations/{id}/send. Un dépassement est refusé explicitement — jamais tronqué en silence — et aucun message ne part.

ÉlémentPlafondRefus
Pièces jointes (nombre)50 par message413
Pièces jointes (volume cumulé)25 Mo413
Adresses par champ (to, cc, bcc)500400
Adresses par message (to + cc + bcc réunis)500400
Longueur d’une adresse (nom d’affichage compris)640 caractères400

Les deux bornes d’adresses sont indépendantes : chaque champ peut être sous son plafond et le message être refusé quand même. Avec to = 400 et cc = 400, aucun champ ne dépasse 500, mais le total (800) dépasse 500 : la requête est refusée en 400, aucun message ne part, aucune liste n’est tronquée. Dimensionnez vos lots sur le total, pas sur la borne par champ.

Ces bornes comptent des adresses, pas des entrées de tableau : une chaîne "a@x.fr, b@y.fr" compte pour deux, et une entrée unique qui porterait deux adresses est refusée en 400 plutôt que comptée pour une. Une adresse dont la forme n’est pas exploitable (objet sans champ email ou address, valeur qui n’est ni une chaîne ni un objet) est elle aussi refusée en 400, avant tout envoi — jamais devinée, jamais convertie de force.

Le volume des pièces jointes est mesuré avant décodage et cumulé sur tout le message : c’est la somme qui compte, pas la plus grosse pièce. Le contenu doit être du base64 canonique strict, sinon 400.

Erreurs communes#

Toutes les réponses sont application/json. Le corps d’une erreur porte toujours un champ error, mais sa nature dépend de la famille de routes :

FamillesFormeÀ savoir
Conversations, envoi, brouillons, signatures, analytics, webhooks, MCP{ "error": "message lisible en français" }Le message est rédigé pour un humain et ne contient jamais de détail d’implémentation. N’écrivez pas de test sur son texte : il peut être reformulé.
Contacts, agenda, tâches{ "error": "CODE_EN_MAJUSCULES" }Le champ error porte un identifiant stable, pas une phrase : testez-le tel quel plutôt que d’afficher sa valeur à un utilisateur final. Tout code hors liste blanche est remplacé par le code de repli du domaine.
CodeSignification
400requête invalide (paramètre manquant, valeur hors plafond), ou en-tête Idempotency-Key absent : toute écriture demandée par une clé de type agent à approbation requise l’exige, y compris sur des routes où elle est facultative pour une clé humaine. Sur les routes d’envoi, 400 couvre aussi les listes d’adresses hors plafond — par champ ET au total du message — et les adresses dont la forme n’est pas exploitable (voir « Plafonds des routes d’envoi »).
401clé absente, invalide ou révoquée — réponse identique dans les trois cas, aucun moyen de distinguer « clé inconnue » de « clé révoquée ». Le refus est tracé : toujours dans le journal du service, et en plus dans le journal d’audit de votre espace quand la clé présentée est reconnue comme une clé de cet espace (révoquée, incomplète). Ce que le journal retient n’est jamais la clé, seulement un préfixe non réversible de son empreinte ; les entrées d’audit sont plafonnées par clé et par minute pour qu’une rafale ne noie pas le journal.
403scope manquant, boîte non autorisée, clé en lecture seule sur la boîte, interrupteur d’envoi fermé, clé de type agent sur une route d’envoi, ou clé non rattachée à un membre identifié (routes contacts, agenda et tâches — voir la note de chaque route).
404route inconnue, ou ressource introuvable dans l’espace / la boîte demandée.
409conflit : même Idempotency-Key rejouée avec une charge différente (IDEMPOTENCY_KEY_REUSED, en-tête X-EPB-Idempotency: key-reused), Idempotency-Key déjà enregistrée par une autre voie d’envoi de l’espace et donc invérifiable (IDEMPOTENCY_KEY_UNVERIFIABLE, en-tête X-EPB-Idempotency: key-unverifiable), envoi déjà en cours pour cette clé, avertissement de prévention de fuite de données à confirmer, ou report modifié entre-temps.
413pièces jointes hors plafond (voir « Plafonds des routes d’envoi ») — aucun message n’est parti.
429plafond de fréquence dépassé pour cette clé. Ce plafond est compté par instance de service : sous charge, plusieurs instances servent la même clé et le débit agrégé effectivement accepté peut dépasser la valeur affichée dans « Limites de fréquence ». Traitez ces valeurs comme un garde-fou anti-emballement, jamais comme une garantie de débit maximal ; un plafond global partagé est une évolution prévue. Comme le 401, ce refus est tracé : toujours dans le journal du service, et dans le journal d’audit de votre espace si la clé a pu être identifiée — un plafond peut aussi viser une empreinte inconnue, qui n’appartient à aucun espace et ne peut donc y être journalisée. Plafonné par clé et par minute.
500erreur interne — le message ne contient jamais de détail d’implémentation.
502reçu d’envoi indisponible : l’issue est AMBIGUË (le message a pu partir). Voir la note de la route concernée.
503service indisponible ou mal configuré (rafraîchissement des autorisations, autorisation d’écriture, verrou d’envoi, file d’approbation, service MCP) — la requête est réessayable. Ce statut est possible sur toute route : il est rendu avant même le routage quand les autorisations ne peuvent pas être chargées.

Une écriture demandée par une clé de type agent à approbation requise ne renvoie pas le statut de succès de la route mais 202 : { "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400). Les routes concernées le signalent dans leur liste de réponses.

Codes d’erreur des routes contacts, agenda et tâches#

Ces routes ne renvoient pas de phrase dans error mais un identifiant stable. Testez la valeur, ne l’affichez pas telle quelle à un utilisateur final.

Contacts

CodeStatutCas
VALIDATION_FAILED400contenu du contact refusé par la validation.
EMPTY_CONTACT400ni nom, ni adresse, ni organisation — un contact vide n’est pas créé.
INVALID_ARGUMENT400requête non exploitable.
FORBIDDEN403carnet ou contact hors de portée de la clé.
BOOK_NOT_FOUND404bookId inconnu dans cet espace.
CONTACT_NOT_FOUND404contact inconnu (ou fusionné).
METHOD_NOT_ALLOWED405méthode non exposée sur ce chemin.
CONTACTS_FAILED500repli : toute erreur non listée est ramenée à ce code.

Agenda

CodeStatutCas
TITLE_REQUIRED400title vide.
INVALID_DATES400startMs / endMs absents, non numériques ou nuls.
END_BEFORE_START400endMs inférieur ou égal à startMs.
DURATION_TOO_LONG400durée supérieure à 366 jours.
INVALID_CONFERENCE_LINK400conferenceLink qui ne commence pas par http:// ou https://.
INVALID_ARGUMENT400requête non exploitable (par exemple name vide à la création d’un calendrier).
FORBIDDEN403calendrier ou événement hors de portée de la clé.
CALENDAR_SCOPE_FORBIDDEN403création d’un calendrier shared demandée par une clé non administratrice.
CALENDAR_NOT_FOUND404calendarId inconnu dans cet espace.
EVENT_NOT_FOUND404événement inconnu.
METHOD_NOT_ALLOWED405méthode non exposée sur ce chemin.
CALENDAR_FAILED500repli : toute erreur non listée est ramenée à ce code.

Tâches

CodeStatutCas
TITLE_REQUIRED400title vide.
RECURRENCE_NEEDS_DUE400récurrence demandée sans dueMs.
INVALID_ARGUMENT400requête non exploitable.
SOURCE_FORBIDDEN403source réservée au serveur (ai, rule, sync) posée par le client — seules manual et mail sont acceptées.
TASK_NOT_FOUND404tâche inconnue.
METHOD_NOT_ALLOWED405méthode non exposée sur ce chemin.
TASKS_FAILED500repli : toute erreur non listée est ramenée à ce code.

Conversations#

GET /v1/conversations#

Scope requis : read:conversations.

Liste les conversations de la boîte demandée, de la plus récente à la plus ancienne.

ParamètreEmplacementRequisTypeDétail
mailboxrequêteouistringboîte ciblée, obligatoirement accessible à la clé.
limitrequêtenoninteger1 à 100, défaut 25.
pageTokenrequêtenonstringjeton de pagination renvoyé par l’appel précédent.

Réponses :

  • 200{ "mailbox", "conversations": [...], "nextPageToken": string|null }
bash
curl "https://<base-api>/public/v1/conversations?mailbox=contact@exemple.com&limit=25" \
  -H "X-Api-Key: <votre-clé>"

GET /v1/conversations/{id}#

Scope requis : read:conversations.

Détail d’une conversation (fil complet avec ses messages).

ParamètreEmplacementRequisTypeDétail
mailboxrequêteouistringboîte ciblée, obligatoirement accessible à la clé.

Réponses :

  • 200{ "mailbox", "conversation": { "id", "subject", "messages": [...] } }
  • 404 — conversation introuvable ou hors de la boîte demandée.

POST /v1/conversations/{id}/assign#

Scope requis : write:conversations.

Assigne la conversation à une personne, avec un statut et une équipe optionnels.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
assigneecorpsnonstringadresse de la personne assignée, ou null pour désassigner.
statuscorpsnonstringstatut applicatif à poser sur le fil.
teamIdcorpsnonstringidentifiant d’équipe destinataire.

Réponses :

  • 200{ "ok": true, "id", "result" }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).

POST /v1/conversations/{id}/comment#

Scope requis : write:conversations.

Ajoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant.

ParamètreEmplacementRequisTypeDétail
Idempotency-Keyen-têteouistring8 à 200 caractères imprimables ; un rejeu avec la même valeur et la même charge ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé 409, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée 409, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
textcorpsouistringcontenu du commentaire, 10 000 caractères maximum.

Réponses :

  • 200{ "ok": true, "id", "result" }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400text absent, trop long, ou en-tête Idempotency-Key manquant.
bash
curl -X POST "https://<base-api>/public/v1/conversations/18f2a3b4c5d6/comment" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Idempotency-Key: op-2026-001-abcdef" \
  -H "Content-Type: application/json" \
  -d '{"mailbox":"contact@exemple.com","text":"Relancé le client par téléphone."}'

POST /v1/conversations/{id}/labels#

Scope requis : write:conversations.

Ajoute et/ou retire des étiquettes sur le fil.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
addcorpsnonarrayétiquettes à ajouter (50 maximum par requête, 128 caractères chacune).
removecorpsnonarrayétiquettes à retirer (mêmes plafonds).

Réponses :

  • 200{ "ok": true, "id", "result" }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400 — ni add ni remove, ou plafonds dépassés.

Au moins un des deux tableaux doit être non vide.

POST /v1/conversations/{id}/archive#

Scope requis : write:conversations.

Archive la conversation : elle quitte la boîte de réception sans être supprimée.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.

Réponses :

  • 200{ "ok": true, "id", "result": { "action": "archive", "applied": true } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400 — en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.
  • 404 — conversation introuvable dans la boîte demandée.

Action idempotente : archiver une conversation déjà archivée ne produit aucun effet supplémentaire.

L’en-tête Idempotency-Key est facultatif pour une clé humaine (l’action est naturellement idempotente) mais obligatoire pour une clé de type agent à approbation requise, qui pose une demande de validation : sans lui, la réponse est 400.

Aucun message n’est supprimé — la conversation reste consultable et retrouvable par la recherche.

Aucun événement sortant n’est publié pour cette action.

bash
curl -X POST "https://<base-api>/public/v1/conversations/18f2a3b4c5d6/archive" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"mailbox":"contact@exemple.com"}'

POST /v1/conversations/{id}/reopen#

Scope requis : write:conversations.

Remet la conversation dans la boîte de réception (opération inverse de l’archivage).

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.

Réponses :

  • 200{ "ok": true, "id", "result": { "action": "reopen", "applied": true, "inbox": true } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400 — en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.
  • 404 — conversation introuvable dans la boîte demandée.

Comme pour l’archivage, l’en-tête Idempotency-Key est facultatif pour une clé humaine et obligatoire pour une clé de type agent à approbation requise.

Le nom reopen est conservé pour la parité avec les intégrations existantes ; dans l’interface, l’action s’appelle « remettre dans la boîte de réception ».

Cette action ne sort pas une conversation de la corbeille ni des indésirables : elle rétablit uniquement la boîte de réception.

Aucun événement sortant n’est publié pour cette action.

POST /v1/conversations/{id}/snooze#

Scope requis : write:conversations.

Reporte la conversation : elle est mise de côté puis revient à l’échéance indiquée.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
untilcorpsouistringéchéance du report, date ISO 8601 avec fuseau explicite (…Z ou ±hh:mm), strictement future et à 365 jours au maximum.

Réponses :

  • 200{ "ok": true, "id", "result": { "action": "snooze", "until", "reminderId", "created", "changed" } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400until absent, sans fuseau, dans le passé, au-delà du plafond de 365 jours — ou en-tête Idempotency-Key absent alors que la clé est de type agent à approbation requise.
  • 403 — la clé n’est rattachée à aucun membre identifié : le report est nominatif.
  • 409 — le report a été modifié entre-temps par un autre appareil ou une autre intégration — relire puis réessayer.

Le report ne déplace ni ne supprime aucun message : la conversation est masquée des vues jusqu’à l’échéance, puis revient.

Un report est nominatif : il est posé pour le membre auquel la clé est rattachée, comme lorsqu’il reporte depuis l’interface.

Reposer exactement le même report est sans effet ("changed": false) ; indiquer une autre échéance décale le report existant au lieu d’en créer un second.

Une clé de type agent à approbation requise fait passer l’action par la file de validation : si l’échéance demandée est déjà dépassée au moment de la validation, l’opération est refusée plutôt que reportée à une date arbitraire.

Aucun événement sortant n’est publié pour cette action.

bash
curl -X POST "https://<base-api>/public/v1/conversations/18f2a3b4c5d6/snooze" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"mailbox":"contact@exemple.com","until":"2026-08-01T09:00:00Z"}'

GET /v1/search#

Scope requis : read:conversations.

Recherche dans les conversations d’une boîte, via le moteur de recherche de l’espace.

ParamètreEmplacementRequisTypeDétail
mailboxrequêteouistringboîte ciblée, obligatoirement accessible à la clé.
qrequêteouistringtexte recherché, 500 caractères maximum.
limitrequêtenoninteger1 à 100, défaut 25.

Réponses :

  • 200{ "mailbox", "query", "results": [...] }

↑ Index des routes

Envoi de message#

POST /v1/conversations/{id}/send#

Scope requis : mail:send.

Envoie une réponse dans un fil existant, au nom de la boîte indiquée.

ParamètreEmplacementRequisTypeDétail
Idempotency-Keyen-têteouistring8 à 200 caractères imprimables ; un rejeu avec la même valeur et la même charge ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé 409, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée 409, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
tocorpsouiarraydestinataires principaux.
cccorpsnonarraydestinataires en copie.
bcccorpsnonarraydestinataires en copie cachée.
subjectcorpsouistringobjet du message.
bodycorpsouistringcorps du message (HTML accepté).
inReplyTocorpsnonstringidentifiant du message auquel on répond.
prioritycorpsnonstringpriorité déclarée du message.
attachmentscorpsnonarraypièces jointes. Chaque entrée est un objet { "contentBase64", "filename", "mimeType" } : contentBase64 est obligatoire et doit être du base64 canonique strict (alphabet standard, longueur multiple de 4, bits de bourrage nuls — sinon 400) ; filename accepte l’alias name et mimeType l’alias type, tous deux facultatifs. Le type déclaré n’est pas repris tel quel : le serveur inspecte les premiers octets et impose le type réellement détecté. Plafonds : voir « Plafonds des routes d’envoi ».
dlpWarningTokencorpsnonstringjeton à renvoyer pour confirmer un envoi malgré un avertissement de prévention de fuite de données.

Réponses :

  • 200{ "id", "threadId", "accepted", "providerMessageId", "receiptSource", "dlpStatus" }. dlpStatus vaut "ok", ou "warning_required" quand un avertissement de prévention de fuite de données a mordu et a été acquitté via dlpWarningToken : dans ce cas aussi, le message est bien parti. Le corps porte "webhookDeferred": true quand l’événement message.sent n’a pas pu être mis en file (le message est parti, la notification sortante est perdue). Un rejeu à l’identique renvoie le même corps, avec l’en-tête X-EPB-Idempotency: hit (rejeu reconnu par le verrou partagé) ou hit-mem (rejeu servi par le cache de l’instance qui a traité l’envoi initial). Le premier envoi ne porte pas cet en-tête, ou la valeur miss.
  • 400to ou subject manquant, en-tête Idempotency-Key absent / hors format (8 à 200 caractères imprimables), pièce jointe en base64 non canonique, adresse dont la forme n’est pas exploitable, ou liste d’adresses hors plafond — par champ (to/cc/bcc) ou au total du message.
  • 403 — interrupteur d’envoi fermé, politique de prévention de fuite de données bloquante, clé sans droit d’écriture sur la boîte, ou clé de type agent (refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte mail:send).
  • 409 — quatre cas distincts, tous sans aucun envoi : (a) même Idempotency-Key rejouée avec une charge différente — corps { "code": "IDEMPOTENCY_KEY_REUSED", "idempotencyKeyReused": true } et en-tête X-EPB-Idempotency: key-reused ; (a bis) Idempotency-Key déjà enregistrée par une autre voie d’envoi de l’espace (ou par une version antérieure de l’empreinte de charge utile) : impossible de prouver que le reçu enregistré correspond à votre demande — corps { "code": "IDEMPOTENCY_KEY_UNVERIFIABLE", "idempotencyKeyUnverifiable": true } et en-tête X-EPB-Idempotency: key-unverifiable, à rejouer avec une nouvelle valeur ; voir la note sur la portée exacte de cette garantie ; (b) envoi déjà en cours pour cette clé — en-tête X-EPB-Idempotency: duplicate-suppressed, corps { "duplicate": true, "outcomeUnknown": bool } ; (c) avertissement de prévention de fuite de données à confirmer — { "dlpStatus": "warning_required", "dlpWarningToken" }, à renvoyer dans dlpWarningToken pour confirmer.
  • 413 — pièces jointes hors plafond (nombre ou volume cumulé) — refus avant tout appel sortant, aucun message n’est parti.
  • 502 — le fournisseur n’a pas rendu de reçu exploitable : { "error": "reçu d’envoi indisponible", "outcomeUnknown": true }. L’issue est ambiguë — le message a pu partir. Ne pas réessayer avec une nouvelle Idempotency-Key (doublon garanti) : rejouer la MÊME clé, ou vérifier le fil avant toute relance.
  • 503 — service d’envoi indisponible, verrou d’envoi occupé, ou autorisation d’écriture indisponible — réessayable. Un { "outcomeUnknown": true } dans le corps signale ici aussi une issue ambiguë (message parti, confirmation perdue).

Le fil ciblé vient de l’URL : le corps ne peut pas le contredire.

Chaque champ d’adresses (to, cc, bcc) accepte une chaîne ("a@x.fr, b@y.fr"), un tableau de chaînes, ou un tableau d’objets { "email", "name" }. Deux bornes s’appliquent, avec les mêmes valeurs pour toutes ces formes : une par champ et une par MESSAGE (to + cc + bcc réunis) — to et cc peuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en 400, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».

Une adresse dont la forme n’est pas exploitable est refusée en 400 avant tout envoi : objet sans champ email (ou address), valeur qui n’est ni une chaîne ni un objet, ou entrée unique portant deux adresses (Nom <a@x.fr> <b@y.fr>). Aucune adresse n’est devinée ni convertie de force : la requête entière est refusée. Comme toutes les erreurs des routes d’envoi, la réponse porte une phrase explicative, pas un code machine — testez le statut 400, pas le texte.

Portée exacte de la garantie d’idempotence. La règle appliquée est : le service ne rend jamais le reçu d’un message dont il ne peut pas prouver qu’il correspond à votre demande. La preuve est une empreinte de la charge utile, enregistrée avec la réservation par les routes d’envoi de l’API. Trois issues, jamais deux : (a) empreinte identique → 200 + le reçu d’origine (X-EPB-Idempotency: hit ou hit-mem) ; (b) empreinte comparable mais différente → 409 IDEMPOTENCY_KEY_REUSED (X-EPB-Idempotency: key-reused) ; (c) rien de comparable409 IDEMPOTENCY_KEY_UNVERIFIABLE (X-EPB-Idempotency: key-unverifiable). Aucun message n’est envoyé dans les cas (b) et (c). Le cas (c) se produit quand votre valeur se heurte à une réservation posée par une autre voie d’envoi de l’espace — l’application elle-même partage le même verrou et la même formule de clé, sans empreinte — ou par une version antérieure de l’empreinte : rejouez alors avec une nouvelle valeur, votre message n’a jamais été envoyé. Contrepartie assumée : après un changement de l’algorithme d’empreinte, un rejeu par ailleurs légitime peut recevoir ce même 409 pendant la durée de vie des réservations ; repartir avec une nouvelle valeur peut alors produire un doublon — visible, plutôt qu’une perte silencieuse. Le code distinct des deux 409 rend ce cas diagnosticable. En pratique, utilisez une valeur unique par message (un identifiant aléatoire, jamais un compteur ni un identifiant métier partagé avec un autre système) : le cas (c) devient inatteignable.

Plafond d’envoi : voir « Limites de fréquence ». Il est appliqué par instance de service, donc le plafond agrégé observé peut être supérieur à la valeur affichée ; ne vous en servez pas comme d’une garantie de débit maximal.

Ce qui garde l’envoi, précisément — (1) le scope mail:send est strictement opt-in : jamais pré-coché, jamais impliqué par un autre scope, accordé uniquement par un administrateur de l’espace ; (2) une clé de type agent est refusée en 403 sur toute route d’envoi, sans exception ; (3) un interrupteur d’envoi global, qui doit être ouvert pour que le moindre message parte — mais cet interrupteur est le même que celui de l’application : dès que les membres de l’espace peuvent envoyer du courrier depuis Trame, il est ouvert. Ne le comptez donc pas comme une barrière propre à l’API : les deux vraies portes côté intégration sont le scope et le type de la clé. Fermé, il fait répondre 403 avant tout appel sortant.

Clé de type agent : refus 403 sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte déjà mail:send. Deux barrières distinctes, à ne pas confondre : accorder mail:send à une clé agent est refusé en 400 à la création comme à l’édition des droits ; et si une clé de ce type porte malgré tout le scope (droit accordé avant cette règle, ou clé importée), l’envoi est refusé au moment de l’appel. L’envoi n’entre jamais dans la file d’approbation humaine : il n’y a donc aucune approbation capable de le débloquer. Pour un droit d’envoi, créez une clé de type humain.

État de déploiement (juillet 2026) : les routes d’envoi de l’API publique ne sont pas encore actives en production — le service qui sert l’API publique n’a pas été redéployé depuis leur ajout, elles répondent donc 404 comme une route inconnue. C’est un état conjoncturel, pas une garantie : au premier déploiement de ce service, elles s’ouvrent, gardées uniquement par le scope et le type de la clé décrits ci-dessus.

bash
curl -X POST "https://<base-api>/public/v1/conversations/18f2a3b4c5d6/send" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Idempotency-Key: op-2026-001-abcdef" \
  -H "Content-Type: application/json" \
  -d '{"mailbox":"contact@exemple.com","to":["candidat@exemple.com"],"subject":"Réponse à votre candidature","body":"<p>Bonjour,</p>"}'

POST /v1/send#

Scope requis : mail:send.

Envoie un nouveau message au nom de la boîte indiquée.

ParamètreEmplacementRequisTypeDétail
Idempotency-Keyen-têteouistring8 à 200 caractères imprimables ; un rejeu avec la même valeur et la même charge ne produit jamais un second effet et renvoie le premier résultat. Réutiliser la même valeur avec une charge différente est refusé 409, jamais silencieusement accepté. Sur les routes d’envoi, une valeur qui se heurte à une réservation posée par une autre voie d’envoi de l’espace est également refusée 409, avec un code distinct — voir la note « Portée exacte de la garantie d’idempotence » de la route.
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
tocorpsouiarraydestinataires principaux.
cccorpsnonarraydestinataires en copie.
bcccorpsnonarraydestinataires en copie cachée.
subjectcorpsouistringobjet du message.
bodycorpsouistringcorps du message (HTML accepté).
threadIdcorpsnonstringfil auquel rattacher le message.
inReplyTocorpsnonstringidentifiant du message auquel on répond.
prioritycorpsnonstringpriorité déclarée du message.
attachmentscorpsnonarraypièces jointes. Chaque entrée est un objet { "contentBase64", "filename", "mimeType" } : contentBase64 est obligatoire et doit être du base64 canonique strict (alphabet standard, longueur multiple de 4, bits de bourrage nuls — sinon 400) ; filename accepte l’alias name et mimeType l’alias type, tous deux facultatifs. Le type déclaré n’est pas repris tel quel : le serveur inspecte les premiers octets et impose le type réellement détecté. Plafonds : voir « Plafonds des routes d’envoi ».
dlpWarningTokencorpsnonstringjeton confirmant un envoi malgré un avertissement de prévention de fuite de données.

Réponses :

  • 200 — même corps que la route précédente (dlpStatus, webhookDeferred), mêmes en-têtes d’idempotence (hit / hit-mem).
  • 400 — mêmes refus de charge utile que la route précédente (champs obligatoires, Idempotency-Key, base64, forme des adresses, listes d’adresses hors plafond par champ ou au total du message).
  • 403 — interrupteur d’envoi fermé, contenu bloqué, clé en lecture seule sur la boîte, ou clé de type agent (refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte mail:send).
  • 409 — mêmes conflits que la route précédente (clé d’idempotence réutilisée avec une autre charge, clé déjà enregistrée par une autre voie d’envoi et donc invérifiable, envoi déjà en cours, avertissement de prévention de fuite de données à confirmer) — aucun n’envoie de message.
  • 413 — pièces jointes hors plafond — aucun message n’est parti.
  • 502 — reçu d’envoi indisponible, outcomeUnknown: true — issue ambiguë, ne pas relancer avec une nouvelle clé d’idempotence.
  • 503 — service d’envoi indisponible ou verrou occupé — réessayable.

Mêmes garde-fous que POST /v1/conversations/{id}/send (scope opt-in, refus des clés de type agent, interrupteur d’envoi partagé avec l’application, idempotence, plafond dédié appliqué par instance de service) et même état de déploiement.

Chaque champ d’adresses (to, cc, bcc) accepte une chaîne ("a@x.fr, b@y.fr"), un tableau de chaînes, ou un tableau d’objets { "email", "name" }. Deux bornes s’appliquent, avec les mêmes valeurs pour toutes ces formes : une par champ et une par MESSAGE (to + cc + bcc réunis) — to et cc peuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en 400, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».

Une adresse dont la forme n’est pas exploitable est refusée en 400 avant tout envoi : objet sans champ email (ou address), valeur qui n’est ni une chaîne ni un objet, ou entrée unique portant deux adresses (Nom <a@x.fr> <b@y.fr>). Aucune adresse n’est devinée ni convertie de force : la requête entière est refusée. Comme toutes les erreurs des routes d’envoi, la réponse porte une phrase explicative, pas un code machine — testez le statut 400, pas le texte.

Portée exacte de la garantie d’idempotence. La règle appliquée est : le service ne rend jamais le reçu d’un message dont il ne peut pas prouver qu’il correspond à votre demande. La preuve est une empreinte de la charge utile, enregistrée avec la réservation par les routes d’envoi de l’API. Trois issues, jamais deux : (a) empreinte identique → 200 + le reçu d’origine (X-EPB-Idempotency: hit ou hit-mem) ; (b) empreinte comparable mais différente → 409 IDEMPOTENCY_KEY_REUSED (X-EPB-Idempotency: key-reused) ; (c) rien de comparable409 IDEMPOTENCY_KEY_UNVERIFIABLE (X-EPB-Idempotency: key-unverifiable). Aucun message n’est envoyé dans les cas (b) et (c). Le cas (c) se produit quand votre valeur se heurte à une réservation posée par une autre voie d’envoi de l’espace — l’application elle-même partage le même verrou et la même formule de clé, sans empreinte — ou par une version antérieure de l’empreinte : rejouez alors avec une nouvelle valeur, votre message n’a jamais été envoyé. Contrepartie assumée : après un changement de l’algorithme d’empreinte, un rejeu par ailleurs légitime peut recevoir ce même 409 pendant la durée de vie des réservations ; repartir avec une nouvelle valeur peut alors produire un doublon — visible, plutôt qu’une perte silencieuse. Le code distinct des deux 409 rend ce cas diagnosticable. En pratique, utilisez une valeur unique par message (un identifiant aléatoire, jamais un compteur ni un identifiant métier partagé avec un autre système) : le cas (c) devient inatteignable.

Clé de type agent : refus 403 sur toute route d’envoi, quel que soit son mode d’approbation et même si elle porte déjà mail:send. Deux barrières distinctes, à ne pas confondre : accorder mail:send à une clé agent est refusé en 400 à la création comme à l’édition des droits ; et si une clé de ce type porte malgré tout le scope (droit accordé avant cette règle, ou clé importée), l’envoi est refusé au moment de l’appel. L’envoi n’entre jamais dans la file d’approbation humaine : il n’y a donc aucune approbation capable de le débloquer. Pour un droit d’envoi, créez une clé de type humain.

↑ Index des routes

Brouillons#

GET /v1/drafts#

Scope requis : drafts:write.

Liste les brouillons de la boîte.

ParamètreEmplacementRequisTypeDétail
mailboxrequêteouistringboîte ciblée, obligatoirement accessible à la clé.
limitrequêtenoninteger1 à 100, défaut 30.

Réponses :

  • 200{ "mailbox", "drafts": [...] }

La lecture des brouillons exige le même scope drafts:write que leur écriture — il n’existe pas de scope de lecture séparé.

POST /v1/drafts#

Scope requis : drafts:write.

Crée un brouillon dans la boîte.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
draftcorpsnonobjectcontenu du brouillon (to, subject, body…). À défaut, le corps entier est utilisé.

Réponses :

  • 201{ "draft": { ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).

drafts:write n’autorise que l’écriture de brouillons : il ne donne aucun droit d’envoi.

bash
curl -X POST "https://<base-api>/public/v1/drafts" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"mailbox":"contact@exemple.com","draft":{"to":["x@exemple.com"],"subject":"Brouillon","body":"<p>…</p>"}}'

PATCH /v1/drafts/{id}#

Scope requis : drafts:write.

Met à jour un brouillon existant.

ParamètreEmplacementRequisTypeDétail
mailboxcorpsouistringboîte ciblée, obligatoirement accessible à la clé.
draftcorpsnonobjectchamps à écrire.

Réponses :

  • 200{ "draft": { ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).

DELETE /v1/drafts/{id}#

Scope requis : drafts:write.

Supprime un brouillon.

ParamètreEmplacementRequisTypeDétail
mailboxrequêteouistringboîte ciblée, obligatoirement accessible à la clé.

Réponses :

  • 200{ "ok": true, "id" }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).

↑ Index des routes

Contacts#

GET /v1/contacts#

Scope requis : read:contacts.

Liste les contacts des carnets accessibles à la clé.

ParamètreEmplacementRequisTypeDétail
bookIdrequêtenonstringrestreint la liste à un carnet.
grouprequêtenonstringrestreint la liste à un groupe (comparaison insensible à la casse).
qrequêtenonstringfiltre texte sur le nom affiché, l’organisation et les adresses.
sortrequêtenonstringname (défaut, ordre alphabétique français) ou updated (plus récemment modifiés d’abord).

Réponses :

  • 200{ "contacts": [...], "total": nombre }total est le nombre de contacts correspondant au filtre, contacts en contient au plus 2 000 (troncature ; il n’y a pas de pagination sur cette route). Les contacts fusionnés sont exclus, et le résumé de fusion (mergeLog) n’est rendu que par la route de détail.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

GET /v1/contacts/{id}#

Scope requis : read:contacts.

Détail d’un contact.

Réponses :

  • 200{ "contact": { ... } } — forme complète, mergeLog inclus.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

POST /v1/contacts#

Scope requis : write:contacts.

Crée un contact dans un carnet.

ParamètreEmplacementRequisTypeDétail
bookIdcorpsouistringcarnet destinataire, obligatoire ; la clé doit pouvoir y écrire. Un carnet inconnu répond 404 BOOK_NOT_FOUND.
displayNamecorpsnonstringnom affiché, 120 caractères maximum ; il peut différer de l’état civil. À défaut, il est déduit de givenName/familyName, puis de la première adresse, puis de organization.
givenNamecorpsnonstringprénom, 80 caractères maximum.
familyNamecorpsnonstringnom de famille, 80 caractères maximum.
emailscorpsnonarrayadresses, sous la forme [{ "value": "jean@exemple.com", "type": "work" }]objets, pas des chaînes : une entrée sans value valide est ignorée. 10 entrées maximum, doublons retirés.
phonescorpsnonarraytéléphones, même forme { "value", "type" }, 10 entrées maximum.
organizationcorpsnonstringorganisation, 120 caractères maximum.
jobTitlecorpsnonstringintitulé de poste, 120 caractères maximum.
groupscorpsnonarraygroupes (chaînes), 20 maximum, 60 caractères chacun.
notescorpsnonstringnotes libres, 2 000 caractères maximum.

Réponses :

  • 200{ "contact": { ... }, "existed": true }fusion, pas création : un contact du même carnet portait déjà la même clé de déduplication (adresse principale normalisée, ou nom affiché à défaut d’adresse). Les champs fournis écrasent les anciens, l’identifiant et l’auteur d’origine sont conservés.
  • 201{ "contact": { ... } } — contact créé.
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400 — contact vide (EMPTY_CONTACT : ni nom, ni adresse, ni organisation) ou charge refusée (VALIDATION_FAILED).
  • 404BOOK_NOT_FOUNDbookId inconnu dans cet espace.

Les champs du contact s’écrivent à la racine du corps, ou dans un objet contact — les deux formes sont acceptées.

Un contact doit porter au moins un nom, une adresse ou une organisation, sinon 400 EMPTY_CONTACT.

Créer deux fois le même contact dans le même carnet ne produit jamais de doublon : le second appel renvoie 200 avec "existed": true. Testez donc existed, pas le code 201, si vous devez distinguer une création d’une mise à jour.

L’omission de bookId n’est pas traitée comme une erreur de validation : elle produit une erreur générique. Fournissez toujours bookId.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

bash
curl -X POST "https://<base-api>/public/v1/contacts" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"bookId":"book-exemple","displayName":"Jean Dupont (RH)","givenName":"Jean","familyName":"Dupont","emails":[{"value":"jean@exemple.com","type":"work"}]}'

PATCH /v1/contacts/{id}#

Scope requis : write:contacts.

Met à jour un contact.

ParamètreEmplacementRequisTypeDétail
displayNamecorpsnonstringmêmes champs qu’à la création (racine du corps ou objet contact).

Réponses :

  • 200{ "contact": { ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404CONTACT_NOT_FOUND — contact inconnu ou fusionné.

Le carnet d’appartenance est immuable par cette route : bookId fourni ici est ignoré.

La piste d’audit de fusion (mergedFrom, mergeLog) et l’auteur d’origine sont préservés.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

DELETE /v1/contacts/{id}#

Scope requis : write:contacts.

Supprime un contact.

Réponses :

  • 200{ "ok": true }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404CONTACT_NOT_FOUND.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

↑ Index des routes

Agenda#

GET /v1/calendars#

Scope requis : read:calendar.

Liste les calendriers accessibles à la clé.

Réponses :

  • 200{ "calendars": [...], "me": "adresse de la clé" } — calendriers shared de l’espace et calendriers personal du membre auquel la clé est rattachée.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

POST /v1/calendars#

Scope requis : write:calendar.

Crée un calendrier.

ParamètreEmplacementRequisTypeDétail
namecorpsouistringnom du calendrier, obligatoire, 80 caractères maximum — vide ou absent : 400 INVALID_ARGUMENT.
scopecorpsnonstringpersonal (défaut) ou shared. Un calendrier shared est visible par tout l’espace et sa création est réservée à la clé de l’administrateur de l’espace (sinon 403 CALENDAR_SCOPE_FORBIDDEN).
colorcorpsnonstringcouleur d’affichage, 20 caractères maximum.

Réponses :

  • 201{ "calendar": { "id", "name", "color", "scope", "ownerUid", ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400INVALID_ARGUMENTname vide ou absent.
  • 403CALENDAR_SCOPE_FORBIDDENscope: "shared" demandé par une clé non administratrice.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

bash
curl -X POST "https://<base-api>/public/v1/calendars" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Entretiens","scope":"personal","color":"#2563eb"}'

DELETE /v1/calendars/{id}#

Scope requis : write:calendar.

Supprime un calendrier et tous ses événements.

Réponses :

  • 200{ "ok": true, "deletedEvents": nombre }
  • 403FORBIDDEN — calendrier shared : seule la clé de l’administrateur de l’espace peut le supprimer.
  • 404CALENDAR_NOT_FOUND.

Suppression en cascade : tous les événements du calendrier sont supprimés, et deletedEvents en donne le compte. Il n’y a pas de corbeille pour cette opération.

Un calendrier ne se lit ni ne se modifie unitairement par cette API : seules la liste, la création et la suppression sont exposées.

Cette route n’est pas prise en charge par la file d’approbation : une clé de type agent à approbation requise reçoit 404, jamais 202.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

GET /v1/calendar-events#

Scope requis : read:calendar.

Liste les événements des calendriers accessibles, sur une fenêtre de temps.

ParamètreEmplacementRequisTypeDétail
fromrequêtenonintegerdébut de la fenêtre, en millisecondes depuis epoch (pas une date ISO). Défaut : il y a 30 jours.
torequêtenonintegerfin de la fenêtre, en millisecondes depuis epoch. Défaut : dans 60 jours.
calendarIdrequêtenonstringrestreint la liste à un calendrier (inconnu ou hors de portée : 404 / 403).

Réponses :

  • 200{ "events": [...], "occurrences": [...], "window": { "fromMs", "toMs" }, "remindersDue": [...], "me" }. events porte les événements (récurrents inclus, au plus 1 000) ; occurrences développe les récurrences dans la fenêtre sous la forme { "eventId", "calendarId", "startMs", "endMs" }, triées par début et plafonnées à 5 000. Les événements annulés sont exclus. Ces deux plafonds sont des troncatures : il n’y a pas de pagination — resserrez from/to pour rester en deçà.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

POST /v1/calendar-events#

Scope requis : write:calendar.

Crée un événement dans un calendrier.

ParamètreEmplacementRequisTypeDétail
calendarIdcorpsouistringcalendrier destinataire, obligatoire ; la clé doit pouvoir y écrire. Inconnu : 404 CALENDAR_NOT_FOUND.
titlecorpsouistringintitulé, obligatoire, 200 caractères maximum — vide : 400 TITLE_REQUIRED.
startMscorpsouiintegerdébut, en millisecondes depuis epoch (nombre). Ce n’est pas une date ISO 8601 : une chaîne "2026-08-01T09:00:00Z" est refusée (400 INVALID_DATES).
endMscorpsouiintegerfin, en millisecondes depuis epoch, strictement supérieure à startMs (sinon 400 END_BEFORE_START) et à 366 jours au maximum (sinon 400 DURATION_TOO_LONG).
allDaycorpsnonbooleanjournée entière : les bornes sont recadrées sur des jours UTC pleins, fin exclusive.
descriptioncorpsnonstringdescription, 5 000 caractères maximum.
locationcorpsnonstringlieu, 200 caractères maximum.
conferenceLinkcorpsnonstringlien de visioconférence ; doit commencer par http:// ou https:// (sinon 400 INVALID_CONFERENCE_LINK).
timezonecorpsnonstringfuseau de l’heure murale, défaut Europe/Paris.
attendeescorpsnonarrayparticipants. Aucune invitation n’est envoyée par courriel : la réponse se fait dans l’application.
reminderscorpsnonarrayrappels, sous la forme [{ "minutes": 15 }].
recurrencecorpsnonobjectrègle de répétition.

Réponses :

  • 200{ "event": { ... }, "existed": true, "conflicts": [...] }fusion, pas création : un événement importé portant le même identifiant externe existait déjà.
  • 201{ "event": { ... }, "conflicts": [...] } — événement créé. conflicts liste les chevauchements détectés dans le même calendrier ; ils sont signalés, jamais bloquants.
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400TITLE_REQUIRED, INVALID_DATES, END_BEFORE_START, DURATION_TOO_LONG ou INVALID_CONFERENCE_LINK.
  • 404CALENDAR_NOT_FOUNDcalendarId inconnu.

Les champs de l’événement s’écrivent à la racine du corps, ou dans un objet event — les deux formes sont acceptées.

Toutes les dates de cette API sont des millisecondes depuis epoch, jamais des chaînes ISO 8601.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

bash
curl -X POST "https://<base-api>/public/v1/calendar-events" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"calendarId":"calendar-exemple","title":"Entretien candidat","startMs":1785574800000,"endMs":1785576600000}'

GET /v1/calendar-events/{id}#

Scope requis : read:calendar.

Détail d’un événement.

Réponses :

  • 200{ "event": { ... }, "me" }
  • 404EVENT_NOT_FOUND.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

PATCH /v1/calendar-events/{id}#

Scope requis : write:calendar.

Met à jour un événement.

ParamètreEmplacementRequisTypeDétail
titlecorpsnonstringmêmes champs qu’à la création (racine du corps ou objet event) ; les champs absents gardent leur valeur.

Réponses :

  • 200{ "event": { ... }, "conflicts": [...] }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404EVENT_NOT_FOUND.

Le calendrier d’appartenance est immuable par cette route.

Poser "status": "cancelled" annule l’événement : il sort des listes sans être supprimé.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

DELETE /v1/calendar-events/{id}#

Scope requis : write:calendar.

Supprime un événement.

Réponses :

  • 200{ "ok": true }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404EVENT_NOT_FOUND.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

↑ Index des routes

Tâches#

GET /v1/tasks#

Scope requis : read:tasks.

Liste les tâches de l’espace.

ParamètreEmplacementRequisTypeDétail
statusrequêtenonstringfiltre par statut.
assigneerequêtenonstringfiltre par adresse assignée.
teamIdrequêtenonstringfiltre par équipe.
threadIdrequêtenonstringfiltre par fil de conversation rattaché.
qrequêtenonstringfiltre texte sur l’intitulé et les notes.

Réponses :

  • 200{ "tasks": [...], "groups": { ... }, "remindersDue": [...], "me" }tasks est trié et tronqué à 2 000 entrées (pas de pagination) ; groups compte les tâches par statut sur l’ensemble filtré.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

POST /v1/tasks#

Scope requis : write:tasks.

Crée une tâche.

ParamètreEmplacementRequisTypeDétail
titlecorpsouistringintitulé, obligatoire, 200 caractères maximum — vide : 400 TITLE_REQUIRED.
notescorpsnonstringnotes libres, 5 000 caractères maximum.
statuscorpsnonstringstatut initial ; une valeur inconnue retombe sur todo.
prioritycorpsnonstringpriorité ; une valeur inconnue retombe sur normal.
assigneecorpsnonstringadresse de la personne assignée ; une adresse invalide est ignorée (champ vidé).
teamIdcorpsnonstringéquipe destinataire.
dueMscorpsnonintegeréchéance en millisecondes depuis epoch (pas une date ISO). Obligatoire si recurrence est fourni, sinon 400 RECURRENCE_NEEDS_DUE.
threadIdcorpsnonstringfil de conversation rattaché. Avec title, il forme la clé de déduplication de la tâche.
recurrencecorpsnonobjectrègle de répétition ; l’occurrence suivante est créée à la complétion.
reminderscorpsnonarrayrappels, sous la forme [{ "minutes": 60 }].
sourcecorpsnonstringprovenance ; seules manual (défaut) et mail sont acceptées d’un client. Toute autre valeur est refusée en 403 SOURCE_FORBIDDEN.

Réponses :

  • 200{ "task": { ... }, "existed": true }aucune création : une tâche portant le même threadId et le même intitulé existait déjà ; elle est renvoyée telle quelle, sans être modifiée.
  • 201{ "task": { ... } } — tâche créée.
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 400TITLE_REQUIRED ou RECURRENCE_NEEDS_DUE.
  • 403SOURCE_FORBIDDENsource réservée au serveur.

Les champs de la tâche s’écrivent à la racine du corps, ou dans un objet task — les deux formes sont acceptées.

La déduplication ne joue que si threadId est fourni : sans lui, deux appels identiques créent deux tâches.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

bash
curl -X POST "https://<base-api>/public/v1/tasks" \
  -H "X-Api-Key: <votre-clé>" \
  -H "Content-Type: application/json" \
  -d '{"title":"Relancer le dossier","assignee":"julie@exemple.com","dueMs":1785574800000}'

GET /v1/tasks/{id}#

Scope requis : read:tasks.

Détail d’une tâche.

Réponses :

  • 200{ "task": { ... }, "me" }
  • 404TASK_NOT_FOUND.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

PATCH /v1/tasks/{id}#

Scope requis : write:tasks.

Met à jour une tâche.

ParamètreEmplacementRequisTypeDétail
titlecorpsnonstringmêmes champs qu’à la création (racine du corps ou objet task) ; les champs absents gardent leur valeur.

Réponses :

  • 200{ "task": { ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404TASK_NOT_FOUND.

La provenance (source) est immuable : une tâche posée par le serveur ne peut pas être « blanchie » en manual.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

DELETE /v1/tasks/{id}#

Scope requis : write:tasks.

Supprime une tâche.

Réponses :

  • 200{ "ok": true }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).
  • 404TASK_NOT_FOUND.

Cette route exige que la clé soit rattachée à un membre identifié de l’espace : sinon la réponse est 403 { "error": "identité membre incomplète pour cette route" }, avant tout accès aux données.

Les erreurs de cette route portent un code machine ({ "error": "CODE" }), pas une phrase — voir « Codes d’erreur des routes contacts, agenda et tâches ».

↑ Index des routes

Signatures et analytics#

GET /v1/signatures#

Scope requis : read:conversations.

Liste les signatures configurées pour l’espace.

Réponses :

  • 200{ "signatures": [...] }

Cette route relève de la famille « conversations » : elle exige read:conversations, pas un scope dédié.

GET /v1/analytics/summary#

Scope requis : read:analytics.

Résumé analytique de l’espace (volumes).

ParamètreEmplacementRequisTypeDétail
fromrequêtenonstringdébut de période (date ISO).
torequêtenonstringfin de période (date ISO).

Réponses :

  • 200{ "summary": { "volumes": {...}, "metricAvailability": "volumes_only" } }
bash
curl "https://<base-api>/public/v1/analytics/summary?from=2026-07-01&to=2026-07-31" \
  -H "X-Api-Key: <votre-clé>"

↑ Index des routes

Webhooks#

GET /v1/webhooks/test#

Scope requis : write:conversations.

Émet un événement de test vers les souscriptions webhook de l’espace.

Réponses :

  • 200{ "ok": true, "event": { ... } }
  • 202{ "approvalRequired": true, "approval": { ... } } — la clé est de type agent à approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-tête Idempotency-Key est obligatoire dans ce cas (sinon 400).

La gestion des souscriptions elles-mêmes ne passe pas par cette API à clé, mais par les réglages de l’espace.

C’est la seule route GET considérée comme une écriture : elle produit un effet observable (livraison sortante). C’est aussi la seule route GET qui peut répondre 202 à une clé de type agent à approbation requise.

↑ Index des routes

MCP#

POST /mcp#

Scope requis : mcp.

Point d’entrée JSON-RPC 2.0 du serveur MCP (agents IA).

Réponses :

  • 200 — réponse JSON-RPC de l’outil appelé.
  • 503 — service MCP indisponible sur cet espace.

Le scope exigé n’est pas fixe : il est dérivé de l’outil appelé (par exemple read:conversations pour une recherche, drafts:write pour la création d’un brouillon). Un agent n’a donc jamais plus de droits que sa clé.

Aucun outil MCP ne peut envoyer de message.

Le gate d’approbation REST ne s’applique pas ici : le serveur MCP gère lui-même la file d’approbation d’une clé de type agent. Cette route ne répond donc jamais 202.

↑ Index des routes

Ce qui n’est pas une route de cette API#

Le lien de consultation invité (GET / POST /guest) existe dans le produit mais s’authentifie par un jeton signé propre à un fil, pas par une clé API : il ne fait pas partie de la surface d’intégration décrite ici. La gestion des clés et des souscriptions webhook se fait depuis les réglages de votre espace, pas par cette API.