POST /api/v1/lists/{listId}/contacts/bulk
Envoie jusqu'à 500 contacts en un seul appel, avec tout ce que vous savez d'eux — y compris la preuve de consentement d'une base migrée. Chaque ligne est ajoutée ou mise à jour selon l'e-mail, exactement comme avec Ajouter ou mettre à jour un contact.
Chaque entrée :
| Champ | Obligatoire | Description |
|---|---|---|
email | oui | La clé de l'ajout-ou-mise-à-jour, avec la liste |
firstName | non | Jusqu'à 100 caractères |
lastName | non | Jusqu'à 100 caractères |
tags | non | Noms, créés dans l'organisation s'ils n'existent pas encore |
status | non | pending, subscribed, unsubscribed, bounced, complained — à la création uniquement |
optinAt | non | Date ISO de l'inscription |
optinIp | non | IP de l'inscription |
confirmedAt | non | Date ISO de la confirmation double opt-in |
confirmedIp | non | IP de la confirmation |
doubleOptIn | — | Refusé : un import n'envoie jamais d'e-mail à personne, la ligne est rejetée |
subscribed, ni pour un tag ajouté par l'import. Des personnes inscrites il y a des années dans un autre outil ne doivent pas recevoir un « Bienvenue, merci de votre inscription ! » parce que vous avez changé de prestataire. Le status que vous déclarez est simplement celui avec lequel le contact est créé.
Pour la même raison, une entrée de lot refuse doubleOptIn : la ligne est rejetée et nommée dans le rapport, plutôt qu'acceptée alors qu'aucune confirmation ne serait jamais envoyée. Les inscriptions passent par POST /api/v1/lists/{listId}/contacts, un contact à la fois — c'est cet endpoint qui envoie les e-mails et inscrit aux séquences.
Les champs de consentement sont écrits, jamais effacés. Un champ que vous omettez conserve ce qui est déjà enregistré — c'est la seule partie d'une base migrée qu'on ne peut pas reconstituer après coup. Et un contact existant conserve son statut : déclarer
subscribed pour quelqu'un qui s'est désabonné ne change rien, le rapport compte quand même la ligne comme mise à jour.Le rapport
La réponse est un200 dont data est le rapport du lot :
json
{
"success": true,
"data": {
"total": 500,
"created": 468,
"updated": 31,
"rejected": 1,
"errors": [{"index": 342, "email": "pas-un-email", "reason": "email: Invalid email address"}]
}
}| Champ | Signification |
|---|---|
total | Lignes soumises — toujours created + updated + rejected |
created | Contacts qui n'existaient pas dans la liste |
updated | Contacts déjà présents, complétés sans que leur statut change jamais |
rejected | Lignes refusées, détaillées une à une dans errors |
errors | {index, email, reason} — index est la position dans le lot que vous avez envoyé |
La boucle de l'agent
index est ce qui rend le rapport exploitable : il désigne la ligne dans le tableau que vous avez envoyé, si bien qu'un échec se rejoue tel quel, et non par un renvoi complet.
- Envoyez un bloc de 500.
- Lisez
errors. - Reconstituez un lot à partir des seuls index en échec —
errors.map(e => batch[e.index])— corrigez-les, renvoyez. - Recommencez jusqu'à ce que
rejectedvaille0, puis passez au bloc suivant.
js
const send = async (batch) => {
const response = await fetch(`${base}/api/v1/lists/${listId}/contacts/bulk`, {
method: 'POST',
headers: {'x-api-key': key, 'content-type': 'application/json'},
body: JSON.stringify({contacts: batch}),
})
return (await response.json()).data
}
for (let offset = 0; offset < all.length; offset += 500) {
const batch = all.slice(offset, offset + 500)
const report = await send(batch)
const failed = report.errors.map((error) => ({...batch[error.index], ...fix(error)}))
if (failed.length > 0) await send(failed)
}47 rejected vous dit lesquelles 47.
| Statut | Quand |
|---|---|
200 | Lot traité — lisez le rapport, certaines lignes ont pu être rejetées |
400 | Le corps lui-même est mal formé (contacts absent ou qui n'est pas un tableau) |
403 | Le propriétaire de la clé est member : importer exige admin |
404 | Liste inconnue, ou liste d'une autre organisation |
413 | Plus de 500 entrées — rien n'a été écrit, rien n'a été tronqué |
Autorisation
| Rôle dans l'organisation | Cet endpoint |
|---|---|
OWNER | Autorisé |
ADMIN | Autorisé |
MEMBER | Refusé — 403 |
Corps de la requête
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
contacts | object[] | oui | Tableau d'entrées, 500 au maximum |
Exemple
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"contacts":[
{"email":"camille@example.fr","firstName":"Camille","lastName":"Roux","tags":["vip"],
"status":"subscribed","optinAt":"2024-03-12T09:14:00Z","optinIp":"81.250.14.7"},
{"email":"theo@example.com","status":"unsubscribed"}
]}' \
"https://www.agentsmail.io/api/v1/lists/$LIST_ID/contacts/bulk"Réponse
200 OK— l'appel a réussi.401 Unauthorized— clé absente ou invalide, ou son propriétaire a quitté l'organisation.403 Forbidden— le propriétaire de la clé estmemberde l'organisation, pasadmin.404 Not Found— ressource inexistante, ou elle appartient à une autre organisation. L'API ne confirme jamais qu'un identifiant existe à un appelant qui n'y a pas droit.413 Payload Too Large— plus de 500 contacts en un seul appel ; le corps indique lalimit.429 Too Many Requests— plus de 120 requêtes en une minute pour cette clé.
Champs de la réponse
| Champ | Type | Description |
|---|---|---|
success | boolean | Indique si l'opération a réussi |
data.total | number | Lignes soumises — toujours created + updated + rejected |
data.created | number | Contacts qui n'existaient pas dans la liste |
data.updated | number | Contacts déjà présents, complétés sans que leur statut change jamais |
data.rejected | number | Lignes refusées, détaillées une à une dans errors |
data.errors | array | {index, email, reason} — index est la position dans le lot que vous avez envoyé |
Exemple de réponse
json
{
"success": true,
"data": {
"total": 500,
"created": 468,
"updated": 31,
"rejected": 1,
"errors": [
{
"index": 342,
"email": "not-an-email",
"reason": "email: Invalid email address"
}
]
}
}Remarques
- Une liste supprimée répond
404, et tout le lot est refusé. Une liste supprimée dans l'interface n'accepte plus rien : envoyez le lot à une liste active.