Historial
Al vincular un número, el teléfono le manda al dispositivo nuevo una parte de su historial. El motor
siempre procesa esos lotes por lo que el protocolo necesita (identidades, secretos, nombres),
pero no importa los mensajes salvo que la política de la instancia lo diga: la importación es
opcional y viene apagada. Encendida, los mensajes viejos llegan como message.received con
origin: history, con la misma identidad que uno en vivo (nunca se duplican), y cada importación
es un trabajo con su progreso y su resultado. Después puedes pedirle al teléfono mensajes más
viejos de un chat, o de varios.
Requisitos
Sección titulada «Requisitos»| Operación | Scope |
|---|---|
GET /v1/instances/{id}/history (política y trabajos) y GET …/history/jobs/{job_id} |
history:read |
POST /v1/instances/{id}/history/requests (pedir más de un chat) |
history:manage |
POST /v1/instances/{id}/history/imports (importar varios chats) |
history:manage |
POST /v1/instances/{id}/history/jobs/{job_id}/cancel |
history:manage |
PATCH /v1/instances/{id} con la política (config.historial_*) |
instances:manage |
GET /v1/instances/{id}/messages (lo importado, en el store) |
messages:read |
GET /v1/instances/{id}/events (history.sync, sync.* y los mensajes) |
events:read |
La política
Sección titulada «La política»Cada instancia tiene su política; lo que no fija lo toma de Ajustes → Retención e historial:
Campo de config |
De fábrica | Qué hace |
|---|---|---|
historial_importar |
false |
Importar los mensajes del historial. Apagado, solo se procesan los metadatos. |
historial_dias |
90 (1 a 3650) | El corte: nada más viejo se importa. |
historial_mensajes |
5000 (1 a 100 000) | La cuota de mensajes de cada trabajo. |
historial_medios |
false |
Descargar la multimedia de lo importado. Apagado, cada medio queda con sus metadatos y estado: no_descargado. |
GET /v1/instances/{id}/history devuelve la política vigente (politica, con propia: true si la
instancia cambió algo), los trabajos más nuevos primero (con next_cursor y has_more) y cuántos
lotes llegaron.
curl -X PATCH https://motor.ejemplo.com/v1/instances/INSTANCIA_ID \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"config": {"historial_importar": true, "historial_dias": 90, "historial_mensajes": 5000}}'
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/history \ -H "Authorization: Bearer TU_API_KEY"// El SDK de Go no trae el historial: es HTTP directo.cuerpo := strings.NewReader(`{"config": {"historial_importar": true, "historial_dias": 90, "historial_mensajes": 5000}}`)req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, c.Base+"/v1/instances/"+instanciaID, cuerpo)req.Header.Set("Authorization", "Bearer "+c.Token)req.Header.Set("Content-Type", "application/json")res, err := c.HTTP.Do(req) // 200 con la instanciaawait fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ config: { historial_importar: true, historial_dias: 90, historial_mensajes: 5000 } }),});
const historial = await ( await fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/history`, { headers: { Authorization: `Bearer ${apiKey}` }, })).json(); // { politica, trabajos, has_more, lotes, aviso }{ "politica": { "importar": true, "dias": 90, "max_mensajes": 5000, "medios": false, "propia": true }, "trabajos": [ { "id": "5c2e7a10-0000-4000-8000-0000000000d1", "instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1", "tipo": "inicial", "estado": "completado", "motivo": "cuota", "progreso": 100, "mensajes_vistos": 10138, "importados": 5000, "duplicados": 323, "omitidos_fecha": 123, "omitidos_cuota": 3426, "no_soportados": 1266, "aviso": "WhatsApp no garantiza la historia completa: es lo que el teléfono tenía y envió." } ], "has_more": false, "lotes": { "recibidos": 6, "sin_contenido": 3, "pendientes": 0, "importados": 3, "omitidos": 0, "ultimo_en": "2026-10-16T22:09:40Z" }, "aviso": "WhatsApp no garantiza la historia completa: es lo que el teléfono tenía y envió."}Los trabajos
Sección titulada «Los trabajos»Un trabajo de historial junta los lotes que llegan para un fin, con sus contadores. Hay tres:
tipo |
Lo crea | Qué hace |
|---|---|---|
inicial |
El primer lote del historial del vínculo, con la importación encendida | Importa lo que el teléfono manda al vincular. Uno por vínculo. |
pedido |
POST …/history/requests (comando history.request) |
Pide al teléfono los mensajes anteriores de un chat. |
importacion |
POST …/history/imports (comando history.import) |
Pide hacia atrás, chat por chat, varios chats. |
El id de un trabajo es el mismo que el command_id del comando que lo creó:
GET …/history/jobs/{command_id}.
estado |
Qué significa |
|---|---|
en_curso |
Importando o por pedir el siguiente tramo. |
esperando |
Un pedido salió al teléfono y espera su respuesta (pedido_vence_en). |
en_pausa |
Contrapresión: hay demasiadas entregas de webhooks pendientes en la instancia. Sigue sola. |
completado |
Terminó por un motivo normal (ver abajo). |
cancelado |
Lo cancelaste. |
fallido |
No pudo terminar (por ejemplo, el teléfono no contestó). |
Los contadores: mensajes_vistos (todo lo que trajeron los lotes), importados (nuevos para el
motor), duplicados (ya estaban, en vivo o de otro lote), omitidos_fecha (anteriores al corte),
omitidos_cuota (fuera de la cuota), no_soportados (avisos del sistema o del protocolo: se
cuentan, no se inventan), pedidos, sin_respuesta, progreso (el 0-100 que informó WhatsApp en
el último lote) y mas_viejo_en/mas_nuevo_en.
motivo dice por qué está en pausa o por qué terminó:
motivo |
Qué significa |
|---|---|
contrapresion |
En pausa por las entregas pendientes. |
cuota |
Se llegó a la cuota del trabajo. |
corte |
Se llegó a la fecha de corte. |
sin_mas_lotes |
El teléfono dejó de mandar lotes (el inicial). |
chats_recorridos |
Una importación terminó con todos sus chats. |
pedido_atendido |
El teléfono contestó el pedido. |
sin_mas_historial |
WhatsApp marcó que no queda nada más viejo de ese chat. |
fuera_del_historial |
El chat no vino en el historial inicial de este vínculo con mensajes que se puedan pedir (por ejemplo, empezó después de vincular). |
solo_en_el_telefono |
El teléfono dijo que le quedan mensajes más viejos de ese chat, pero que no se pueden pedir. |
sin_respuesta |
El teléfono no contestó a tiempo (sin conexión o, como en WhatsApp Web, sin nada más viejo). |
pedido_rechazado |
El pedido no se pudo mandar. |
no_autorizado |
La key que lo pidió ya no lo autoriza (revocada, sin el scope o sin la instancia). |
cancelado |
Lo cancelaste. |
Pedir más historial de un chat
Sección titulada «Pedir más historial de un chat»POST /v1/instances/{id}/history/requests (Idempotency-Key obligatoria) pide al teléfono los
cantidad mensajes (de 1 a 50; por omisión, 50) anteriores al más viejo que el motor conoce de
ese chat. Responde 202 con el comando history.request, que crea el trabajo pedido.
- Si el motor no conoce ningún mensaje del chat, 409
sin_mensajes: no hay desde dónde pedir. - Con la importación apagada, 409
importacion_apagada. - Si el teléfono no contesta en
historial.espera_pedido_minutos(10 de fábrica), el trabajo terminafallidoconsin_respuesta.
Para ir más atrás, repite el pedido cuando termine: cada uno parte del mensaje más viejo que ya se importó.
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/history/requests \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: historial-chat-1042-tanda-1" \ -d '{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "cantidad": 50}'
# El trabajo: su id es el command_id del 202curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/history/jobs/COMMAND_ID \ -H "Authorization: Bearer TU_API_KEY"// El SDK de Go no trae el historial: es HTTP directo.cuerpo := strings.NewReader(`{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "cantidad": 50}`)req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.Base+"/v1/instances/"+instanciaID+"/history/requests", cuerpo)req.Header.Set("Authorization", "Bearer "+c.Token)req.Header.Set("Content-Type", "application/json")req.Header.Set("Idempotency-Key", "historial-chat-1042-tanda-1")res, err := c.HTTP.Do(req) // 202if err != nil { return err}defer res.Body.Close()var cmd motor.Comandoif err := json.NewDecoder(res.Body).Decode(&cmd); err != nil { return err}// Después: GET /v1/instances/{id}/history/jobs/{cmd.ID}, o espera sync.completed / sync.failed.const base = `https://motor.ejemplo.com/v1/instances/${instanciaId}`;const res = await fetch(`${base}/history/requests`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'historial-chat-1042-tanda-1', }, body: JSON.stringify({ destino: { peer_ref: '4b7d9e01-0000-4000-8000-0000000000b1' }, cantidad: 50 }),});const { command_id } = await res.json(); // 202
const trabajo = await ( await fetch(`${base}/history/jobs/${command_id}`, { headers: { Authorization: `Bearer ${apiKey}` } })).json(); // { estado: "esperando" | "completado" | …, motivo, importados, … }{ "command_id": "5c2e7a10-0000-4000-8000-0000000000d2", "instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1", "idempotency_key": "historial-chat-1042-tanda-1", "type": "history.request", "generation": 1, "status": "aceptado", "created_at": "2026-10-24T16:00:00Z", "updated_at": "2026-10-24T16:00:00Z"}Las fronteras
Sección titulada «Las fronteras»Como WhatsApp Web, el motor solo pide los mensajes anteriores de un chat que vino en el historial inicial de este vínculo con mensajes que se pueden pedir. Con esa frontera completa (15 minutos sin lotes nuevos del historial inicial), un pedido que no tiene sentido termina al instante, sin preguntarle al teléfono:
fuera_del_historial: el chat no vino en el historial inicial (por ejemplo, empezó después de vincular). El teléfono no contesta esos pedidos: solo calla.sin_mas_historial: el teléfono marcó en el historial que no le quedan mensajes más viejos de ese chat.solo_en_el_telefono: el teléfono marcó que le quedan mensajes más viejos, pero que no se pueden pedir («usa WhatsApp en tu teléfono»).
El ajuste historial.pedido_fuera_del_historial (auto de fábrica; también pedir y no_pedir)
cambia esa regla. Los demás historial.pedido_* de Ajustes son variantes del pedido para
diagnóstico.
Importar varios chats
Sección titulada «Importar varios chats»POST /v1/instances/{id}/history/imports (Idempotency-Key obligatoria) crea un trabajo
importacion: pedidos sucesivos hacia atrás, chat por chat, hasta el corte, la cuota o lo que tenga
el teléfono. chats es una lista de hasta 50 destinos (vacía: los 20 chats con actividad más
reciente que conoce el motor); dias y max_mensajes pueden achicar la política para este trabajo
(max_mensajes nunca pasa la cuota de la política).
Un chat se deja cuando no tiene mensajes, cuando llegó al corte o cuando un pedido no trajo nada más
viejo; no se vuelve a pedir lo mismo. Dos pedidos seguidos sin respuesta hacen fallar la importación
(sin_respuesta). Con demasiadas entregas pendientes (historial.contrapresion_entregas, 2000 de
fábrica) el trabajo pasa a en_pausa y sigue solo.
{ "chats": [ {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, {"jid": "GRUPO@g.us"} ], "dias": 30, "max_mensajes": 2000}Para cancelar un trabajo, POST /v1/instances/{id}/history/jobs/{job_id}/cancel: deja de importar,
omite lo que faltaba y borra su contenido guardado (sync.failed con motivo: cancelado). Lo ya
importado queda. Si el trabajo ya terminó, 409 trabajo_terminado.
Los eventos
Sección titulada «Los eventos»| Evento | Cuándo |
|---|---|
history.sync |
Llegó un lote de historial (siempre, con la importación encendida o apagada). Solo metadatos. |
sync.progress |
Un trabajo importó un lote o entró en pausa. |
sync.completed |
Un trabajo terminó completado. |
sync.failed |
Un trabajo terminó fallido o cancelado. |
message.received (origin: history) |
Cada mensaje importado, con la misma forma que en vivo. |
{ "tipo_sync": "INITIAL_BOOTSTRAP", "fragmento": 1, "progreso": 51, "conversaciones": 400, "mensajes": 692, "contenido_importado": true, "sync_job_id": "5c2e7a10-0000-4000-8000-0000000000d1"}tipo_sync es el del lote de WhatsApp (INITIAL_BOOTSTRAP, RECENT, FULL, ON_DEMAND,
PUSH_NAME…). Con la importación apagada, contenido_importado es false y no hay sync_job_id.
{ "sync_job_id": "5c2e7a10-0000-4000-8000-0000000000d2", "tipo": "pedido", "estado": "completado", "motivo": "pedido_atendido", "lotes_recibidos": 1, "lotes_importados": 1, "importados": 50, "duplicados": 0, "omitidos_fecha": 0, "omitidos_cuota": 0, "no_soportados": 0, "progreso": 0, "mas_viejo_en": "2026-09-23T01:50:00Z", "mas_nuevo_en": "2026-09-25T18:12:00Z", "pedidos": 1, "sin_respuesta": 0, "chats": 1, "chat_actual": 0, "historia_completa": false, "aviso": "WhatsApp no garantiza la historia completa: es lo que el teléfono tenía y envió."}sync.progress y sync.failed tienen la misma forma; en el evento, chats es la cantidad de
chats del trabajo. El sobre de los tres trae el command_id del trabajo (en un pedido o una
importación).
Lo importado tiene la misma identidad que lo que llega en vivo (instancia, chat estable,
provider_id, dirección y operación): un mensaje que ya llegó en vivo, que viene en otro lote o que
el teléfono nombra por su PN y después por su LID, se cuenta como duplicado, no se repite. Las
respuestas a interactivos del historial se importan como en vivo (interactive.response con
origin: history), y los estados (stories) del historial no se importan. Consulta lo importado con
GET /v1/instances/{id}/messages?origen=history.
Errores comunes
Sección titulada «Errores comunes»| HTTP / estado | Código | Cuándo |
|---|---|---|
| 409 | importacion_apagada |
Pedir o importar con historial_importar apagado. |
| 409 | sin_mensajes |
El motor no conoce ningún mensaje de ese chat. |
| 409 | trabajo_terminado |
Cancelar un trabajo que ya terminó. |
| 409 | sin_vincular |
La instancia no está vinculada. |
| 409 | — | La Idempotency-Key ya se usó con otros datos. |
| 404 | — | El trabajo no existe en esta instancia, o la instancia no conoce ese chat. |
| 422 | — | cantidad fuera de 1 a 50 (100 es 422), más de 50 chats, o dias/max_mensajes fuera de rango. |
fallido |
sin_respuesta, pedido_rechazado, no_autorizado |
Ver los motivos de arriba. |
Límites de WhatsApp y mediciones reales
Sección titulada «Límites de WhatsApp y mediciones reales»Lo medido con números reales, del 16 al 23 de octubre de 2026:
- Importación al vincular (caso A53, 16/10/2026): con la importación encendida antes de vincular
(90 días, 5000 mensajes), el trabajo inicial empezó con el primer lote, mostró el progreso de
WhatsApp (51 % y 100 %) y terminó por la cuota en unos 2 minutos y medio: 5000 importados de
10 138 vistos, 323 duplicados, 123 fuera del corte, 3426 fuera de la cuota y 1266 no soportados;
lo más viejo, de 88 días antes. Una pausa por contrapresión se reanudó sola en ~1 minuto. Con la
importación apagada (caso A12): solo
history.synccon metadatos y ningún mensaje. - Importar varios chats (caso A55, 16/10/2026): 10 pedidos sucesivos hacia atrás, contestados en 1 a 32 s cada uno; un chat llegó al corte de 90 días y el trabajo pasó al siguiente sin repetir el pedido, hasta la cuota de 400. Tardó 3 min 46 s.
- El historial inicial trae poco por chat: en un vínculo, el
INITIAL_BOOTSTRAPtrajo 555 mensajes en 400 chats (casi uno por chat), con la marca de que quedan más en el teléfono; la historia larga se pide después, chat por chat (21/10/2026). - Un chat hasta el límite (21/10/2026, tandas de 50): 195 tandas
pedido_atendido, con 9604 mensajes vistos y 9577 importados, hasta el 28/07/2026 (unos 3 meses); mediana de 3 s por tanda, ~42 minutos en total, con algunas demoras de 79 a 476 s. Ninguna tanda repitió ni saltó mensajes. La tanda 196 terminósin_respuesta: con WhatsApp abierto en primer plano, las 10 siguientes contestaron en 3 a 6 s, así que las demoras venían de la pantalla apagada del teléfono, no de un límite. El teléfono seguía marcando que quedaba más: no se llegó asin_mas_historial. La base creció 26 MB; sinhistorial_medios, el almacenamiento no cambió. - Pico de recursos al vincular con historial (caso A88): el 22/10/2026, 1846 mensajes vistos y
722 importados en 104 chats, con un pico de 133 MB de RSS y 0 esperas del pool de la base; el
23/10/2026, 31 744 vistos y 5000 importados en 60 chats (el tope; 14 687 fuera de la cuota y
3800 no soportados), con un pico de 167 MB y 0 esperas, y el trabajo inicial cerró solo
completadoa los ~12 minutos sin lotes. Los dos, dentro de los umbrales de Capacidad. - El pedido de un chat (caso A54, fallas F11 y ronda R12): WhatsApp no contesta el pedido de un
chat que nació después de vincular (solo calla). Con la frontera completa, el motor termina al
instante con
fuera_del_historialosin_mas_historial, sin preguntarle al teléfono (23/10/2026). Ver Historial bajo pedido.
Los no_soportados se cuentan solo en total por lote (no por tipo): en las dos importaciones del
caso A88 fueron 275 y 3800.