Guides & Resources

Everything you need to integrate Hasos into your system.

Quickstart

From zero to your first call in four steps.

  1. 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.

    bash
    curl https://api.hasos.com.br/v1/instances \
      -H "x-api-key: hs_live_your_key"
    Create key
  2. 2

    Connect a WhatsApp number

    Create a device and link WhatsApp via QR code or pairing code. Each device is one number.

    bash
    # 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"
    Add device
  3. 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.

    bash
    # 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)
    Add device
  4. 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.

    bash
    curl -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>
    Open a device
  5. 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).

    bash
    curl -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"}'
    Set up webhook

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.

Verify the signature (Node.js)
js
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),
  );
}
Payload format
json
// 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 declined
  • busyBusy
  • timeoutRang and nobody answered — or was ended on timeout by the platform
  • cancelledThe caller canceled before it was answered
  • failedTechnical failure (connection or audio)
  • do_not_disturbThe contact is in do-not-disturb mode
  • after_hoursReceived outside the configured business hours — declined automatically
  • unknownReason 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.

bash
# 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 status
  • create_deviceCreate a device to connect a WhatsApp number
  • get_device_qr / get_pairing_codeQR code or pairing code to link
  • restart_device / delete_deviceRestart or remove a device
  • list_live_callsCalls in progress right now
  • list_callsCall history (filter by device and limit)
  • get_call_recording / get_call_transcriptRecording and AI transcript + summary
  • get_analyticsAnswer rate, volume, average duration
  • get_webhook / set_webhook / test_webhookConfigure and test the account webhook
  • list_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.

bash
# 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.

js
// 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.

html
<!-- 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