Quickstart

In 5 steps you'll have an instance connected and sending messages.

  1. Get API key — Contact sales to obtain your tenant credentials.
  2. Create instancePOST /instance/create with the name and your webhook URL.
  3. Scan QRGET /instance/:name/qr and scan the code with your phone.
  4. Wait for connectionGET /instance/:name/status until you receive state: "open".
  5. Send first messagePOST /message/text.
# 1. Create instance
curl -X POST https://wa.sinapsia.com.ar/instance/create \
  -H "apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"instanceName":"acme__soporte__main","webhookUrl":"https://my-saas.com/webhooks/whatsapp","webhookSecret":"my-secure-secret-32"}'

# 2. Get QR (base64 image)
curl https://wa.sinapsia.com.ar/instance/acme__soporte__main/qr \
  -H "apikey: YOUR_API_KEY"

# 3. Check status (wait for state: "open")
curl https://wa.sinapsia.com.ar/instance/acme__soporte__main/status \
  -H "apikey: YOUR_API_KEY"

# 4. Send first message
curl -X POST https://wa.sinapsia.com.ar/message/text \
  -H "apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"instanceName":"acme__soporte__main","to":"5491112345678","text":"Hello! This is my first message.","lane":"normal"}'

Authentication

All requests (except GET /health) require the apikey header.

apikey: YOUR_API_KEY
Header Access
apikey: <your-api-key> Only instances and resources belonging to your tenant

Instance naming

All instances follow this format:

{tenantId}__{clientId}__{label}

Example:
acme__soporte__main
Format rules: Lowercase letters, numbers, hyphens, and underscores only. The separator between segments is a double underscore (__). The system validates this format before creating any instance.

Lanes (Anti-ban)

Every message is sent through one of three lanes. The lane controls the delay and send limits to protect the WhatsApp number.

Lane Delay Limit/hour Limit/day Use for
urgent 1–2s No limit No limit OTP, critical alerts
normal 3–8s 80 msg 500 msg Conversations, notifications
bulk 8–15s 30 msg 200 msg Mass campaigns
Important notes:
  • Limits are per instance, not per tenant.
  • urgent is always processed before normal or bulk.
  • Messages that exceed the limit wait in queue, they are not discarded.
  • Default if not specified: normal.

Endpoints — Instances

POST /instance/create
Creates a new instance for a client. The instance starts in close state until a number is linked by scanning the QR.
Request body
{
  "instanceName": "acme__soporte__main",
  "webhookUrl":    "https://my-saas.com/webhooks/whatsapp",
  "webhookSecret": "secret-minimum-16-characters"
}
201 Created
{ "instanceName": "acme__soporte__main", "status": "close" }
Errors: 400 body validation · 409 instance already exists
GET /instance/:name/qr
Returns the QR code to link the WhatsApp number. The user must open WhatsApp on their phone and scan the code.
200 OK
{ "qrcode": "data:image/png;base64,iVBORw0KGgo..." }
The QR expires in ~40 seconds. If it has expired, call the endpoint again to get a new one.
POST /instance/:name/pairing-code/request
Alternative to QR. Generates a 6-digit pairing code. The user enters it in WhatsApp → Linked devices → Link with phone number.
Request body
{ "phoneNumber": "5491112345678" }
200 OK
{ "pairingCode": "123456" }
GET /instance/:name/status
Returns the current state of the instance. Recommended polling every 3–5s while waiting for connection.
200 OK
{
  "instanceName":       "acme__soporte__main",
  "state":              "open",
  "phoneNumber":        "5491112345678",
  "connectedAt":        "2026-03-15T10:00:00.000Z",
  "messagesQueuedCount": 0
}
stateDescription
closeNot linked. Requires QR or pairing code.
connectingQR scanned. Waiting for confirmation.
openConnected and ready to send messages.
GET /instance/:name/info
Information about the device linked to the instance.
200 OK
{ "device": "iPhone 12", "platform": "WHATSAPP" }
GET /instances
Lists all instances belonging to your tenant.
200 OK
[
  { "instanceName": "acme__soporte__main", "state": "open", "phoneNumber": "5491112345678" },
  { "instanceName": "acme__ventas__main",   "state": "close", "phoneNumber": null }
]
GET /tenant/:tenantId/instances
Lists all instances for a specific tenant.
200 OK
// Array of tenant instances
GET /tenant/:tenantId/client/:clientId/instances
Lists all instances for a specific client within a tenant. Useful for clients with multiple numbers.
200 OK
// Array of client instances
GET /instance/:name/queue
Returns the current state of the instance's message queue, broken down by lane.
200 OK
{ "urgent": 0, "normal": 3, "bulk": 12, "total": 15 }
DELETE /instance/:name
Disconnects WhatsApp and deletes the instance. Pending messages in the queue are marked as failed.
204 No Content

Endpoints — Messages

All send endpoints return 202 Accepted immediately. The message is queued and sent after the anti-ban delay corresponding to the lane.

Shared response (all send endpoints)

{
  "id":       "3b2e1f4a-8c9d-4e2f-b7a1-0d5c6e3f9b2a",
  "status":   "pending",
  "lane":     "normal",
  "queuedAt": "2026-03-15T10:30:00.000Z"
}
POST /message/text
Sends a text message. The text field supports spintax.
{
  "instanceName": "acme__soporte__main",
  "to":          "5491112345678",
  "text":        "{Hello|Hey}! Your code is 1234.",
  "lane":        "normal"
}
to: international number without + or spaces. lane is optional, default normal.
POST /message/image
Sends an image. caption is optional and supports spintax.
{
  "instanceName": "acme__soporte__main",
  "to":          "5491112345678",
  "imageUrl":    "https://cdn.example.com/photo.jpg",
  "caption":     "Check this out",
  "lane":        "normal"
}
POST /message/document
Sends a document. The file name is displayed in the chat.
{
  "instanceName":  "acme__soporte__main",
  "to":           "5491112345678",
  "documentUrl":  "https://cdn.example.com/invoice.pdf",
  "fileName":     "invoice-001.pdf",
  "caption":      "Your invoice attached",
  "lane":         "normal"
}
POST /message/audio
Sends an audio as a voice note (PTT — Push To Talk). Recommended format: OGG/Opus.
{
  "instanceName": "acme__soporte__main",
  "to":          "5491112345678",
  "audioUrl":    "https://cdn.example.com/audio.ogg",
  "lane":        "normal"
}
POST /message/video
Sends a video. caption is optional.
{
  "instanceName": "acme__soporte__main",
  "to":          "5491112345678",
  "videoUrl":    "https://cdn.example.com/video.mp4",
  "caption":     "Watch this",
  "lane":        "normal"
}
POST /message/route
Round-robin auto-routing for clients with multiple numbers. Automatically selects which instance to use. The response includes instanceName to indicate which number was chosen.
{
  "tenantId": "acme",
  "clientId": "soporte",
  "to":       "5491112345678",
  "type":     "text",
  "text":     "Hello!",
  "lane":     "normal"
}
type is required: text | image | document | audio | video. Each type requires its own additional specific fields.
GET /message/:id
Retrieves the status of a message by its UUID.
200 OK
{
  "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
}
statusDescription
pendingIn queue, waiting to be sent
sentSuccessfully sent
failedError — see error field

Webhooks

When creating an instance a webhookUrl is provided. The gateway sends a POST to that URL each time a relevant event occurs.

HMAC signature verification

Each request includes the x-gateway-signature header with an HMAC-SHA256 hex digest of the raw body, signed with your webhookSecret. Always verify the signature before processing.

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

Retry policy

The gateway retries 3 times: delays of 1s, 5s, 30s. Your endpoint must respond 2xx quickly.

Events

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":        "Hello, I need help",
    "messageId":   "AC3EADB76DB5BE4130602FBF2039604A",
    "timestamp":   1773596760
  },
  "timestamp": "2026-03-15T10:30:00.000Z"
}
  • text is null for media messages.
  • Only messages with fromMe: false are forwarded (not your own sent messages).
  • messageType: conversation, imageMessage, documentMessage, audioMessage, videoMessage.

Monitoring

Endpoints to query statistics and manage the message queue for your instances.

GET /admin/stats
Message statistics per instance.
200 OK
{
  "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 }
    }
  ]
}
DELETE /admin/queue/:instanceName
Flushes all pending messages from the queue of an instance.
200 OK
{ "instanceName": "acme__soporte__main", "flushed": 12 }

GET /health

Health endpoint. No authentication required. Useful for monitors and load balancers.

{
  "status":          "ok",
  "tenants":         1,
  "instances":       2,
  "queuedMessages":  5,
  "uptime":          3600
}

Errors

Status Meaning Action
400 Invalid body Check the issues field in the response
401 Missing or invalid API key Verify the apikey header
403 No access to this resource The instanceName must start with your tenantId
404 Instance or message not found Verify the name or ID
409 Instance already exists Use the existing one or choose a different name
502 Error in the WhatsApp service Check WhatsApp session; retry
500 Internal error Report to the operator

400 error format

{
  "error":  "Validation error",
  "issues": [
    {
      "path":    ["instanceName"],
      "message": "Must follow {tenant}__{client}__{label} format"
    }
  ]
}

Spintax

Spintax allows generating text variants to avoid repetitive messages. It is resolved randomly at send time.

Syntax: {option1|option2|option3} — supports nesting.

# Simple
{Hello|Hey|Hi}! {How are you|What's up}?
→ "Hey! What's up?"

# Nested
{Good {morning|afternoon}|Hello}!
→ "Good afternoon!"
Supported in: text field (in /message/text) · caption field (in /message/image, /message/document, /message/video)

Quick reference

Field Format Example
instanceName {tenant}__{client}__{label} acme__soporte__main
to International number without + 5491112345678
lane urgent / normal / bulk normal
type (route) text / image / document / audio / video text
webhookSecret String, min. 16 characters my-secure-secret-32
Timestamps ISO 8601 UTC 2026-03-15T10:30:00.000Z
Message IDs UUID v4 3b2e1f4a-...

Integrations

Ready-to-use configuration for the most popular platforms. All examples send a text message via POST /message/text — the same pattern applies to all other endpoints.

n8n

Add an HTTP Request node and configure it as follows:

Method:   POST
URL:      https://wa.sinapsia.com.ar/message/text
Auth:     Header Auth  →  Name: apikey  /  Value: {{ $vars.WA_API_KEY }}
Body:     JSON

{
  "instanceName": "acme__support__main",
  "to":            "{{ $json.phone }}",
  "text":          "{{ $json.message }}",
  "lane":          "normal"
}

Make (ex-Integromat)

Module HTTP → Make a request:

URL:           https://wa.sinapsia.com.ar/message/text
Method:        POST
Headers:       apikey: YOUR_API_KEY
Body type:     Raw
Content type:  application/json
Request body:

{
  "instanceName": "acme__support__main",
  "to":            "{{1.phone}}",
  "text":          "{{1.message}}",
  "lane":          "normal"
}

Zapier

Action Webhooks by Zapier → POST:

URL:          https://wa.sinapsia.com.ar/message/text
Payload Type: JSON
Data:
  instanceName  acme__support__main
  to            (phone field from trigger)
  text          (message field from trigger)
  lane          normal
Headers:
  apikey        YOUR_API_KEY

Power Automate

Add an HTTP action to your flow:

Method:  POST
URI:     https://wa.sinapsia.com.ar/message/text
Headers:
  apikey        @{variables('WA_API_KEY')}
  Content-Type  application/json
Body:
{
  "instanceName": "acme__support__main",
  "to":            "@{triggerBody()?['phone']}",
  "text":          "@{triggerBody()?['message']}",
  "lane":          "normal"
}

Pipedream

Add a Node.js code step:

import axios from "axios"

export default defineComponent({
  async run({ steps, $ }) {
    const { data } = await axios.post(
      "https://wa.sinapsia.com.ar/message/text",
      {
        instanceName: "acme__support__main",
        to:           steps.trigger.event.phone,
        text:         steps.trigger.event.message,
        lane:         "normal",
      },
      { headers: { apikey: process.env.WA_API_KEY } }
    )
    return data
  },
})

Home Assistant

Add to configuration.yaml. Store the API key in secrets.yaml.

# configuration.yaml
rest_command:
  send_whatsapp:
    url: https://wa.sinapsia.com.ar/message/text
    method: POST
    headers:
      apikey: !secret wa_api_key
      Content-Type: application/json
    payload: >
      {"instanceName":"acme__support__main",
       "to":"{{ to }}",
       "text":"{{ message }}",
       "lane":"normal"}
    content_type: application/json
Call with: service: rest_command.send_whatsapp and data: {to: "1...", message: "Hello!"}

Your backend

// native fetch (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__support__main",
    to: "15551234567",
    text: "Hello from 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__support__main",
        "to":            "15551234567",
        "text":          "Hello from 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__support__main',
        'to'           => '15551234567',
        'text'         => 'Hello from PHP!',
        'lane'         => 'normal',
    ],
]);
$data = json_decode($res->getBody(), true);
import (
    "bytes"; "encoding/json"; "net/http"; "os"
)

payload, _ := json.Marshal(map[string]string{
    "instanceName": "acme__support__main",
    "to":           "15551234567",
    "text":         "Hello from 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)