Quickstart
In 5 steps you'll have an instance connected and sending messages.
- Get API key — Contact sales to obtain your tenant credentials.
- Create instance —
POST /instance/createwith the name and your webhook URL. - Scan QR —
GET /instance/:name/qrand scan the code with your phone. - Wait for connection —
GET /instance/:name/statusuntil you receivestate: "open". - Send first message —
POST /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
__). 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 |
- Limits are per instance, not per tenant.
urgentis always processed beforenormalorbulk.- Messages that exceed the limit wait in queue, they are not discarded.
- Default if not specified:
normal.
Endpoints — Instances
close state until a number is linked by scanning the QR.{
"instanceName": "acme__soporte__main",
"webhookUrl": "https://my-saas.com/webhooks/whatsapp",
"webhookSecret": "secret-minimum-16-characters"
}
201 Created
{ "instanceName": "acme__soporte__main", "status": "close" }
400 body validation · 409 instance already exists
{ "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 | Description |
|---|---|
close | Not linked. Requires QR or pairing code. |
connecting | QR scanned. Waiting for confirmation. |
open | Connected and ready to send messages. |
{ "device": "iPhone 12", "platform": "WHATSAPP" }
[
{ "instanceName": "acme__soporte__main", "state": "open", "phoneNumber": "5491112345678" },
{ "instanceName": "acme__ventas__main", "state": "close", "phoneNumber": null }
]
// Array of tenant instances
// Array of client instances
{ "urgent": 0, "normal": 3, "bulk": 12, "total": 15 }
Endpoints — Messages
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"
}
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.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"
}
{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"documentUrl": "https://cdn.example.com/invoice.pdf",
"fileName": "invoice-001.pdf",
"caption": "Your invoice attached",
"lane": "normal"
}
{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"audioUrl": "https://cdn.example.com/audio.ogg",
"lane": "normal"
}
caption is optional.{
"instanceName": "acme__soporte__main",
"to": "5491112345678",
"videoUrl": "https://cdn.example.com/video.mp4",
"caption": "Watch this",
"lane": "normal"
}
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.{
"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 | Description |
|---|---|
| pending | In queue, waiting to be sent |
| sent | Successfully sent |
| failed | Error — 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"
}
textisnullfor media messages.- Only messages with
fromMe: falseare 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.
{
"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
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!"
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
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)
Responsible use
WhatsApp can ban numbers it detects as spam or that violate its terms. The gateway applies delays and rate limits automatically, but you are ultimately responsible for how the service is used.
Mandatory rules
| Rule | Detail |
|---|---|
| One number = one active instance | Don't connect the same number on multiple platforms simultaneously. WhatsApp detects duplicate sessions and bans the number. |
| Opt-in contacts only | Only message contacts who wrote to you first or gave explicit consent. No purchased lists or cold outreach. |
| Use the right lane | urgent for OTP/alerts, normal for conversations, bulk for mass campaigns. |
| Gradual ramp-up | New numbers need history. Start low volume for 2-4 weeks before scaling. Never launch mass campaigns from day one. |
| Allowed content | No spam, phishing, impersonation, or content that violates WhatsApp's Terms of Service. |
Warning signs
- WhatsApp shows "This account is not allowed to use WhatsApp"
- Messages show a single checkmark and are never delivered
- Contacts report they can't message you
If you see any of these signs, pause sending immediately and contact soporte@sinapsia.com.ar.
Responsibilities
| Responsibility | Who |
|---|---|
| Technical delays and rate limits | Gateway (automatic) |
| What messages are sent and to whom | You (the client) |
| Number status (banned or not) | |
| Technical support | soporte@sinapsia.com.ar |
SinapsIA is not responsible for bans resulting from misuse of the service.