Quickstart
En 5 pasos tenés una instancia conectada y enviando mensajes.
- Obtener API key — Contactar a ventas para obtener las credenciales de tu tenant.
- Crear instancia —
POST /instance/createcon el nombre y tu webhook URL. - Escanear QR —
GET /instance/:name/qry escanear el código con el celular. - Esperar conexión —
GET /instance/:name/statushasta recibirstate: "open". - Enviar primer mensaje —
POST /message/text.
# 1. Crear instancia
curl -X POST https://wa.sinapsia.com.ar/instance/create \
-H "apikey: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"instanceName":"acme__soporte__main","webhookUrl":"https://mi-saas.com/webhooks/whatsapp","webhookSecret":"mi-secreto-seguro-32"}'
# 2. Obtener QR (imagen base64)
curl https://wa.sinapsia.com.ar/instance/acme__soporte__main/qr \
-H "apikey: TU_API_KEY"
# 3. Verificar estado (esperar state: "open")
curl https://wa.sinapsia.com.ar/instance/acme__soporte__main/status \
-H "apikey: TU_API_KEY"
# 4. Enviar primer mensaje
curl -X POST https://wa.sinapsia.com.ar/message/text \
-H "apikey: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"instanceName":"acme__soporte__main","to":"5491112345678","text":"Hola! Este es mi primer mensaje.","lane":"normal"}'
Autenticación
Todas las requests (salvo GET /health) requieren el header apikey.
apikey: TU_API_KEY
| Header | Acceso |
|---|---|
apikey: <tu-api-key> |
Solo instancias y recursos de tu tenant |
Naming de instancias
Todas las instancias siguen el formato:
{tenantId}__{clientId}__{label}
Ejemplo:
acme__soporte__main
__). El sistema valida este formato antes de crear cualquier instancia.
Carriles (Anti-ban)
Cada mensaje se envía por uno de tres carriles. El carril controla el delay y los límites de envío para proteger el número de WhatsApp.
| Lane | Delay | Límite/hora | Límite/día | Usar para |
|---|---|---|---|---|
urgent |
1–2s | Sin límite | Sin límite | OTP, alertas críticas |
normal |
3–8s | 80 msg | 500 msg | Conversación, notificaciones |
bulk |
8–15s | 30 msg | 200 msg | Campañas masivas |
- Límites por instancia, no por tenant.
urgentsiempre se procesa antes quenormalobulk.- Mensajes que exceden el límite esperan en cola, no se descartan.
- Default si no se especifica:
normal.
Endpoints — Instancias
close hasta que se vincula un número escaneando el QR.{
"instanceName": "acme__soporte__main",
"webhookUrl": "https://mi-saas.com/webhooks/whatsapp",
"webhookSecret": "secreto-minimo-16-caracteres"
}
201 Created
{ "instanceName": "acme__soporte__main", "status": "close" }
400 validación de body · 409 la instancia ya existe
{ "qrcode": "data:image/png;base64,iVBORw0KGgo..." }
{ "phoneNumber": "5491112345678" }
200 OK
{ "pairingCode": "123456" }
{
"instanceName": "acme__soporte__main",
"state": "open",
"phoneNumber": "5491112345678",
"connectedAt": "2026-03-15T10:00:00.000Z",
"messagesQueuedCount": 0
}
| state | Descripción |
|---|---|
close | No vinculado. Requiere QR o pairing code. |
connecting | QR escaneado. Esperando confirmación. |
open | Conectado y listo para enviar mensajes. |
{ "device": "iPhone 12", "platform": "WHATSAPP" }
[
{ "instanceName": "acme__soporte__main", "state": "open", "phoneNumber": "5491112345678" },
{ "instanceName": "acme__ventas__main", "state": "close", "phoneNumber": null }
]
// Array de instancias del tenant
// Array de instancias del cliente
{ "urgent": 0, "normal": 3, "bulk": 12, "total": 15 }
Endpoints — Mensajes
202 Accepted inmediatamente. El mensaje se encola y se envía después del delay anti-ban correspondiente al carril.
Respuesta compartida (todos los endpoints de envío)
{
"id": "3b2e1f4a-8c9d-4e2f-b7a1-0d5c6e3f9b2a",
"status": "pending",
"lane": "normal",
"queuedAt": "2026-03-15T10:30:00.000Z"
}
text soporta spintax.{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"text": "{Hola|Hey}! Tu código es 1234.",
"lane": "normal"
}
to: número internacional sin + ni espacios. lane es opcional, default normal.caption es opcional y soporta spintax.{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"imageUrl": "https://cdn.example.com/foto.jpg",
"caption": "Mirá esto",
"lane": "normal"
}
{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"documentUrl": "https://cdn.example.com/factura.pdf",
"fileName": "factura-001.pdf",
"caption": "Tu factura adjunta",
"lane": "normal"
}
{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"audioUrl": "https://cdn.example.com/audio.ogg",
"lane": "normal"
}
caption es opcional.{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"videoUrl": "https://cdn.example.com/video.mp4",
"caption": "Miralo",
"lane": "normal"
}
instanceName para saber qué número se eligió.{
"tenantId": "acme",
"clientId": "soporte",
"to": "5491112345678",
"type": "text",
"text": "Hola!",
"lane": "normal"
}
type es requerido: text | image | document | audio | video. Cada tipo requiere sus campos específicos adicionales.{
"id": "3b2e1f4a-8c9d-4e2f-b7a1-0d5c6e3f9b2a",
"instanceName": "acme__soporte__main",
"lane": "normal",
"to": "5491112345678",
"type": "text",
"status": "sent",
"createdAt": "2026-03-15T10:30:00.000Z",
"sentAt": "2026-03-15T10:30:07.000Z",
"error": null
}
| status | Descripción |
|---|---|
| pending | En cola, esperando ser enviado |
| sent | Enviado correctamente |
| failed | Error — ver campo error |
Webhooks
Al crear una instancia se provee un webhookUrl. El gateway envía un POST a esa URL cada vez que ocurre un evento relevante.
Verificación de firma HMAC
Cada request incluye el header x-gateway-signature con un HMAC-SHA256 en hex del body crudo, firmado con tu webhookSecret. Siempre verificar la firma antes de procesar.
const crypto = require('crypto')
function verifySignature(rawBody, secret, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return expected === signature
}
import hmac, hashlib
def verify_signature(raw_body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return expected == signature
Política de reintentos
El gateway reintenta 3 veces: delays de 1s, 5s, 30s. Tu endpoint debe responder 2xx rápidamente.
Eventos
instance.connected / instance.disconnected
{
"event": "instance.connected",
"instanceName": "acme__soporte__main",
"data": {
"state": "open",
"reason": null
},
"timestamp": "2026-03-15T10:30:00.000Z"
}
message.received
{
"event": "message.received",
"instanceName": "acme__soporte__main",
"data": {
"from": "5491112345678",
"pushName": "Juan Pérez",
"messageType": "conversation",
"text": "Hola, necesito ayuda",
"messageId": "AC3EADB76DB5BE4130602FBF2039604A",
"timestamp": 1773596760
},
"timestamp": "2026-03-15T10:30:00.000Z"
}
textesnullpara mensajes con media.- Solo se reenvían mensajes con
fromMe: false(no los propios mensajes enviados). messageType:conversation,imageMessage,documentMessage,audioMessage,videoMessage.
Monitoreo
Endpoints para consultar estadísticas y gestionar la cola de mensajes de tus instancias.
{
"instances": 2,
"totals": { "queued": 5, "sent": 1250, "failed": 3 },
"perInstance": [
{
"instanceName": "acme__soporte__main",
"queue": { "urgent": 0, "normal": 5, "bulk": 2, "total": 7 },
"messages": { "sent": 450, "failed": 1, "pending": 7 }
}
]
}
{ "instanceName": "acme__soporte__main", "flushed": 12 }
GET /health
Endpoint de salud. No requiere autenticación. Útil para monitores y load balancers.
{
"status": "ok",
"tenants": 1,
"instances": 2,
"queuedMessages": 5,
"uptime": 3600
}
Errores
| Status | Significado | Acción |
|---|---|---|
400 |
Body inválido | Ver campo issues en respuesta |
401 |
API key faltante o inválida | Verificar header apikey |
403 |
Sin acceso a este recurso | El instanceName debe empezar con tu tenantId |
404 |
Instancia o mensaje no existe | Verificar nombre o ID |
409 |
Instancia ya existe | Usar la existente o elegir otro nombre |
502 |
Error en el servicio de WhatsApp | Verificar sesión WhatsApp; reintentar |
500 |
Error interno | Reportar al operador |
Formato de error 400
{
"error": "Validation error",
"issues": [
{
"path": ["instanceName"],
"message": "Must follow {tenant}__{client}__{label} format"
}
]
}
Spintax
El spintax permite generar variantes de texto para evitar mensajes repetitivos. Se resuelve aleatoriamente en el momento del envío.
Sintaxis: {opción1|opción2|opción3} — soporta anidamiento.
# Simple
{Hola|Hey|Buenas}! {Como estás|Qué tal}?
→ "Hey! Qué tal?"
# Anidado
{Buen{os días|as tardes}|Hola}!
→ "Buenas tardes!"
text (en /message/text) · campo caption (en /message/image, /message/document, /message/video)
Referencia rápida
| Campo | Formato | Ejemplo |
|---|---|---|
instanceName |
{tenant}__{client}__{label} |
acme__soporte__main |
to |
Número internacional sin + |
5491112345678 |
lane |
urgent / normal / bulk |
normal |
type (route) |
text / image / document / audio / video |
text |
webhookSecret |
String, mín. 16 caracteres | mi-secreto-seguro-32 |
| Timestamps | ISO 8601 UTC | 2026-03-15T10:30:00.000Z |
| IDs de mensaje | UUID v4 | 3b2e1f4a-... |
Integraciones
Configuración lista para usar con las plataformas más populares. Todos los ejemplos envían un mensaje de texto vía POST /message/text — el mismo patrón aplica al resto de endpoints.
n8n
Agregar un nodo HTTP Request y configurarlo con los siguientes parámetros:
Method: POST
URL: https://wa.sinapsia.com.ar/message/text
Auth: Header Auth → Name: apikey / Value: {{ $vars.WA_API_KEY }}
Body: JSON
{
"instanceName": "acme__soporte__main",
"to": "{{ $json.phone }}",
"text": "{{ $json.message }}",
"lane": "normal"
}
Make (ex-Integromat)
Módulo HTTP → Make a request:
URL: https://wa.sinapsia.com.ar/message/text
Method: POST
Headers: apikey: TU_API_KEY
Body type: Raw
Content type: application/json
Request body:
{
"instanceName": "acme__soporte__main",
"to": "{{1.phone}}",
"text": "{{1.message}}",
"lane": "normal"
}
Zapier
Acción Webhooks by Zapier → POST:
URL: https://wa.sinapsia.com.ar/message/text
Payload Type: JSON
Data:
instanceName acme__soporte__main
to (campo teléfono del trigger)
text (campo mensaje del trigger)
lane normal
Headers:
apikey TU_API_KEY
Tu backend
// fetch nativo (Node.js 18+)
const res = await fetch("https://wa.sinapsia.com.ar/message/text", {
method: "POST",
headers: {
"apikey": process.env.WA_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
instanceName: "acme__soporte__main",
to: "5491112345678",
text: "Hola desde Node.js!",
lane: "normal",
}),
})
const data = await res.json()
import httpx, os
res = httpx.post(
"https://wa.sinapsia.com.ar/message/text",
headers={"apikey": os.environ["WA_API_KEY"]},
json={
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"text": "Hola desde Python!",
"lane": "normal",
},
)
data = res.json()
// Guzzle HTTP
$client = new \GuzzleHttp\Client();
$res = $client->post('https://wa.sinapsia.com.ar/message/text', [
'headers' => ['apikey' => getenv('WA_API_KEY')],
'json' => [
'instanceName' => 'acme__soporte__main',
'to' => '5491112345678',
'text' => 'Hola desde PHP!',
'lane' => 'normal',
],
]);
$data = json_decode($res->getBody(), true);
import (
"bytes"; "encoding/json"; "net/http"; "os"
)
payload, _ := json.Marshal(map[string]string{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"text": "Hola desde Go!",
"lane": "normal",
})
req, _ := http.NewRequest("POST", "https://wa.sinapsia.com.ar/message/text", bytes.NewBuffer(payload))
req.Header.Set("apikey", os.Getenv("WA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)