Ir al contenido

Inicio rápido

En cinco pasos pasas de una API key a un webhook firmado que tu app verifica. Los ejemplos usan https://motor.ejemplo.com como dirección del motor y TU_API_KEY como token: reemplázalos por los tuyos. El motor vive en tu red privada; no se expone a internet.

Una app cliente es tu sistema visto desde el motor: tiene sus instancias, sus webhooks y sus keys, aisladas de las demás apps. Las apps y las keys las crea un operador con rol superadmin en el panel del motor:

  1. En Clientes, elige Nueva app y ponle un nombre.
  2. En la ficha de la app, elige Emitir key. Escoge el entorno (Producción da mw_live_…; Prueba, mw_test_…), los scopes y, si quieres, las instancias a las que alcanza.
  3. Copia el token del diálogo Key emitida. Se muestra una sola vez: si lo pierdes, rota la key y usa la nueva.

Para esta guía, la key necesita estos scopes: instances:create, instances:read, instances:pair, messages:send, messages:read, subscriptions:manage y events:read.

Comprueba que la key funciona con GET /v1/whoami: no pide ningún scope y te dice qué app y qué permisos tiene. Una instancias vacía significa «todas las instancias de la app».

Ventana de terminal
curl https://motor.ejemplo.com/v1/whoami \
-H "Authorization: Bearer TU_API_KEY"
200 OK
{
"cliente": { "id": "8a1d2c3e-0000-4000-8000-000000000001", "nombre": "Mi CRM" },
"key": {
"id": "8a1d2c3e-0000-4000-8000-000000000002",
"prefijo": "mw_live_1a2b3c4d",
"entorno": "produccion",
"scopes": ["events:read", "instances:create", "instances:pair", "instances:read",
"messages:read", "messages:send", "subscriptions:manage"],
"instancias": []
}
}

Una instancia es un número de WhatsApp vinculado al motor. Créala con POST /v1/instances (scope instances:create). owner_ref es una referencia opaca tuya, por ejemplo el ID de la sucursal en tu sistema.

Manda una Idempotency-Key (opcional aquí, hasta 200 caracteres): si repites el pedido con la misma clave y los mismos datos, recibes la misma instancia en lugar de una nueva; con la misma clave y otros datos, un 409. Si la app ya llegó a su cupo de instancias, la respuesta es un 429.

La instancia nace en estado nuevo. Guarda su id: lo usan todos los pasos siguientes.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: alta-sucursal-centro" \
-d '{"nombre": "Sucursal centro", "owner_ref": "sucursal-centro"}'

Pide el emparejamiento con POST /v1/instances/{id}/pairing (scope instances:pair). Hay dos modos:

  • codigo: el motor pide un código de 8 caracteres para el número que indicas en telefono (completo, con + y el código de país). En el teléfono, abre Dispositivos vinculados → Vincular un dispositivo → Vincular con el número de teléfono y escríbelo.
  • qr: el motor genera un QR. reto.valor es el texto del QR, no una imagen: dibújalo con tu biblioteca de QR y escanéalo desde Dispositivos vinculados.

La respuesta es un 202: el reto llega por separado. Consúltalo con GET /v1/instances/{id}/pairing; reto.expira_en dice cuándo vence. Cada pedido nuevo abre una generación nueva y reemplaza al anterior, así que un QR vencido se resuelve pidiendo otro.

Cuando el teléfono acepta, el ledger de la instancia registra pairing.available, paired y connected, y la instancia queda en estado conectado con su numero.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/pairing \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"modo": "codigo", "telefono": "+TU_NUMERO_CON_CODIGO_DE_PAIS"}'
# El reto vigente:
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/pairing \
-H "Authorization: Bearer TU_API_KEY"
GET …/pairing · 200 OK
{
"generacion": 1,
"modo": "codigo",
"fase": "esperando_codigo",
"reto": { "tipo": "codigo", "valor": "ABCD-EFGH", "expira_en": "2026-10-24T15:04:05Z" },
"creado_en": "2026-10-24T15:02:05Z"
}

POST /v1/instances/{id}/messages (scope messages:send) no manda el mensaje en la respuesta: lo guarda como un comando y responde 202 con status: en_cola. Desde ese momento el envío es durable: sobrevive a un reinicio del motor.

  • destino lleva exactamente uno de telefono (número internacional), jid o peer_ref (el que traen los eventos de esta instancia).
  • La Idempotency-Key es obligatoria. Si la red se corta y no sabes si el pedido llegó, repítelo con la misma clave: recibes el mismo comando, nunca un segundo mensaje. La misma clave con otro mensaje es un 409.

El resultado llega después: message.sent cuando el servidor de WhatsApp lo acepta, y message.updated con estado entregado o leido. También puedes consultarlo con GET /v1/commands/{id}. Lo explica a fondo Comandos asíncronos y eventos.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042-confirmacion" \
-d '{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "text",
"texto": "Hola, tu pedido 1042 está confirmado."
}'
202 Accepted
{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c1",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"idempotency_key": "pedido-1042-confirmacion",
"type": "message.text",
"generation": 1,
"status": "en_cola",
"created_at": "2026-10-24T15:10:00Z",
"updated_at": "2026-10-24T15:10:00Z"
}

Suscríbete con POST /v1/subscriptions (scope subscriptions:manage). tipos vacío recibe todos los tipos; instancias vacío, todas las instancias de la app. Con ordenado (por omisión, true) cada instancia se entrega en orden de seq.

La respuesta trae secreto (key_id y valor) una sola vez: guárdalo sellado en tu app. El host de la URL tiene que estar en los destinos permitidos del motor o de tu app: si no, el alta responde 422. Pídeselo al operador.

Cada entrega es un POST con el sobre del evento y estos encabezados: X-MW-Key-Id, X-MW-Timestamp, X-MW-Delivery-Id, X-MW-Event-Id y X-MW-Signature: v1=<hex>, un HMAC-SHA256 con el secreto sobre timestamp + "." + delivery_id + "." + body. En la ventana de una rotación llegan dos firmas separadas por comas, en el mismo orden que los X-MW-Key-Id.

Tu receptor tiene que:

  1. Verificar la firma sobre el cuerpo crudo, antes de parsear nada, y rechazar un timestamp fuera de los 5 minutos con un 408 (es un reloj desfasado: el motor lo reintenta), nunca con un 401.
  2. Guardar el evento y deduplicar por event_id: la entrega es «al menos una vez».
  3. Responder 2xx solo después de guardarlo. Un error de red, un timeout, un 408, un 429 o un 5xx se reintentan; un 401 o un 403 suspenden la suscripción, y cualquier otro 3xx o 4xx deja la entrega como fallida, sin reintentos (se recupera con un replay).

Para probar el destino sin esperar un mensaje, POST /v1/subscriptions/{id}/test manda un webhook.test firmado.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/subscriptions \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"nombre": "CRM principal",
"url": "https://crm.ejemplo.com/webhooks/motor",
"tipos": ["message.received", "message.updated", "command.failed"]
}'
# Un webhook.test firmado al destino:
curl -X POST https://motor.ejemplo.com/v1/subscriptions/SUSCRIPCION_ID/test \
-H "Authorization: Bearer TU_API_KEY"
201 Created
{
"suscripcion": {
"id": "0f3b9d22-0000-4000-8000-0000000000d1",
"nombre": "CRM principal",
"url": "https://crm.ejemplo.com/webhooks/motor",
"tipos": ["message.received", "message.updated", "command.failed"],
"ordenado": true,
"estado": "activa"
},
"secreto": { "key_id": "SECRETO_KEY_ID", "valor": "TU_SECRETO_DE_WEBHOOK" }
}