Erreurs
L'API HTTP renvoie des codes HTTP standards. Toute erreur porte un corps JSON de la même forme, produite par le framework NestJS. Le relais SMTP a sa propre table de codes à trois chiffres, documentée séparément.
Forme du corps d’erreur
statusCode reprend le code HTTP et error son libellé standard. message est toujours une chaîne : la phrase à afficher telle quelle. errors est toujours un tableau de chaînes non vide : le détail, un élément par champ invalide quand plusieurs champs sont en cause. Les deux sont liés par un invariant : message === errors.join(' ') - message n’est que la concaténation de errors, séparée par un espace.
{
"message": "L'adresse du destinataire est invalide : indiquez une seule adresse comme « client@exemple.gn ».",
"error": "Bad Request",
"statusCode": 400,
"errors": [
"L'adresse du destinataire est invalide : indiquez une seule adresse comme « client@exemple.gn »."
]
}{
"message": "L'adresse d'expédition est invalide : utilisez « adresse@domaine » ou « Nom <adresse@domaine> ». L'adresse du destinataire est invalide : indiquez une seule adresse comme « client@exemple.gn ». Le sujet est obligatoire et ne doit pas dépasser 300 caractères.",
"error": "Bad Request",
"statusCode": 400,
"errors": [
"L'adresse d'expédition est invalide : utilisez « adresse@domaine » ou « Nom <adresse@domaine> ».",
"L'adresse du destinataire est invalide : indiquez une seule adresse comme « client@exemple.gn ».",
"Le sujet est obligatoire et ne doit pas dépasser 300 caractères."
]
}Migration : message n'est plus jamais un tableau
message changeait de forme selon le chemin d’échec : une chaîne pour une erreur levée à la main, un tableau pour les erreurs de validation agrégées par NestJS. Un code qui lisait message[0] pour afficher la phrase recevait donc, sans erreur visible, soit la phrase entière soit sa première lettre - selon le chemin emprunté. Remplacez message[0] par errors[0] : message est désormais toujours la chaîne complète à afficher, et errors porte le détail champ par champ.Codes HTTP
- -Paramètre manquant, mal typé ou hors bornes (POST /v1/emails : from, to, subject, html/text).
- -Nom de domaine syntaxiquement invalide sur POST /v1/domains.
- -Nom de clé API vide ou trop long sur POST /v1/api-keys.
- -Domaine sélectionné inexistant, non vérifié ou en double sur POST /v1/api-keys ou POST /v1/smtp-credentials (portée par domaine). Plus de 50 domaines : « Une portée ne peut pas contenir plus de 50 domaines ».
- -Pack de recharge inconnu, méthode de paiement invalide, ou numéro de téléphone mal formé sur POST /v1/billing/topup-requests.
- -Paramètre de pagination (page, limit) non numérique ou hors bornes.
- -Champ non déclaré dans le corps JSON, par exemple une faute de frappe dans un nom de paramètre : désormais refusé par un 400 (« Le champ « X » n'est pas accepté. »), là où il était auparavant ignoré silencieusement.
- -En-tête Authorization absent, mal formé, ou clé API inconnue/révoquée (routes API key).
- -Session absente ou expirée (routes tableau de bord, authentifiées par cookie).
- -POST /v1/emails alors que le solde de crédits du compte est inférieur à 1.
- -Domaine d'envoi non vérifié sur POST /v1/emails.
- -Clé API limitée à certains domaines, utilisée avec une adresse from d'un autre domaine sur POST /v1/emails (code CREDENTIAL_DOMAIN_OUT_OF_SCOPE).
- -Envoi depuis l'adresse de test (mode bac à sable) vers un destinataire autre que l'adresse email de votre compte sur POST /v1/emails.
- -Clé API créée sans la permission de lecture, utilisée sur GET /v1/emails, GET /v1/emails/:publicId, GET /v1/domains ou GET /v1/domains/:id : « Cette clé API n'a pas l'autorisation de lecture : créez une clé avec la lecture autorisée. »
- -Compte suspendu (réputation d'envoi dégradée) - sur toute route authentifiée, API key ou session.
- -Domaine, clé API ou email référencé par :id qui n'existe pas - ou qui appartient à un autre compte : Zendou renvoie volontairement la même 404 dans les deux cas, pour ne jamais révéler l'existence de la ressource d'un tiers.
- -Nom de domaine déjà enregistré (par vous ou par un autre compte) sur POST /v1/domains.
- -Une demande de recharge est déjà en attente avec la même référence de transaction sur POST /v1/billing/topup-requests.
- -Quota journalier : POST /v1/emails alors que le nombre d'emails envoyés depuis minuit UTC a atteint la limite journalière du compte (200 par défaut, relevée automatiquement avec l'ancienneté et le volume - voir Facturation). Message : « Limite journalière atteinte : réessayez demain ou demandez une augmentation de quota. »
- -Rafale : plus de 60 requêtes par minute sur POST /v1/emails, indépendamment du quota journalier et du solde de crédits. Message : « Trop de requêtes. Réessayez dans quelques instants. »
- -Trop d'échecs d'authentification depuis une même adresse IP sur POST /v1/emails (clé API inconnue ou révoquée présentée à répétition). Utile à savoir en intégration : tester avec une mauvaise clé peut donc, après plusieurs essais, renvoyer un 429 plutôt qu'un 401. Une clé valide n'est jamais concernée, même si l'IP est bloquée.
En-têtes de limitation de débit
X-RateLimit-Limit (budget de la fenêtre), X-RateLimit-Remaining (requêtes encore acceptées dans cette fenêtre) et X-RateLimit-Reset (secondes avant remise à zéro). Un 429 porte en plus Retry-After (secondes avant de pouvoir réessayer), désormais aussi bien sur le 429 de rafale que sur celui de quota journalier atteint. Quand plusieurs compteurs s’appliquent à une même requête, ce sont les valeurs du compteur le plus contraignant, celui qui bloquera le client en premier, qui sont publiées.Code d’erreur machine (POST /v1/emails)
Toute réponse d’erreur 4xx de POST /v1/emails porte, en plus de message et errors, un champ code : une chaîne stable et lisible par machine, sur laquelle brancher la logique d’un client plutôt que sur le texte français de message, susceptible d’être reformulé.
{
"message": "Le domaine d'envoi n'est pas vérifié : ajoutez-le à votre compte et validez ses enregistrements DNS avant d'envoyer.",
"error": "Forbidden",
"statusCode": 403,
"code": "DOMAIN_NOT_VERIFIED",
"errors": [
"Le domaine d'envoi n'est pas vérifié : ajoutez-le à votre compte et validez ses enregistrements DNS avant d'envoyer."
]
}| Code | Statut HTTP | Signification |
|---|---|---|
| ACCOUNT_SUSPENDED | 403 | Compte suspendu (réputation d'envoi dégradée). |
| EMAIL_NOT_VERIFIED | 403 | Adresse email du compte non confirmée. |
| INVALID_FROM | 400 | Adresse d'expédition (from) illisible, ou nom d'affichage de plus de 128 caractères. |
| INVALID_TO | 400 | Adresse destinataire (to) illisible, ou nom d'affichage de plus de 128 caractères. |
| INVALID_REPLY_TO | 400 | Adresse de réponse (replyTo) illisible, ou nom d'affichage de plus de 128 caractères. |
| MISSING_BODY | 400 | Ni html ni text fourni. |
| BODY_TOO_LARGE | 400 | Un corps (html ou text) dépasse 500 Ko. |
| DOMAIN_NOT_VERIFIED | 403 | Domaine de l'adresse d'expédition non vérifié ou non rattaché au compte. |
| CREDENTIAL_DOMAIN_OUT_OF_SCOPE | 403 | La clé API n'est pas autorisée pour le domaine de l'adresse d'expédition (portée limitée à certains domaines). L'adresse de test du bac à sable reste permise. |
| CREDENTIAL_DOMAIN_OUT_OF_SCOPE | 403 | Variante : « Cette clé API n'est autorisée pour aucun domaine : créez-en une nouvelle avec la portée voulue. » (clé dont tous les domaines ont disparu). |
| REPLY_TO_NOT_VERIFIED | 403 | Domaine de l'adresse de réponse (replyTo) non vérifié ou non rattaché au compte. Nouveau. |
| TEST_SENDER_UNAVAILABLE | 503 | Mode bac à sable momentanément indisponible (configuration serveur). |
| TEST_SENDER_RECIPIENT_RESTRICTED | 403 | Depuis l'adresse de test, destinataire autre que l'adresse du compte. |
| INSUFFICIENT_CREDITS | 402 | Solde de crédits insuffisant. |
| DAILY_LIMIT_REACHED | 429 | Quota journalier atteint. |
INVALID_REPLY_TO et REPLY_TO_NOT_VERIFIED sont nouveaux, liés à la prise en charge du champ replyTo, voir Envoyer un email.
Pour le détail des messages exacts renvoyés par POST /v1/emails, voir le tableau d’erreurs de la page « Envoyer un email ».
Ces codes ne couvrent que l’API HTTP. Le relais SMTP répond en codes à trois chiffres (RFC 5321), pas en HTTP - sa propre table de correspondance est documentée sur cette page.