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>/publicLe chemin d’une route se concatène à cette base :
GET /v1/conversationss’appelle donchttps://<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éthode | Chemin | Scope | Effet |
|---|---|---|---|
GET | /v1/conversations | read:conversations | Liste les conversations de la boîte demandée, de la plus récente à la plus ancienne. |
GET | /v1/conversations/{id} | read:conversations | Détail d’une conversation (fil complet avec ses messages). |
POST | /v1/conversations/{id}/assign | write:conversations | Assigne la conversation à une personne, avec un statut et une équipe optionnels. |
POST | /v1/conversations/{id}/comment | write:conversations | Ajoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant. |
POST | /v1/conversations/{id}/labels | write:conversations | Ajoute et/ou retire des étiquettes sur le fil. |
POST | /v1/conversations/{id}/archive | write:conversations | Archive la conversation : elle quitte la boîte de réception sans être supprimée. |
POST | /v1/conversations/{id}/reopen | write:conversations | Remet la conversation dans la boîte de réception (opération inverse de l’archivage). |
POST | /v1/conversations/{id}/snooze | write:conversations | Reporte la conversation : elle est mise de côté puis revient à l’échéance indiquée. |
GET | /v1/search | read:conversations | Recherche dans les conversations d’une boîte, via le moteur de recherche de l’espace. |
POST | /v1/conversations/{id}/send | mail:send | Envoie une réponse dans un fil existant, au nom de la boîte indiquée. |
POST | /v1/send | mail:send | Envoie un nouveau message au nom de la boîte indiquée. |
GET | /v1/drafts | drafts:write | Liste les brouillons de la boîte. |
POST | /v1/drafts | drafts:write | Crée un brouillon dans la boîte. |
PATCH | /v1/drafts/{id} | drafts:write | Met à jour un brouillon existant. |
DELETE | /v1/drafts/{id} | drafts:write | Supprime un brouillon. |
GET | /v1/contacts | read:contacts | Liste les contacts des carnets accessibles à la clé. |
GET | /v1/contacts/{id} | read:contacts | Détail d’un contact. |
POST | /v1/contacts | write:contacts | Crée un contact dans un carnet. |
PATCH | /v1/contacts/{id} | write:contacts | Met à jour un contact. |
DELETE | /v1/contacts/{id} | write:contacts | Supprime un contact. |
GET | /v1/calendars | read:calendar | Liste les calendriers accessibles à la clé. |
POST | /v1/calendars | write:calendar | Crée un calendrier. |
DELETE | /v1/calendars/{id} | write:calendar | Supprime un calendrier et tous ses événements. |
GET | /v1/calendar-events | read:calendar | Liste les événements des calendriers accessibles, sur une fenêtre de temps. |
POST | /v1/calendar-events | write:calendar | Crée un événement dans un calendrier. |
GET | /v1/calendar-events/{id} | read:calendar | Détail d’un événement. |
PATCH | /v1/calendar-events/{id} | write:calendar | Met à jour un événement. |
DELETE | /v1/calendar-events/{id} | write:calendar | Supprime un événement. |
GET | /v1/tasks | read:tasks | Liste les tâches de l’espace. |
POST | /v1/tasks | write:tasks | Crée une tâche. |
GET | /v1/tasks/{id} | read:tasks | Détail d’une tâche. |
PATCH | /v1/tasks/{id} | write:tasks | Met à jour une tâche. |
DELETE | /v1/tasks/{id} | write:tasks | Supprime une tâche. |
GET | /v1/signatures | read:conversations | Liste les signatures configurées pour l’espace. |
GET | /v1/analytics/summary | read:analytics | Résumé analytique de l’espace (volumes). |
GET | /v1/webhooks/test | write:conversations | Émet un événement de test vers les souscriptions webhook de l’espace. |
POST | /mcp | mcp | Point 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ée | Plafond 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ément | Plafond | Refus |
|---|---|---|
| Pièces jointes (nombre) | 50 par message | 413 |
| Pièces jointes (volume cumulé) | 25 Mo | 413 |
Adresses par champ (to, cc, bcc) | 500 | 400 |
Adresses par message (to + cc + bcc réunis) | 500 | 400 |
| Longueur d’une adresse (nom d’affichage compris) | 640 caractères | 400 |
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 etcc= 400, aucun champ ne dépasse 500, mais le total (800) dépasse 500 : la requête est refusée en400, 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 :
| Familles | Forme | À 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. |
| Code | Signification |
|---|---|
400 | requê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 »). |
401 | clé 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. |
403 | scope 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). |
404 | route inconnue, ou ressource introuvable dans l’espace / la boîte demandée. |
409 | conflit : 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. |
413 | pièces jointes hors plafond (voir « Plafonds des routes d’envoi ») — aucun message n’est parti. |
429 | plafond 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. |
500 | erreur interne — le message ne contient jamais de détail d’implémentation. |
502 | reçu d’envoi indisponible : l’issue est AMBIGUË (le message a pu partir). Voir la note de la route concernée. |
503 | service 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
| Code | Statut | Cas |
|---|---|---|
VALIDATION_FAILED | 400 | contenu du contact refusé par la validation. |
EMPTY_CONTACT | 400 | ni nom, ni adresse, ni organisation — un contact vide n’est pas créé. |
INVALID_ARGUMENT | 400 | requête non exploitable. |
FORBIDDEN | 403 | carnet ou contact hors de portée de la clé. |
BOOK_NOT_FOUND | 404 | bookId inconnu dans cet espace. |
CONTACT_NOT_FOUND | 404 | contact inconnu (ou fusionné). |
METHOD_NOT_ALLOWED | 405 | méthode non exposée sur ce chemin. |
CONTACTS_FAILED | 500 | repli : toute erreur non listée est ramenée à ce code. |
Agenda
| Code | Statut | Cas |
|---|---|---|
TITLE_REQUIRED | 400 | title vide. |
INVALID_DATES | 400 | startMs / endMs absents, non numériques ou nuls. |
END_BEFORE_START | 400 | endMs inférieur ou égal à startMs. |
DURATION_TOO_LONG | 400 | durée supérieure à 366 jours. |
INVALID_CONFERENCE_LINK | 400 | conferenceLink qui ne commence pas par http:// ou https://. |
INVALID_ARGUMENT | 400 | requête non exploitable (par exemple name vide à la création d’un calendrier). |
FORBIDDEN | 403 | calendrier ou événement hors de portée de la clé. |
CALENDAR_SCOPE_FORBIDDEN | 403 | création d’un calendrier shared demandée par une clé non administratrice. |
CALENDAR_NOT_FOUND | 404 | calendarId inconnu dans cet espace. |
EVENT_NOT_FOUND | 404 | événement inconnu. |
METHOD_NOT_ALLOWED | 405 | méthode non exposée sur ce chemin. |
CALENDAR_FAILED | 500 | repli : toute erreur non listée est ramenée à ce code. |
Tâches
| Code | Statut | Cas |
|---|---|---|
TITLE_REQUIRED | 400 | title vide. |
RECURRENCE_NEEDS_DUE | 400 | récurrence demandée sans dueMs. |
INVALID_ARGUMENT | 400 | requête non exploitable. |
SOURCE_FORBIDDEN | 403 | source réservée au serveur (ai, rule, sync) posée par le client — seules manual et mail sont acceptées. |
TASK_NOT_FOUND | 404 | tâche inconnue. |
METHOD_NOT_ALLOWED | 405 | méthode non exposée sur ce chemin. |
TASKS_FAILED | 500 | repli : 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | requête | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
limit | requête | non | integer | 1 à 100, défaut 25. |
pageToken | requête | non | string | jeton de pagination renvoyé par l’appel précédent. |
Réponses :
200—{ "mailbox", "conversations": [...], "nextPageToken": string|null }
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | requête | oui | string | boî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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
assignee | corps | non | string | adresse de la personne assignée, ou null pour désassigner. |
status | corps | non | string | statut applicatif à poser sur le fil. |
teamId | corps | non | string | identifiant d’équipe destinataire. |
Réponses :
200—{ "ok": true, "id", "result" }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).
POST /v1/conversations/{id}/comment#
Scope requis : write:conversations.
Ajoute un commentaire interne au fil — visible par l’équipe, jamais envoyé au correspondant.
| Paramètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
Idempotency-Key | en-tête | oui | string | 8 à 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. |
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
text | corps | oui | string | contenu du commentaire, 10 000 caractères maximum. |
Réponses :
200—{ "ok": true, "id", "result" }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400—textabsent, trop long, ou en-têteIdempotency-Keymanquant.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
add | corps | non | array | étiquettes à ajouter (50 maximum par requête, 128 caractères chacune). |
remove | corps | non | array | étiquettes à retirer (mêmes plafonds). |
Réponses :
200—{ "ok": true, "id", "result" }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400— niaddniremove, 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boî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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400— en-têteIdempotency-Keyabsent alors que la clé est de typeagentà 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-Keyest facultatif pour une clé humaine (l’action est naturellement idempotente) mais obligatoire pour une clé de typeagentà approbation requise, qui pose une demande de validation : sans lui, la réponse est400.
Aucun message n’est supprimé — la conversation reste consultable et retrouvable par la recherche.
Aucun événement sortant n’est publié pour cette action.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boî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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400— en-têteIdempotency-Keyabsent alors que la clé est de typeagentà approbation requise.404— conversation introuvable dans la boîte demandée.
Comme pour l’archivage, l’en-tête
Idempotency-Keyest facultatif pour une clé humaine et obligatoire pour une clé de typeagentà approbation requise.
Le nom
reopenest 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
until | corps | oui | string | é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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400—untilabsent, sans fuseau, dans le passé, au-delà du plafond de 365 jours — ou en-têteIdempotency-Keyabsent alors que la clé est de typeagentà 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.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | requête | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
q | requête | oui | string | texte recherché, 500 caractères maximum. |
limit | requête | non | integer | 1 à 100, défaut 25. |
Réponses :
200—{ "mailbox", "query", "results": [...] }
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
Idempotency-Key | en-tête | oui | string | 8 à 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. |
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
to | corps | oui | array | destinataires principaux. |
cc | corps | non | array | destinataires en copie. |
bcc | corps | non | array | destinataires en copie cachée. |
subject | corps | oui | string | objet du message. |
body | corps | oui | string | corps du message (HTML accepté). |
inReplyTo | corps | non | string | identifiant du message auquel on répond. |
priority | corps | non | string | priorité déclarée du message. |
attachments | corps | non | array | piè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 ». |
dlpWarningToken | corps | non | string | jeton à 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" }.dlpStatusvaut"ok", ou"warning_required"quand un avertissement de prévention de fuite de données a mordu et a été acquitté viadlpWarningToken: dans ce cas aussi, le message est bien parti. Le corps porte"webhookDeferred": truequand l’événementmessage.sentn’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êteX-EPB-Idempotency: hit(rejeu reconnu par le verrou partagé) ouhit-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 valeurmiss.400—toousubjectmanquant, en-têteIdempotency-Keyabsent / 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 typeagent(refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle portemail:send).409— quatre cas distincts, tous sans aucun envoi : (a) mêmeIdempotency-Keyrejouée avec une charge différente — corps{ "code": "IDEMPOTENCY_KEY_REUSED", "idempotencyKeyReused": true }et en-têteX-EPB-Idempotency: key-reused; (a bis)Idempotency-Keydé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êteX-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êteX-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 dansdlpWarningTokenpour 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 nouvelleIdempotency-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+bccréunis) —toetccpeuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en400, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».
Une adresse dont la forme n’est pas exploitable est refusée en
400avant tout envoi : objet sans champaddress), 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 statut400, 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: hitouhit-mem) ; (b) empreinte comparable mais différente →409 IDEMPOTENCY_KEY_REUSED(X-EPB-Idempotency: key-reused) ; (c) rien de comparable →409 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ême409pendant 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 deux409rend 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:sendest strictement opt-in : jamais pré-coché, jamais impliqué par un autre scope, accordé uniquement par un administrateur de l’espace ; (2) une clé de typeagentest refusée en403sur 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épondre403avant tout appel sortant.
Clé de type
agent: refus403sur 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 : accordermail:sendà une cléagentest refusé en400à 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
404comme 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.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
Idempotency-Key | en-tête | oui | string | 8 à 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. |
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
to | corps | oui | array | destinataires principaux. |
cc | corps | non | array | destinataires en copie. |
bcc | corps | non | array | destinataires en copie cachée. |
subject | corps | oui | string | objet du message. |
body | corps | oui | string | corps du message (HTML accepté). |
threadId | corps | non | string | fil auquel rattacher le message. |
inReplyTo | corps | non | string | identifiant du message auquel on répond. |
priority | corps | non | string | priorité déclarée du message. |
attachments | corps | non | array | piè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 ». |
dlpWarningToken | corps | non | string | jeton 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 typeagent(refus systématique sur toute route d’envoi, quel que soit son mode d’approbation et même si elle portemail: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 typeagent, 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+bccréunis) —toetccpeuvent donc être chacun sous la borne par champ et le message être refusé quand même. Un dépassement est refusé en400, jamais tronqué en silence. Voir « Plafonds des routes d’envoi ».
Une adresse dont la forme n’est pas exploitable est refusée en
400avant tout envoi : objet sans champaddress), 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 statut400, 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: hitouhit-mem) ; (b) empreinte comparable mais différente →409 IDEMPOTENCY_KEY_REUSED(X-EPB-Idempotency: key-reused) ; (c) rien de comparable →409 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ême409pendant 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 deux409rend 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: refus403sur 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 : accordermail:sendà une cléagentest refusé en400à 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.
Brouillons#
GET /v1/drafts#
Scope requis : drafts:write.
Liste les brouillons de la boîte.
| Paramètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | requête | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
limit | requête | non | integer | 1 à 100, défaut 30. |
Réponses :
200—{ "mailbox", "drafts": [...] }
La lecture des brouillons exige le même scope
drafts:writeque 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
draft | corps | non | object | contenu du brouillon (to, subject, body…). À défaut, le corps entier est utilisé. |
Réponses :
201—{ "draft": { ... } }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).
drafts:writen’autorise que l’écriture de brouillons : il ne donne aucun droit d’envoi.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | corps | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
draft | corps | non | object | champs à écrire. |
Réponses :
200—{ "draft": { ... } }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).
DELETE /v1/drafts/{id}#
Scope requis : drafts:write.
Supprime un brouillon.
| Paramètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
mailbox | requête | oui | string | boîte ciblée, obligatoirement accessible à la clé. |
Réponses :
200—{ "ok": true, "id" }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).
Contacts#
GET /v1/contacts#
Scope requis : read:contacts.
Liste les contacts des carnets accessibles à la clé.
| Paramètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
bookId | requête | non | string | restreint la liste à un carnet. |
group | requête | non | string | restreint la liste à un groupe (comparaison insensible à la casse). |
q | requête | non | string | filtre texte sur le nom affiché, l’organisation et les adresses. |
sort | requête | non | string | name (défaut, ordre alphabétique français) ou updated (plus récemment modifiés d’abord). |
Réponses :
200—{ "contacts": [...], "total": nombre }—totalest le nombre de contacts correspondant au filtre,contactsen 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,mergeLoginclus.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
bookId | corps | oui | string | carnet destinataire, obligatoire ; la clé doit pouvoir y écrire. Un carnet inconnu répond 404 BOOK_NOT_FOUND. |
displayName | corps | non | string | nom 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. |
givenName | corps | non | string | prénom, 80 caractères maximum. |
familyName | corps | non | string | nom de famille, 80 caractères maximum. |
emails | corps | non | array | adresses, 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. |
phones | corps | non | array | téléphones, même forme { "value", "type" }, 10 entrées maximum. |
organization | corps | non | string | organisation, 120 caractères maximum. |
jobTitle | corps | non | string | intitulé de poste, 120 caractères maximum. |
groups | corps | non | array | groupes (chaînes), 20 maximum, 60 caractères chacun. |
notes | corps | non | string | notes 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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400— contact vide (EMPTY_CONTACT: ni nom, ni adresse, ni organisation) ou charge refusée (VALIDATION_FAILED).404—BOOK_NOT_FOUND—bookIdinconnu 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
200avec"existed": true. Testez doncexisted, pas le code201, si vous devez distinguer une création d’une mise à jour.
L’omission de
bookIdn’est pas traitée comme une erreur de validation : elle produit une erreur générique. Fournissez toujoursbookId.
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 ».
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
displayName | corps | non | string | mêmes champs qu’à la création (racine du corps ou objet contact). |
Réponses :
200—{ "contact": { ... } }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—CONTACT_NOT_FOUND— contact inconnu ou fusionné.
Le carnet d’appartenance est immuable par cette route :
bookIdfourni 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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—CONTACT_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 ».
Agenda#
GET /v1/calendars#
Scope requis : read:calendar.
Liste les calendriers accessibles à la clé.
Réponses :
200—{ "calendars": [...], "me": "adresse de la clé" }— calendrierssharedde l’espace et calendrierspersonaldu 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
name | corps | oui | string | nom du calendrier, obligatoire, 80 caractères maximum — vide ou absent : 400 INVALID_ARGUMENT. |
scope | corps | non | string | personal (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). |
color | corps | non | string | couleur d’affichage, 20 caractères maximum. |
Réponses :
201—{ "calendar": { "id", "name", "color", "scope", "ownerUid", ... } }202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400—INVALID_ARGUMENT—namevide ou absent.403—CALENDAR_SCOPE_FORBIDDEN—scope: "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 ».
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 }403—FORBIDDEN— calendriershared: seule la clé de l’administrateur de l’espace peut le supprimer.404—CALENDAR_NOT_FOUND.
Suppression en cascade : tous les événements du calendrier sont supprimés, et
deletedEventsen 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çoit404, jamais202.
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
from | requête | non | integer | début de la fenêtre, en millisecondes depuis epoch (pas une date ISO). Défaut : il y a 30 jours. |
to | requête | non | integer | fin de la fenêtre, en millisecondes depuis epoch. Défaut : dans 60 jours. |
calendarId | requête | non | string | restreint la liste à un calendrier (inconnu ou hors de portée : 404 / 403). |
Réponses :
200—{ "events": [...], "occurrences": [...], "window": { "fromMs", "toMs" }, "remindersDue": [...], "me" }.eventsporte les événements (récurrents inclus, au plus 1 000) ;occurrencesdé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 — resserrezfrom/topour 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
calendarId | corps | oui | string | calendrier destinataire, obligatoire ; la clé doit pouvoir y écrire. Inconnu : 404 CALENDAR_NOT_FOUND. |
title | corps | oui | string | intitulé, obligatoire, 200 caractères maximum — vide : 400 TITLE_REQUIRED. |
startMs | corps | oui | integer | dé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). |
endMs | corps | oui | integer | fin, en millisecondes depuis epoch, strictement supérieure à startMs (sinon 400 END_BEFORE_START) et à 366 jours au maximum (sinon 400 DURATION_TOO_LONG). |
allDay | corps | non | boolean | journée entière : les bornes sont recadrées sur des jours UTC pleins, fin exclusive. |
description | corps | non | string | description, 5 000 caractères maximum. |
location | corps | non | string | lieu, 200 caractères maximum. |
conferenceLink | corps | non | string | lien de visioconférence ; doit commencer par http:// ou https:// (sinon 400 INVALID_CONFERENCE_LINK). |
timezone | corps | non | string | fuseau de l’heure murale, défaut Europe/Paris. |
attendees | corps | non | array | participants. Aucune invitation n’est envoyée par courriel : la réponse se fait dans l’application. |
reminders | corps | non | array | rappels, sous la forme [{ "minutes": 15 }]. |
recurrence | corps | non | object | rè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éé.conflictsliste les chevauchements détectés dans le même calendrier ; ils sont signalés, jamais bloquants.202—{ "approvalRequired": true, "approval": { ... } }— la clé est de typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400—TITLE_REQUIRED,INVALID_DATES,END_BEFORE_START,DURATION_TOO_LONGouINVALID_CONFERENCE_LINK.404—CALENDAR_NOT_FOUND—calendarIdinconnu.
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 ».
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" }404—EVENT_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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
title | corps | non | string | mê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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—EVENT_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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—EVENT_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 ».
Tâches#
GET /v1/tasks#
Scope requis : read:tasks.
Liste les tâches de l’espace.
| Paramètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
status | requête | non | string | filtre par statut. |
assignee | requête | non | string | filtre par adresse assignée. |
teamId | requête | non | string | filtre par équipe. |
threadId | requête | non | string | filtre par fil de conversation rattaché. |
q | requête | non | string | filtre texte sur l’intitulé et les notes. |
Réponses :
200—{ "tasks": [...], "groups": { ... }, "remindersDue": [...], "me" }—tasksest trié et tronqué à 2 000 entrées (pas de pagination) ;groupscompte 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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
title | corps | oui | string | intitulé, obligatoire, 200 caractères maximum — vide : 400 TITLE_REQUIRED. |
notes | corps | non | string | notes libres, 5 000 caractères maximum. |
status | corps | non | string | statut initial ; une valeur inconnue retombe sur todo. |
priority | corps | non | string | priorité ; une valeur inconnue retombe sur normal. |
assignee | corps | non | string | adresse de la personne assignée ; une adresse invalide est ignorée (champ vidé). |
teamId | corps | non | string | équipe destinataire. |
dueMs | corps | non | integer | échéance en millisecondes depuis epoch (pas une date ISO). Obligatoire si recurrence est fourni, sinon 400 RECURRENCE_NEEDS_DUE. |
threadId | corps | non | string | fil de conversation rattaché. Avec title, il forme la clé de déduplication de la tâche. |
recurrence | corps | non | object | règle de répétition ; l’occurrence suivante est créée à la complétion. |
reminders | corps | non | array | rappels, sous la forme [{ "minutes": 60 }]. |
source | corps | non | string | provenance ; 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êmethreadIdet 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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).400—TITLE_REQUIREDouRECURRENCE_NEEDS_DUE.403—SOURCE_FORBIDDEN—sourceré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
threadIdest 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 ».
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" }404—TASK_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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
title | corps | non | string | mê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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—TASK_NOT_FOUND.
La provenance (
source) est immuable : une tâche posée par le serveur ne peut pas être « blanchie » enmanual.
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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).404—TASK_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 ».
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ètre | Emplacement | Requis | Type | Détail |
|---|---|---|---|---|
from | requête | non | string | début de période (date ISO). |
to | requête | non | string | fin de période (date ISO). |
Réponses :
200—{ "summary": { "volumes": {...}, "metricAvailability": "volumes_only" } }
curl "https://<base-api>/public/v1/analytics/summary?from=2026-07-01&to=2026-07-31" \
-H "X-Api-Key: <votre-clé>"
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 typeagentà approbation requise : rien n’a été écrit, une demande de validation humaine a été déposée. L’en-têteIdempotency-Keyest obligatoire dans ce cas (sinon400).
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
GETconsidérée comme une écriture : elle produit un effet observable (livraison sortante). C’est aussi la seule routeGETqui peut répondre202à une clé de typeagentà approbation requise.
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:conversationspour une recherche,drafts:writepour 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 jamais202.
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.