Guides & Resources
Everything you need to integrate Hasos into your system.
Quickstart
From zero to your first call in four steps.
- 1
Create your API key
Generate a key under Settings → API keys and send it in the x-api-key header. Always call from your server — never from the browser.
Create keybashcurl https://api.hasos.com.br/v1/instances \ -H "x-api-key: hs_live_your_key"
- 2
Connect a WhatsApp number
Create a device and link WhatsApp via QR code or pairing code. Each device is one number.
Add devicebash# create the device curl -X POST https://api.hasos.com.br/v1/instances \ -H "x-api-key: hs_live_your_key" \ -H "Content-Type: application/json" \ -d '{"phone":"5511999999999"}' # get the pairing code for that device curl https://api.hasos.com.br/v1/instances/INSTANCE_ID/pairing-code \ -H "x-api-key: hs_live_your_key" - 3
Already have a session? Migrate without QR (Baileys, Evolution, WAHA-NOWEB)
If the number is already active on a Baileys-based stack, import the session and connect without scanning the QR again — your end customer never sees a pairing step. Requires the Baileys creds.json. Important: WhatsApp allows 1 active connection per number, so stop using the number on the old stack when you import (it's a handoff). WAHA: works with the NOWEB engine (= Baileys); the WEBJS (browser) engine is not supported.
Add devicebash# 1) create the device (do NOT pair via QR) curl -X POST https://api.hasos.com.br/v1/instances \ -H "x-api-key: hs_live_your_key" \ -H "Content-Type: application/json" \ -d '{"phone":"5511999999999"}' # 2) import the Baileys session (the contents of creds.json) — connects without QR curl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/import-session \ -H "x-api-key: hs_live_your_key" \ -H "Content-Type: application/json" \ -d '{"format":"baileys","creds": { /* ...contents of your creds.json... */ }}' # response: { "ok": true, "status": "CONNECTED" | "CONNECTING" } # confirm with GET /v1/instances/INSTANCE_ID (status CONNECTED) - 4
Embed the dialer in your app
Issue a device token and paste the dialer iframe into your product. The token authenticates that number in the browser — make and receive calls with no voice backend.
Open a devicebashcurl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/tokens \ -H "x-api-key: hs_live_your_key"
html<iframe src="https://phone.hasos.com.br?token=YOUR_TOKEN" allow="microphone" style="width:420px;height:640px;border:0;border-radius:8px" ></iframe>
- 5
Receive events via webhook
Point a URL to receive call events. Every delivery is signed — validate the x-webhook-signature header with your secret (see below).
Set up webhookbashcurl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/webhook \ -H "x-api-key: hs_live_your_key" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-app.com/webhooks/voice"}'
Quick links
Main endpoints
Base URL: https://api.hasos.com.br · authenticated via the x-api-key header.
- POST
/v1/instancesCreate a device (number) - GET
/v1/instancesList the account's devices - GET
/v1/instances/:id/pairing-codeWhatsApp pairing code - GET
/v1/instances/:id/qrQR code to link WhatsApp - POST
/v1/instances/:id/import-sessionImport an existing Baileys session (Evolution, WAHA-NOWEB) and connect without QR - POST
/v1/instances/:id/tokensIssue a dialer token - POST
/v1/instances/:id/webhookSet the webhook URL (returns the secret) - GET
/v1/instances/:id/webhook/deliveriesRecent webhook deliveries - GET
/v1/instances/:id/sipThe number's SIP credentials (host/username/password) - POST
/v1/instances/:id/sip/enableTurn SIP on/off for the number (Pro plan) - POST
/v1/instances/:id/sip/regenerateGenerate a new SIP password (keeps the username) - GET
/v1/callsThe account's call history - GET
/v1/calls/:id/recordingSigned link to the call recording - GET
/v1/recordings/:id/transcriptAI transcript + summary of the recording (Pro plan) - GET
/v1/analyticsAggregate metrics (answer rate, volume…) - GET
/v1/api-keysList keys (masked)
Webhook validation
Every delivery arrives with the header x-webhook-signature = sha256=<HMAC>. Compute the HMAC SHA-256 of the raw body with the device secret and compare.
import crypto from "node:crypto";
// rawBody = RAW request body (before JSON.parse)
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected),
);
}// Envelope shared by all events:
// { "event": "CALL" | "RECORD" | "TRANSCRIPT" | "DEVICE", "instanceId", "phone", "timestamp", "data" }
// "phone" = the instance's WhatsApp number (null until paired).
// CALL event — sent on ringing, answered and ended (inbound and outbound):
{
"event": "CALL",
"instanceId": "a1b2c3d4",
"phone": "5511988887777",
"timestamp": "2026-07-01T13:14:41.077Z",
"data": {
"type": "answered", // ringing | answered | ended
"callId": "00ED0D44…",
"direction": "OUTGOING", // OUTGOING | INCOMING
"peerPhone": "5511999999999",
"durationSec": 23, // set on "ended"
"endReason": "user_ended" // set on "ended" — see the table below
}
}
// RECORD event — sent when the recording is ready, with a download link:
{
"event": "RECORD",
"instanceId": "a1b2c3d4",
"phone": "5511988887777",
"timestamp": "2026-07-01T13:15:10.000Z",
"data": {
"callId": "00ED0D44…",
"direction": "OUTGOING",
"peerPhone": "5511999999999",
"url": "https://s3.hasos.com.br/recordings/…?X-Amz-Signature=…", // valid for 7 days
"objectKey": "recordings/00ED0D44….mp3",
"contentType": "audio/mpeg",
"durationSec": 23,
"sizeBytes": 123456
}
}
// TRANSCRIPT event — sent when the AI transcript is ready (AI Pack):
{
"event": "TRANSCRIPT",
"instanceId": "a1b2c3d4",
"phone": "5511988887777",
"timestamp": "2026-07-01T13:15:40.000Z",
"data": {
"callId": "00ED0D44…",
"transcript": "Full transcribed text of the call…",
"summary": "Summary + next steps.",
"recordingUrl": "https://s3.hasos.com.br/recordings/…?X-Amz-Signature=…" // valid for 7 days
}
}endReason values
Sent in the CALL event with type: "ended". It tells you how the call ended — to know whether it was answered, use durationSec (greater than 0 = answered). When the other side hangs up, we pass through the reason sent by WhatsApp itself, so treat any value not on this list as unknown.
user_endedHung up normally by either side (default when the other side ends the call)declinedThe call was declinedbusyBusytimeoutRang and nobody answered — or was ended on timeout by the platformcancelledThe caller canceled before it was answeredfailedTechnical failure (connection or audio)do_not_disturbThe contact is in do-not-disturb modeafter_hoursReceived outside the configured business hours — declined automaticallyunknownReason not identified
MCP server (AI agents)
Use Hasos straight from your AI agent — Claude, Cursor or any client that supports MCP. It's a remote server: nothing to install, just point to https://api.hasos.com.br/mcp with your API key (x-api-key or Authorization: Bearer). The agent has the same permissions and limits as the key — use a dedicated key and revoke it anytime under API keys.
# Claude Code (terminal)
claude mcp add --transport http hasos https://api.hasos.com.br/mcp \
--header "x-api-key: hs_live_your_key"
# Cursor (~/.cursor/mcp.json) — and other clients with remote (HTTP) MCP
{
"mcpServers": {
"hasos": {
"url": "https://api.hasos.com.br/mcp",
"headers": { "x-api-key": "hs_live_your_key" }
}
}
}
# Claude Desktop (claude_desktop_config.json) — via the mcp-remote bridge
{
"mcpServers": {
"hasos": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.hasos.com.br/mcp",
"--header", "Authorization: Bearer hs_live_your_key"]
}
}
}list_devices / get_deviceConnected numbers and statuscreate_deviceCreate a device to connect a WhatsApp numberget_device_qr / get_pairing_codeQR code or pairing code to linkrestart_device / delete_deviceRestart or remove a devicelist_live_callsCalls in progress right nowlist_callsCall history (filter by device and limit)get_call_recording / get_call_transcriptRecording and AI transcript + summaryget_analyticsAnswer rate, volume, average durationget_webhook / set_webhook / test_webhookConfigure and test the account webhooklist_leads / add_leads / set_lead_statusPipeline leads and stage changes
Example requests to the agent: "create a device and show me the QR so I can connect my WhatsApp" or "how many calls were answered this week and what's the summary of the last one?"
SIP access (softphone/PBX)
Enable SIP for the number via the API and register a softphone (Zoiper, Linphone) or PBX (Asterisk, 3CX) with the returned host, username and password— no new number. It's a Pro plan feature: turning SIP on without a paid plan returns 403 { code: "plan_required" }. Each number handles one call at a time (a WhatsApp limit), and recording and webhooks keep working the same way.
# 1) Turn SIP on for the number (requires the Pro plan).
# Without a paid plan it returns 403 { "code": "plan_required" }.
curl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/sip/enable \
-H "x-api-key: hs_live_your_key" \
-H "Content-Type: application/json" \
-d '{"enabled":true}'
# Response (200):
# {
# "host": "sip.hasos.com.br",
# "username": "hsINSTANCE_ID",
# "password": "…",
# "enabled": true,
# "planAllowed": true
# }
# 2) Fetch the credentials later (host/username/password):
curl https://api.hasos.com.br/v1/instances/INSTANCE_ID/sip \
-H "x-api-key: hs_live_your_key"
# 3) Generate a new password (disconnects softphones using the old one):
curl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/sip/regenerate \
-H "x-api-key: hs_live_your_key"
# 4) Turn SIP off (the number goes back to using the platform dialer):
curl -X POST https://api.hasos.com.br/v1/instances/INSTANCE_ID/sip/enable \
-H "x-api-key: hs_live_your_key" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'Connect a number via the API (pairing)
The WhatsApp QR rotates every ~20s. Fetch GET /v1/instances/{id}/qr in a loop (every 2–3s) and re-render the QR on every response — a static QR expires before the user scans it. The response also includes status (stop when it's CONNECTED) and error (the reason, when WhatsApp refuses). The endpoint self-recovers, so never delete and recreate the number to get a new QR— that triggers WhatsApp's temporary linking block (and increases the risk of a ban). For an even simpler flow, use the pairing-code (8 characters), with no camera or rotating QR.
// 1) Create the number (once).
const { id } = await fetch("https://api.hasos.com.br/v1/instances", {
method: "POST", headers: { "x-api-key": KEY, "Content-Type": "application/json" }, body: "{}",
}).then((r) => r.json());
// 2) Pair: poll /qr every 2–3s and RE-RENDER the QR on every response.
// WhatsApp rotates the code every ~20s — a static QR expires before it's scanned.
const poll = setInterval(async () => {
const { qr, status, error } = await fetch(`https://api.hasos.com.br/v1/instances/${id}/qr`, {
headers: { "x-api-key": KEY },
}).then((r) => r.json());
if (status === "CONNECTED") { clearInterval(poll); showConnected(); return; }
if (error) { showWarning(error); /* do NOT delete/recreate — just keep polling */ }
if (qr) renderQR(qr); // render the STRING as a QR image, always the latest one
}, 2500);
// Simpler alternative (no camera / no rotating QR): an 8-character code.
// The user enters it under Linked devices › Link a device › "Link with phone number instead".
// GET https://api.hasos.com.br/v1/instances/{id}/pairing-code → { "code": "ABCD-1234" }Headless dialer (your UI, no visible iframe)
Want your own call buttons without showing our dialer? Include embed.js: it injects an invisible iframe (present in the DOM for WebRTC audio, but off-screen) and you drive everything from JavaScript — place, answer, decline and hang up — from your own interface. Every method returns a Promise, and the call:started, call:accepted, call:ended and offer:received events notify your UI. The device token (the same one the dialer uses) is the authentication.
<!-- Headless dialer: your UI, invisible iframe. Our dialer is never shown. -->
<script src="https://phone.hasos.com.br/embed.js"></script>
<script>
const phone = HasosPhone.create({
token: "YOUR_DEVICE_TOKEN", // POST /v1/instances/:id/tokens
allowedOrigin: "https://your-app.com", // pins the channel (recommended)
});
await phone.ready();
// Place calls from YOUR buttons:
document.querySelector("#call").onclick = () => phone.call.start("5511999998888");
document.querySelector("#hangup").onclick = () => phone.call.hangup();
// Inbound: answer/decline from YOUR interface:
phone.on("offer:received", (offer) => showIncomingCallPopup(offer));
document.querySelector("#answer").onclick = () => phone.call.accept();
document.querySelector("#decline").onclick = () => phone.call.reject();
// Lifecycle events to update your UI:
phone.on("call:started", (c) => {/* dialing */});
phone.on("call:accepted", (c) => {/* in call */});
phone.on("call:ended", (c) => {/* ended */});
</script>Docs & updates
How to use the dialer
Make calls from the browser — in the dashboard or embedded in your product via iframe.
API reference
Endpoints, parameters and usage examples.
Webhook validation
How to verify the x-webhook-signature header.
MCP server (AI agents)
Connect Claude, Cursor or your own agent to your account: devices, QR, calls, recordings and leads.
SIP access
Enable SIP via the API (Pro plan) and connect a softphone or PBX to your number.
Headless dialer
Your own calling UI, no visible iframe — via embed.js and postMessage.
Avoiding bans
Best practices to reduce the risk of WhatsApp restricting your number.
AI agent (ElevenLabs)
Connect your ElevenLabs agent (BYOK) and let AI answer incoming calls.
Changelog
Latest updates and new features.
Status page
Real-time service status.
