Importar contactos en bloque

POST /api/v1/lists/{listId}/contacts/bulk Envía hasta 500 contactos en una sola llamada, con todo lo que sabes de ellos, incluida la prueba de consentimiento de una base de datos migrada. Cada línea se inserta o actualiza por email, exactamente igual que en Añadir o actualizar un contacto. Cada entrada:
CampoObligatorioDescripción
emailsíLa clave de la inserción o actualización, junto con la lista
firstNamenoHasta 100 caracteres
lastNamenoHasta 100 caracteres
tagsnoNombres, que se crean en la organización si todavía no existen
statusnopending, subscribed, unsubscribed, bounced, complained: solo en la creación
optinAtnoFecha ISO de la suscripción
optinIpnoIP de la suscripción
confirmedAtnoFecha ISO de la confirmación del doble opt-in
confirmedIpnoIP de la confirmación
doubleOptIn—Rechazado: una importación nunca envía emails a nadie, la línea se rechaza
Un lote es invisible para los contactos. Una importación no es una suscripción: no se envía nada, no se pone nada en cola, no arranca ninguna automatización. Ningún email de confirmación, ninguna inscripción en una secuencia: ni para una línea declarada subscribed, ni para una etiqueta añadida por la importación. Las personas que se suscribieron hace años en otra herramienta no deben recibir un «¡Bienvenido, gracias por suscribirte!» porque cambiaste de proveedor. El status que declaras es simplemente con el que se crea el contacto. Por la misma razón, una entrada de un lote rechaza doubleOptIn: la línea se rechaza y se nombra en el informe, en lugar de aceptarse sin que nunca se envíe una confirmación. Las suscripciones pasan por POST /api/v1/lists/{listId}/contacts, un contacto a la vez: ese endpoint es el que envía emails e inscribe.
Los campos de consentimiento se escriben, nunca se vacían. Un campo que omites conserva lo que ya está guardado: es la única parte de una base de datos migrada que no se puede reconstruir después. Y un contacto existente conserva su estado: declarar subscribed para alguien que se dio de baja no cambia nada, y el informe sigue contando la línea como actualizada.

El informe

La respuesta es un 200 cuyo data es el informe del lote:
json
{
  "success": true,
  "data": {
    "total": 500,
    "created": 468,
    "updated": 31,
    "rejected": 1,
    "errors": [{"index": 342, "email": "pas-un-email", "reason": "email: Invalid email address"}]
  }
}
CampoSignificado
totalLíneas enviadas: siempre created + updated + rejected
createdContactos que no existían en la lista
updatedContactos que ya estaban, completados sin cambiar nunca su estado
rejectedLíneas rechazadas, detalladas una a una en errors
errors{index, email, reason}: index es la posición en el lote que enviaste
Una línea errónea solo rechaza esa línea. Las otras 499 se escriben. No hay transacción global: una sola coma no puede costarte el lote entero.

El bucle del agente

index es lo que hace que el informe sea accionable: apunta a la línea del array que enviaste tú, así que un fallo se reenvía tal cual, no como un reenvío completo.
  1. Envía un bloque de 500.
  2. Lee errors.
  3. Reconstruye un lote solo con los índices que fallaron —errors.map(e => batch[e.index])—, corrígelos y vuelve a enviarlos.
  4. Repite hasta que rejected sea 0 y pasa al siguiente bloque.
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)
}
Reenviar una línea que funcionó es inofensivo —el endpoint hace upsert—, pero la idea es que no tengas que hacerlo: 47 rejected te dice cuáles son las 47.
EstadoCuándo
200Lote procesado: lee el informe, algunas líneas pueden haberse rechazado
400El propio cuerpo está mal formado (contacts ausente o no es un array)
403El propietario de la clave es member: importar requiere admin
404Lista desconocida, o una lista de otra organización
413Más de 500 entradas: no se escribió nada, no se truncó nada
Por encima del límite, la llamada se rechaza entera, con el límite en el mensaje: un agente que cree haber enviado 800 contactos nunca debe descubrir más tarde que solo entraron 500. Las plantillas enviadas a esa audiencia usan etiquetas de combinación; consulta Chuleta de etiquetas de combinación. Escribir esas plantillas, comprobar su renderizado y enviar una campaña a esta audiencia es la otra mitad de la API; consulta API de contenido y envío.

Autorización

Rol en la organizaciónEste endpoint
OWNERPermitido
ADMINPermitido
MEMBERRechazado — 403
Una clave lleva el rol que su creador tiene en esta organización, nunca un rol sobre AgentsMail en sí. Consulta Claves de API.

Cuerpo de la solicitud

CampoTipoObligatorioDescripción
contactsobject[]síArray de entradas, 500 como máximo

Ejemplo

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"

Respuesta

  • 200 OK: la llamada se realizó correctamente.
  • 401 Unauthorized: clave ausente o no válida, o su propietario salió de la organización.
  • 403 Forbidden: el propietario de la clave es member de la organización, no admin.
  • 404 Not Found: no existe ese recurso, o pertenece a otra organización. La API nunca confirma que un id existe a un llamante que no tiene derecho sobre él.
  • 413 Payload Too Large: más de 500 contactos en una llamada; el cuerpo indica el limit.
  • 429 Too Many Requests: más de 120 solicitudes en un minuto para esta clave.

Campos de la respuesta

CampoTipoDescripción
successbooleanIndica si la operación se realizó correctamente
data.totalnumberLíneas enviadas: siempre created + updated + rejected
data.creatednumberContactos que no existían en la lista
data.updatednumberContactos que ya estaban, completados sin que su estado cambie nunca
data.rejectednumberLíneas rechazadas, detalladas una a una en errors
data.errorsarray{index, email, reason}: index es la posición en el lote que enviaste

Ejemplo de respuesta

json
{
  "success": true,
  "data": {
    "total": 500,
    "created": 468,
    "updated": 31,
    "rejected": 1,
    "errors": [
      {
        "index": 342,
        "email": "not-an-email",
        "reason": "email: Invalid email address"
      }
    ]
  }
}

Notas

  • Una lista eliminada responde 404, y se rechaza el lote entero. Una lista eliminada en la interfaz ya no acepta nada: envía el lote a una lista activa.

Relacionado