---
title: "Importar contactos al directorio en bloque (remitentes y destinatarios)"
method: POST
path: "/contacts/import"
tags: ["addresses"]
---

# Importar contactos al directorio en bloque (remitentes y destinatarios)

`POST /contacts/import`

## Request

- Base URL: `https://api.enviadores.com.mx/api/v1`
- URL: `https://api.enviadores.com.mx/api/v1/contacts/import`
- Auth: HTTP bearer

Importación masiva del directorio — pensada para migrar contactos desde una hoja de cálculo, otra plataforma o una lista fotografiada, típicamente a través de un asistente de IA. Requiere scope `addresses:write` (es el mismo alcance de `POST /senders` / `POST /recipients`: una importación son esos dos creates en bloque).

**Dos carriles de entrada, mutuamente excluyentes:**
- `contacts`: arreglo de filas `{type: 'sender'|'recipient', name, phone, street, number?, colonia, city?, state?, postal_code, email?, company?, rfc?}` — el mismo vocabulario de campos que `POST /senders` / `POST /recipients` (los destinatarios además aceptan `sender_id?`, `alias?`, `referencia?`, `delivery_instructions?`). El cliente (p. ej. el modelo) debe mapear los datos del usuario TAL CUAL, nunca inventar valores.
- `raw_csv`: el contenido del CSV del usuario, VERBATIM (UTF-8, BOM opcional, máx. 256KB). El servidor lo interpreta de forma determinista: delimitador coma/punto y coma/tabulador (detectado), fila de encabezado obligatoria con las columnas de la plantilla `tipo,nombre,telefono,calle,numero,colonia,ciudad,estado,codigo_postal,email,empresa,rfc` (coincidencia insensible a mayúsculas y acentos, cualquier orden; columnas desconocidas → 422, nunca adivinadas). `tipo` acepta `remitente`/`destinatario` (o `sender`/`recipient`).

**Dos fases sin estado:** `dry_run: true` (el DEFAULT — el modo seguro) valida todo y devuelve la vista previa normalizada sin escribir nada; `dry_run: false` re-valida e inserta. La seguridad ante reintentos viene del DEDUPE POR CONTENIDO, no de claims: una fila idéntica (tupla normalizada nombre+teléfono+dirección+CP — el MISMO dedupe de `POST /senders`) a un contacto existente visible se reporta `duplicate` y se omite, así que repetir un commit jamás duplica el directorio.

**Validación por fila (nunca aborta el lote completo):** `codigo_postal` de 5 dígitos y existente en el catálogo SEPOMEX; si `ciudad`/`estado` vienen, deben ser coherentes con el CP (un desajuste nombra el valor esperado) y si faltan se AUTOCOMPLETAN del catálogo; `telefono` se normaliza a 10 dígitos (se toleran espacios, guiones y el prefijo +52/521); `email` con formato válido cuando venga. Cada fila responde `status: ok|duplicate|invalid` con `issues[]` en español nombrando campo y motivo.

**Padre de los destinatarios:** un destino siempre pertenece a un remitente. Orden de resolución: `sender_id` explícito de la fila → el PRIMER remitente válido del mismo lote → el remitente más reciente del directorio. Sin candidato, la fila es `invalid` con instrucciones.

**Topes:** máximo 200 filas por llamada (más → `422` pidiendo trocear el lote) y 256KB de `raw_csv`.

**Conteos (invariantes):** `received = valid + invalid`, `valid = filas ok + duplicates`; en commit además `created` y `skipped` con `received = created + skipped + invalid` — imposible perder filas en silencio.

Con una llave `ek_test_` la importación escribe en el directorio del sandbox (mismas reglas, mismos formatos de respuesta). La visibilidad/destino de las filas es la misma de `POST /senders`: llaves de cliente y llaves admin sin `X-PDV-ID` escriben como el usuario de la llave; una llave admin con `X-PDV-ID` validado escribe en el directorio de ese punto de venta.

## Headers

- `X-PDV-ID` string

## Request body

- object
  - `contacts` object[]: Filas mapeadas tal cual de los datos del usuario. Mutuamente excluyente con raw_csv.
    - `type` 'sender' | 'recipient', required
    - `name` string, required
    - `phone` string, required: 10 dígitos MX; se normalizan espacios/guiones/+52.
    - `email` string
    - `company` string: Solo remitentes (razón social).
    - `rfc` string: Solo remitentes.
    - `apellido_paterno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `apellido_materno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `street` string, required
    - `number` string
    - `colonia` string, required
    - `city` string: Opcional — se autocompleta desde el CP; si viene, se valida contra él.
    - `state` string: Opcional — se autocompleta desde el CP; si viene, se valida contra él.
    - `postal_code` string, required
    - `country` string: Opcional; solo MX.
    - `sender_id` string: Solo destinatarios: remitente dueño; default = primer remitente del lote o el más reciente del directorio.
    - `alias` string: Solo destinatarios.
    - `referencia` string: Solo destinatarios.
    - `delivery_instructions` string: Solo destinatarios.
  - `raw_csv` string: CSV del usuario, verbatim (UTF-8, máx. 256KB). Mutuamente excluyente con contacts.
  - `dry_run` boolean: true (default): valida y previsualiza sin escribir. false: re-valida e inserta.

## Response `200`

Resultado por fila + resumen. En dry_run nada se escribió; en commit, `created`/`skipped` reportan lo insertado/omitido.

- object
  - `success` unknown, required
  - `data` object, required
    - `dry_run` boolean, required
    - `rows` object[], required
      - `index` integer, required: Posición 0-based de la fila en la solicitud.
      - `line` integer, nullable: Línea del archivo en el carril raw_csv (encabezado = línea 1); null en el carril contacts.
      - `type` 'sender' | 'recipient' | 'null', nullable, required
      - `name` string, nullable, required
      - `status` 'ok' | 'duplicate' | 'invalid', required
      - `issues` string[], required: Motivos en español, nombrando el campo.
      - `contact` object, nullable: La fila normalizada (teléfono a 10 dígitos, ciudad/estado autocompletados) cuando status=ok.
      - `id` string, nullable: Id creado (commit) o del contacto existente (duplicate).
    - `summary` object, required
      - `received` integer, required
      - `valid` integer, required: Filas ok + duplicates (received = valid + invalid).
      - `invalid` integer, required
      - `duplicates` integer, required
      - `created` integer: Solo en commit. received = created + skipped + invalid.
      - `skipped` integer: Solo en commit; = duplicates.
  - `requestId` string, required: Correlación de logs; también en el header `X-Request-Id`.

## Other responses

- `401`: `UNAUTHORIZED` — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos).
- `403`: `INSUFFICIENT_SCOPE` — a la llave le falta el scope requerido · `PDV_NOT_ALLOWED` — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde `429 RATE_LIMITED` (el 429 tiene precedencia sobre este 403).
- `422`: `VALIDATION_ERROR` — error de contrato (ambos carriles a la vez, ninguno, campo desconocido, más de 200 filas → trocear el lote, CSV >256KB) o de forma del CSV (encabezado con columnas desconocidas/faltantes, no-UTF-8), con los motivos en `details.fields`. Los problemas POR FILA nunca son 422: viajan como `status: invalid` dentro de `rows`.
- `429`: `RATE_LIMITED` — bucket de la llave agotado. Espera `Retry-After` s (`details.retry_after_ms` para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 `INSUFFICIENT_SCOPE`.
- `500`: `SERVER_ERROR` — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff.

---

[API](https://skmtc.dev/enviadores/apis/enviadores-public-api.md) · [All operations](https://skmtc.dev/enviadores/apis/enviadores-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/enviadores/apis/enviadores-public-api/revisions/3633f08c889c?raw)
