Contactos y consentimiento
Todo número que conversa con tu cuenta se vuelve un contacto. La API permite que tu sistema — un CRM, una tienda, un ERP — mantenga esa base sincronizada: crear y actualizar contactos, guardar campos propios, organizarlos con etiquetas y, sobre todo, registrar quién aceptó recibir marketing. Ese registro es lo que decide quién entra en una campaña.
Crear o actualizar un contacto
curl -X POST 'https://api.joinotify.com/contacts?upsert=true' \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"phone": "+55 41 98711-1527",
"firstName": "Ana",
"email": "[email protected]",
"attributes": { "plan": "Pro" }
}'
El teléfono se normaliza a E.164 y se guarda sin el + (5541987111527). Sin
código de país, envía defaultCountry: "BR". Los móviles brasileños con y sin el noveno
dígito se reconocen como el mismo contacto.
Sin ?upsert=true, un teléfono que ya existe responde 409 contact_exists con su
contactId. Con él, el contacto existente se actualiza y la respuesta es 200.
Sincronizar por lotes
Para una base grande, POST /contacts/batch acepta hasta 500 contactos por llamada y
devuelve el resultado de cada fila, en el orden en que se enviaron:
{
"contacts": [
{ "phone": "+5541987111527", "name": "Ana Souza", "tags": ["cliente"] },
{ "phone": "+5511912345678", "firstName": "Bruno", "attributes": { "ciudad": "São Paulo" } }
],
"options": { "onDuplicate": "update", "defaultCountry": "BR" }
}
La respuesta es 200 aunque fallen filas — revisa results[].status. Las filas con
failed traen reason y, cuando se puede, el field culpable. Un teléfono repetido
dentro del mismo lote se vuelve un solo contacto.
En una actualización por lote, solo los valores rellenos sobrescriben y las etiquetas se
suman, nunca se quitan. Para una importación de miles de filas en varios lotes, abre
antes un registro con POST /contacts/imports y envía su id en options.importId: los
contadores se suman allí, y el panel muestra el avance.
contact.created y contact.opted_in salen en la creación individual, no en el lote —
una importación de diez mil filas no se vuelve diez mil llamadas a tu endpoint.
Campos personalizados y etiquetas
Los campos personalizados guardan lo tuyo: el plan contratado, la ciudad, la fecha de la
última compra. Crea el campo una vez con POST /contacts/fields y graba el valor en
attributes, por su key. El valor se valida por tipo — un campo date acepta
2026-09-19 o 19/09/2026, un boolean acepta si/no, un select solo acepta las
opciones registradas.
Las etiquetas son rótulos libres para segmentar. En una edición individual, tagIds es
el conjunto completo; para poner o quitar una etiqueta a muchos contactos, usa
POST /contacts/tags/apply con una lista de ids o con los mismos filtros del listado.
Consentimiento de marketing
Las campañas de marketing solo van a quien aceptó recibirlas. Cada contacto tiene
optInStatus: opted_in, opted_out o unknown.
Registra la aceptación junto con la evidencia de cómo se obtuvo — es lo que vas a necesitar mostrar si alguien pregunta:
curl -X POST https://api.joinotify.com/contacts/cm1c0ntact0001/opt-in \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "evidence": "Casilla marcada en el checkout del pedido 1042" }'
O ya en la creación, con "optIn": { "evidence": "..." }.
La baja ocurre por varios caminos, y no todos pasan por tu integración: la persona envía
una palabra de baja, Meta rechaza un envío con el error 131050, alguien la registra en el
panel o tu integración llama a POST /contacts/{id}/opt-out. Para enterarte de todas,
suscríbete al evento contact.opted_out.
Ningún camino automático — lote, upsert, unión de duplicados — convierte una baja en un
opt-in. Solo un nuevo POST /contacts/{id}/opt-in, con evidencia, lo hace.
Lista de supresión
La lista de supresión es lo que garantiza la baja: los teléfonos y BSUID que están en
ella nunca reciben una campaña de marketing, sea cual sea el consentimiento del
contacto. Una baja pone el contacto en la lista automáticamente, con el motivo opt_out;
puedes añadir otros a mano con POST /suppressions.
| Motivo | De dónde viene |
|---|---|
opt_out | El contacto se dio de baja |
meta_marketing_block | Meta está bloqueando el marketing para ese número |
invalid_number | El número no tiene WhatsApp |
manual | Alguien lo añadió en el panel o con POST /suppressions |
Quitar de la lista una fila opt_out o meta_marketing_block exige confirm=true — y no
cambia el consentimiento: el contacto sigue dado de baja hasta un nuevo opt-in.
Privacidad
DELETE /contacts/{id}borra el contacto de forma definitiva, con conversaciones e historial — la solicitud de supresión de datos. La fila de la lista de supresión queda, sin la identidad.GET /contacts/{id}/exportdevuelve todo lo que la cuenta guarda sobre la persona, en un JSON — la solicitud de acceso.GET /contacts/exportgenera un CSV de toda la base, o de lo que seleccionen los filtros.
Una API key restringida a algunos números solo ve los contactos que tienen conversación
en esos números: la lista trae solo a ellos, y un contacto fuera de ese recorte responde
404, como uno que no existe. Con ella puedes leer, editar campos y tags y registrar un
opt-out.
Lo que es de toda la base responde 403: exportar, importar, crear, lote, borrar, unir,
opt-in, las definiciones de campos y tags y la lista de supresión. Para eso, usa una clave
sin restricción.