Saltar al contenido

Apartado 6 de 12 · Sincronizar

Escribir en Chatendo: contactos y mensajes

Alta y edición de contactos, etiquetas, envío de mensajes por WhatsApp e Instagram, la ventana de 24 horas y los webhooks de entrada para sistemas sin token.

Revisado el API 1.11.9 5 min de lectura

En esta página (6)
  1. 6.1 · Contactos
  2. 6.2 · Mensajes
  3. 6.3 · La ventana de 24 horas
  4. 6.4 · Empujar sin token: los webhooks de entrada
  5. 6.5 · Lo que NO hay que hacer
  6. 6.6 · Conversaciones: guardar tu id (external_id)

6.1 · Contactos

Terminal
# Alta
curl -s -X POST "$API/contacts" \
     -H "Authorization: Bearer $TOKEN" \
     -H 'Accept: application/json' -H 'Content-Type: application/json' \
     -d '{"name":"Ana Ruiz","phone":"+34600111222"}'

# Edición
curl -s -X PATCH "$API/contacts/41" … -d '{"name":"Ana Ruiz Gómez"}'

# Etiquetas (reemplaza la lista entera, no añade)
curl -s -X PUT "$API/contacts/41/tags" … -d '{"tag_ids":[3,7]}'

phone es obligatorio y se normaliza a E.164. name es opcional.

El alta responde 201 con el contacto dentro de contact —{"contact":{"id":541,…}}—, y la lista lo trae dentro de data. No es la misma envoltura: léela según la ruta, o tu código buscará el id donde no está (medido el 22-09-2026).

POST /contacts NO es un upsert. Si el teléfono ya existe responde 422 PHONE_TAKEN y no escribe nada — probado. Para sincronizar usa PUT /contacts (abajo) o busca primero por teléfono o external_id.

Búsqueda exacta y upsert — para sincronizar no hace falta el q (es un LIKE):

Terminal
# 0 o 1 fila, nunca una lista. El teléfono se normaliza igual que en el alta:
# +34600111222, 0034600111222 y 600111222 son el mismo contacto.
curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
     "$API/contacts?phone=+34600111222"
curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
     "$API/contacts?external_id=CRM-7f3a9c"

# Crea (201) o actualiza (200); nunca da PHONE_TAKEN.
curl -s -X PUT "$API/contacts" \
     -H "Authorization: Bearer $TOKEN" \
     -H 'Accept: application/json' -H 'Content-Type: application/json' \
     -d '{"phone":"+34600111222","external_id":"CRM-7f3a9c","name":"Ana Ruiz"}'

PUT /contacts exige phone o external_id (al menos uno). Si mandas los dos y no casan con el mismo contacto —o el que casa es el perdedor de una fusión—, 409 CONTACT_CONFLICT con conflicting_contact_id (nunca PHONE_TAKEN); si el external_id no existe todavía y no mandas phone, 422 VALIDATION_ERROR con el error en errors.phone, porque hace falta para dar de alta. Llamarlo dos veces con el mismo cuerpo deja un único contacto, no dos. El external_id se compara como en §6.6: sin distinguir mayúsculas, acentos ni espacios al final.

6.2 · Mensajes

Terminal
curl -s -X POST "$API/conversations/2856/messages" \
     -H "Authorization: Bearer $TOKEN" \
     -H 'Accept: application/json' -H 'Content-Type: application/json' \
     -d '{"type":"text","body":"Tu pedido sale hoy."}'

Necesita messages:send. Sin ese permiso responde 403 TOKEN_SCOPE_DENIED y no envía nada — comprobado en producción, que es como se prueba esto sin escribirle a una persona real.

type admite: text, template, location, contacts, reaction, product, product_list y, solo en Instagram, quick_replies, button_template y generic_template. Texto libre: 4.096 caracteres en WhatsApp, 1.000 en Instagram.

6.3 · La ventana de 24 horas

Regla de Meta, no de Chatendo. Fuera de las 24 h desde el último mensaje del cliente no sale texto libre: 422 WINDOW_EXPIRED, y no se envía nada.

Fuera de ventana solo pasa una plantilla aprobada (type: "template"). Que el cliente conteste vuelve a abrir la ventana.

Cada conversación te dice cuándo cierra, en window_expires_at y last_inbound_at. Si tu sistema manda avisos que pueden caer fuera, ten la plantilla aprobada antes, no el día que la necesites: Meta tarda en aprobarlas y rechaza, por ejemplo, cualquier plantilla cuya variable quede al principio o al final del texto.

6.4 · Empujar sin token: los webhooks de entrada

Para sistemas que no pueden guardar un token —una tienda, un CRM— hay once puertas firmadas con HMAC contra el secreto del canal (shop_webhook_secret), fuera de /v1:

Puerta Para qué
POST /api/webhooks/shop-order · shop-order-v2 Un pedido nuevo de la tienda
POST /api/webhooks/shop-order-summary La ficha del pedido, compuesta por la tienda
POST /api/webhooks/shop-delivery-reminder Aviso de reparto
POST /api/webhooks/shop-order-reminder «Aún no has pedido»
POST /api/webhooks/contact-identity-v1 El CRM rellena nombre, empresa y código de cliente
POST /api/webhooks/lead-v1 Alguien dejó su número en una web y le escribimos nosotros
POST /api/webhooks/shop-company-account-approved Alta de cuenta de empresa aprobada
POST /api/webhooks/cupones/{tenant} Cupón canjeado
POST /api/webhooks/wa-admins-changed Cambió la lista de administradores

Dos detalles que ahorran una tarde:

  • contact-identity-v1 nunca crea un contacto, solo rellena los que ya existen. Si el contacto no está, no pasa nada y no hay error.
  • lead-v1 sí crea, y además escribe: el primer mensaje es siempre una plantilla aprobada. Sin plantilla no da de alta al contacto.

El secreto es del canal, no de tu integración. Si se lo das a dos sistemas y quieres echar a uno, tienes que rotarlo y romper el otro. Eso cambia en §10.

6.5 · Lo que NO hay que hacer

  • No reintentes una escritura a ciegas. Hoy solo tres rutas miran Idempotency-Key (enlaces de pago, ofertas y etapas). En las demás, un reintento tras un corte de red crea el contacto o manda el mensaje dos veces. Hasta que eso cambie (§10), comprueba antes de reintentar.
  • No uses el token desde el navegador del cliente. Es una llave de tu cuenta: va en tu servidor.
  • No borres conversaciones por API aunque encuentres la ruta: conversations:write no se le da a un token de fuera a propósito.
  • No inventes parámetros. Un parámetro que la ruta no conoce se ignora en silencio y te devuelve 200 con la colección entera. Lo que parece un filtro puede no serlo: compruébalo con un caso que tenga que dar cero.

6.6 · Conversaciones: guardar tu id (external_id)

Igual que en los contactos, una conversación puede guardar el id que tiene en tu sistema:

Terminal
# Guardarlo (null lo borra). Necesita conversations:write.
curl -s -X PATCH "$API/conversations/2856" \
     -H "Authorization: Bearer $TOKEN" \
     -H 'Accept: application/json' -H 'Content-Type: application/json' \
     -d '{"external_id":"CRM-LEAD-2291"}'

# Buscarla luego: 0 o 1 fila, por igualdad, no un `q`. `filter=all` la
# encuentra también si está cerrada (sin `filter`, la lista solo trae las abiertas).
curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
     "$API/conversations?filter=all&external_id=CRM-LEAD-2291"
  • external_id es obligatorio en el cuerpo del PATCH (hasta 191 caracteres; mandar null lo borra). Cualquier otro campo en el cuerpo da 422 UNKNOWN_BODY_FIELD.
  • Es único por inquilino, contando las conversaciones de la papelera: si ya lo tiene otra, 422 VALIDATION_ERROR con el error en errors.external_id — a diferencia de PUT /contacts, aquí no hay un 409 CONTACT_CONFLICT con el id de quien lo tiene, solo el 422 de validación.
  • Una conversación de otro inquilino, o de un canal que no ves, da 404. Buscar el external_id de otro inquilino da una lista vacía, no un error.
  • ?external_id[]= (una lista) no es el mismo parámetro: da 422.
  • La igualdad es la de la base de datos de producción: no distingue mayúsculas, acentos ni espacios al final. CRM-LEAD-1, crm-lead-1 y CRM-LEAD-1 son el mismo id: la búsqueda devuelve la conversación que lo tenga y un segundo PATCH con la variante da 422. Si en tu sistema dos ids solo se diferencian en eso, no los guardes tal cual.
Escríbenos por WhatsApp Te contesta nuestro agente de IA al momento