Volver al blog
Desarrolladores

Abrir un chat de WhatsApp desde tu CRM con la API de Waspy

Desde el CRM abrís el chat con GET /conversations/resolve, sin el UUID interno. by-phone arma variantes +54 y 549. El PATCH de estado solo acepta open o closed.

· 12 min
Abrir un chat de WhatsApp desde tu CRM con la API de Waspy

Respuesta corta: para abrir el chat de un cliente desde el CRM, el ERP o un ticket, no hace falta el UUID interno. GET https://api.waspytech.com/api/v2/conversations/resolve?phone= (o ig, o email) devuelve conversationId. El scope es conversations:read. Si no hay chat, conversationId es null: este GET no crea la conversación.

La referencia está en conversations. Donde esa página y el servidor no coinciden, abajo va lo que acepta el código. El listado general de la bandeja está en conversaciones por API. La clave se crea en los planes Scale y Enterprise: Planes y la API.

Hace falta al menos un identificador: teléfono, usuario de Instagram o email. line y channel no filtran. Si la persona tiene un chat de WhatsApp y otro de Instagram, ordenan cuál vuelve primero. Los otros quedan en alternatives.

ParámetroQué acepta
phoneDígitos, espacios, guiones y +. Variantes 54 y 549. También los últimos 8 dígitos si el número guardado no coincide exacto
igUsuario, con o sin @, o el IGSID
emailEl email del contacto, o el del canal de email
linePreferencia de línea: últimos dígitos del número de la tienda, el id de Meta o el UUID del canal en Waspy
channelwhatsapp, instagram o email. Otro valor: 400 BAD_REQUEST
curl -sS "https://api.waspytech.com/api/v2/conversations/resolve?phone=5491140796637&line=28392709&channel=whatsapp" \
  -H "Authorization: Bearer $WASPY_API_KEY"

La respuesta 200 trae conversationId, match, alternatives y phoneHasWhatsApp. match incluye channel, contactName, contactPhone, contactUsername, contactEmail, lastMessageAt y lineLabel.

phoneHasWhatsApp es verdadero solo si algún chat devuelto es de WhatsApp y el teléfono guardado es una variante 54/549 del que pediste. Un match por Instagram, por email o por los últimos 8 dígitos de otro número no cuenta. Si conversationId vino lleno y phoneHasWhatsApp es falso, mirá match.contactPhone antes de asumir que es esa persona.

Sin teléfono, sin Instagram y sin email, la respuesta es 400 BAD_REQUEST. Los grupos no entran en la búsqueda. Una API key limitada a ciertas líneas solo ve esas líneas. Un token de usuario respeta la visibilidad del inbox: si esa persona no puede abrir el chat, no aparece.

El mismo enlace, para alguien que ya está logueado en el panel:

https://waspytech.com/inbox/open?phone=5491140796637&line=28392709&channel=whatsapp

El panel pide login y vuelve a esa URL. Si no hay chat de WhatsApp y el rol puede escribir, el inbox prepara la conversación y abre el selector de plantilla. Eso no es la API pública. GET /api/v2/conversations/resolve no tiene un equivalente que cree el hilo. Si conversationId es null, en tu CRM mostrá “todavía no hay chat”. El primer mensaje, si corresponde, es un POST /api/v2/messages con plantilla, como está en la API para desarrolladores.

Node, el caso del ERP

El ERP de una distribuidora de Rosario tiene el teléfono del cliente y el interno de la línea de ventas. No guarda UUIDs de Waspy.

const BASE = 'https://api.waspytech.com/api/v2';

type ResolveMatch = {
  conversationId: string;
  channel: string;
  contactName: string | null;
  contactPhone: string | null;
  contactUsername: string | null;
  contactEmail: string | null;
  lastMessageAt: string | null;
  lineLabel: string | null;
};

type ResolveData = {
  conversationId: string | null;
  match: ResolveMatch | null;
  alternatives: ResolveMatch[];
  phoneHasWhatsApp: boolean;
};

export async function openChatFromCrm(phone: string, line?: string): Promise<ResolveData> {
  const url = new URL(BASE + '/conversations/resolve');
  url.searchParams.set('phone', phone);
  url.searchParams.set('channel', 'whatsapp');
  if (line) url.searchParams.set('line', line);

  const res = await fetch(url, {
    headers: { Authorization: 'Bearer ' + process.env.WASPY_API_KEY },
  });
  if (!res.ok) {
    throw new Error('resolve ' + res.status);
  }
  const body = (await res.json()) as { data: ResolveData };
  return body.data;
}

Con el UUID, el inbox embebido o tu panel abre /inbox/ más ese id. Para asignarlo al vendedor del ticket:

curl -sS -X PATCH "https://api.waspytech.com/api/v2/conversations/$CONV_ID" \
  -H "Authorization: Bearer $WASPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "open", "assignedTo": "USER_UUID" }'

Scope: conversations:write. assignedTo: null suelta el chat. Si el UUID no es un usuario activo de esa cuenta, 400 VALIDATION_ERROR (assignedTo user not found in this tenant). Cerrar es status: "closed". Cerrar no borra el hilo y no impide que el cliente vuelva a escribir.

open y closed, nada más

La página de docs lista open, pending, resolved y closed en el listado y en el PATCH. El schema del servidor es otro: solo open y closed. pending y resolved no se guardan. La respuesta es 400 VALIDATION_ERROR, porque zod frena el body antes del handler.

Lo mismo en el filtro del listado: GET /conversations?status=resolved no es un estado válido. El estado de la base también es open o closed.

by-phone, si solo tenés el celular

GET /api/v2/conversations/by-phone/:phone devuelve el chat de WhatsApp más reciente de ese número, o data: null con 200 (no es un 404). No trae alternatives, no busca Instagram y no trae serviceWindowExpiresAt. Para la ventana de 24 horas hacé después GET /api/v2/conversations/:id.

El + en el path va codificado (%2B). La ruta borra todo lo que no sea dígito y arma variantes:

Input (solo dígitos)Qué busca, además del propio número
Empieza con 549 y tiene 12 dígitos o másTambién 54 + el resto, sin el 9
Empieza con 54 pero no con 549, 11 dígitos o másTambién 549 + el resto (inserta el 9)
No empieza con 54 y tiene 8 dígitos o másTambién 54 + dígitos y 549 + dígitos
Cada variante se busca con + y sin +.

El 15 viejo no se recorta en esta ruta. 54111540796637 (54, área 11, 15, abonado) se busca tal cual y como 549111540796637: el 15 sigue en el medio, así que no matchea el +5491140796637 canónico.

resolve suma formatE164. Esa función saca ceros de adelante y, si el 15 está pegado al 54 (5415…), lo borra y deja +549…. Si el 15 viene después del código de área (541115…), no lo toca. La página de docs dice que by-phone elimina el 15 solo. El handler no hace eso. Pasá el número en +549 sin 0 y sin 15.

Qué no hacer con el id

  • No uses resolve para “ver si el número tiene WhatsApp en el mundo”. Dice si en tu cuenta ya hay un chat, nada más.
  • No mandes status: "resolved" copiado de la doc. El servidor lo rechaza.
  • No marques leído ni muestres “escribiendo…” en este paso. Esos POST están en el indicador y el leído.
  • Si una persona del equipo ya contestó, tu integración no debería seguir el hilo como si fuera suyo. La señal es origin: "panel", en handoff.

Leer, resolver y asignar no son un mensaje entregado: Meta no los cobra. Lo que cobra es el texto o la plantilla que después mandás. En Argentina, desde el 1 de octubre de 2026, marketing alrededor de ARS 89,56 (US$0,0618) y utilidad o servicio, pasado el cupo de 1.000 por número y por mes, alrededor de ARS 37,68 (US$0,026). La rate card es la fuente. Los scopes están en docs.waspytech.com/scopes.

Diez días de prueba, sin tarjeta, en el registro.

Tu equipo atendiendo por WhatsApp en minutos

CRM facil pero potente. En 10 minutos tenes a tu equipo trabajando — no necesitas hablar con nadie.