Importer des contacts en masse

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 :
ChampObligatoireDescription
emailouiLa clé de l'ajout-ou-mise-à-jour, avec la liste
firstNamenonJusqu'à 100 caractères
lastNamenonJusqu'à 100 caractères
tagsnonNoms, créés dans l'organisation s'ils n'existent pas encore
statusnonpending, subscribed, unsubscribed, bounced, complained — à la création uniquement
optinAtnonDate ISO de l'inscription
optinIpnonIP de l'inscription
confirmedAtnonDate ISO de la confirmation double opt-in
confirmedIpnonIP de la confirmation
doubleOptIn—Refusé : un import n'envoie jamais d'e-mail à personne, la ligne est rejetée
Un lot est invisible pour les contacts. Un import n'est pas une inscription : rien n'est envoyé, rien n'est mis en file d'attente, aucune automatisation ne démarre. Pas d'e-mail de confirmation, pas d'inscription à une séquence — ni pour une ligne déclaré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 un 200 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"}]
  }
}
ChampSignification
totalLignes soumises — toujours created + updated + rejected
createdContacts qui n'existaient pas dans la liste
updatedContacts déjà présents, complétés sans que leur statut change jamais
rejectedLignes refusées, détaillées une à une dans errors
errors{index, email, reason} — index est la position dans le lot que vous avez envoyé
Une mauvaise ligne ne rejette que cette ligne. Les 499 autres sont écrites. Il n'y a pas de transaction globale : une seule virgule ne peut pas vous coûter tout le lot.

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.
  1. Envoyez un bloc de 500.
  2. Lisez errors.
  3. Reconstituez un lot à partir des seuls index en échec — errors.map(e => batch[e.index]) — corrigez-les, renvoyez.
  4. Recommencez jusqu'à ce que rejected vaille 0, 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)
}
Renvoyer une ligne qui a réussi est sans danger — l'endpoint fait un ajout-ou-mise-à-jour — mais l'intérêt est que vous n'avez pas à le faire : 47 rejected vous dit lesquelles 47.
StatutQuand
200Lot traité — lisez le rapport, certaines lignes ont pu être rejetées
400Le corps lui-même est mal formé (contacts absent ou qui n'est pas un tableau)
403Le propriétaire de la clé est member : importer exige admin
404Liste inconnue, ou liste d'une autre organisation
413Plus de 500 entrées — rien n'a été écrit, rien n'a été tronqué
Au-delà de la limite, l'appel est refusé en entier, avec la limite dans le message : un agent qui croit avoir envoyé 800 contacts ne doit jamais découvrir plus tard que seuls 500 sont entrés. Les modèles envoyés à cette audience utilisent des balises de fusion — voir Aide-mémoire des balises de fusion. Écrire ces modèles, vérifier leur rendu et envoyer une campagne à cette audience, c'est l'autre moitié de l'API — voir API Contenu et envoi.

Autorisation

Rôle dans l'organisationCet endpoint
OWNERAutorisé
ADMINAutorisé
MEMBERRefusé — 403
Une clé porte le rôle que son créateur détient dans cette organisation — jamais un rôle sur AgentsMail lui-même. Voir Clés d'API.

Corps de la requête

ChampTypeObligatoireDescription
contactsobject[]ouiTableau 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é est member de l'organisation, pas admin.
  • 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 la limit.
  • 429 Too Many Requests — plus de 120 requêtes en une minute pour cette clé.

Champs de la réponse

ChampTypeDescription
successbooleanIndique si l'opération a réussi
data.totalnumberLignes soumises — toujours created + updated + rejected
data.creatednumberContacts qui n'existaient pas dans la liste
data.updatednumberContacts déjà présents, complétés sans que leur statut change jamais
data.rejectednumberLignes refusées, détaillées une à une dans errors
data.errorsarray{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.

Voir aussi