Referência da API
A FluxoAPI envia e recebe mensagens pelo WhatsApp Cloud API oficial da Meta. Seu número continua funcionando no aplicativo do celular — o que você faz por aqui aparece lá, e o que o dono responde por lá aparece aqui.
https://api.fluxointeligenteia.com/v1
Chave + assinatura HMAC-SHA256 por requisição.
JSON em tudo. UTF-8. Datas em ISO 8601 com fuso.
Chaves fxk_test_ não enviam mensagem de verdade.
Autenticação
Cada requisição carrega quatro cabeçalhos. A chave identifica quem chama. O segredo nunca é enviado — ele assina. O carimbo de tempo e o nonce impedem que uma requisição capturada seja reenviada depois.
fxk_live_ ou fxk_test_. Pode aparecer em log.401 — sincronize por NTP.409.v1=. Veja a string assinada ao lado.A string assinada
Concatene exatamente nesta ordem, separando por quebra de linha. O corpo entra como hash, não inteiro — assim a assinatura funciona com streaming.
"v1" // versão do esquema timestamp // 1756220400 nonce // 01JB8XQ2M5K7VN9F method // POST path // /v1/instances/…/messages sha256(body) // hex do corpo bruto
import crypto from 'node:crypto'; const ts = Math.floor(Date.now() / 1000); const nonce = crypto.randomUUID(); const body = JSON.stringify(payload); const signing = [ 'v1', ts, nonce, 'POST', path, crypto.createHash('sha256').update(body).digest('hex') ].join('\n'); const sig = crypto .createHmac('sha256', process.env.FLUXO_SECRET) .update(signing) .digest('hex'); await fetch(base + path, { method: 'POST', headers: { 'X-Fluxo-Key': process.env.FLUXO_KEY, 'X-Fluxo-Timestamp': String(ts), 'X-Fluxo-Nonce': nonce, 'X-Fluxo-Signature': `v1=${sig}`, 'Content-Type': 'application/json' }, body });
import hmac, hashlib, time, uuid, json, requests ts = int(time.time()) nonce = uuid.uuid4().hex body = json.dumps(payload, separators=(',', ':')) signing = "\n".join([ "v1", str(ts), nonce, "POST", path, hashlib.sha256(body.encode()).hexdigest() ]) sig = hmac.new( secret.encode(), signing.encode(), hashlib.sha256 ).hexdigest() requests.post(base + path, data=body, headers={ "X-Fluxo-Key": key, "X-Fluxo-Timestamp": str(ts), "X-Fluxo-Nonce": nonce, "X-Fluxo-Signature": f"v1={sig}", "Content-Type": "application/json", })
$ts = time(); $nonce = bin2hex(random_bytes(16)); $body = json_encode($payload); $signing = implode("\n", [ 'v1', $ts, $nonce, 'POST', $path, hash('sha256', $body) ]); $sig = hash_hmac('sha256', $signing, $secret); // headers: X-Fluxo-Key, -Timestamp, -Nonce, // X-Fluxo-Signature: "v1={$sig}"
Listar instâncias
Todos os números conectados à sua conta, com o estado de cada um. Use no início do seu app para descobrir por qual instância enviar.
connected, pending, disconnected, suspended.coexistence ou api_only. Instâncias em coexistência também recebem mensagens enviadas pelo celular do dono.{
"data": [
{
"id": "inst_01JB8XR4KP2M9TVQ",
"name": "Central de Reservas",
"phone": "+5519996155951",
"status": "connected",
"mode": "coexistence",
"quality": "high",
"throughput_mps": 80,
"messaging_limit_24h": 10000,
"access_expires_at": "2026-10-12T00:00:00-03:00"
}
]
}
Detalhar instância
Estado completo de um número, incluindo consumo das últimas 24 horas e quando o acesso vence. Consulte no máximo uma vez por minuto — o resto vem por notificação, que é mais rápido e não gasta seu limite.
instance.status_changed e instance.access_expiring.{
"id": "inst_01JB8XR4KP2M9TVQ",
"name": "Central de Reservas",
"phone": "+5519996155951",
"verified_name": "Kallytur Turismo",
"status": "connected",
"mode": "coexistence",
"quality": "high",
"usage_24h": {
"conversations": 1284,
"limit": 10000,
"peak_mps": 62
},
"access_expires_at": "2026-10-12T00:00:00-03:00",
"webhook_url": "https://kallytur.com.br/hooks/fluxo"
}
Enviar mensagem
Envia texto, mídia ou localização para um número. Só funciona dentro da janela de 24 horas — se o contato não escreveu nos últimos um dia, use um modelo aprovado.
+ e código do país. Ex.: +5519991802264.text, image, document, audio, video ou location.type é text.wamid da mensagem que está sendo respondida. Aparece como citação no WhatsApp.curl -X POST "$BASE/v1/instances/$INST/messages" \ -H "X-Fluxo-Key: $KEY" \ -H "X-Fluxo-Timestamp: $TS" \ -H "X-Fluxo-Nonce: $NONCE" \ -H "X-Fluxo-Signature: v1=$SIG" \ -H "Content-Type: application/json" \ -d '{ "to": "+5519991802264", "type": "text", "text": { "body": "Sua reserva está confirmada!" }, "idempotency_key": "reserva-8842-confirmacao" }'
const res = await fluxo.messages.send({ instance: 'inst_01JB8XR4KP2M9TVQ', to: '+5519991802264', type: 'text', text: { body: 'Sua reserva está confirmada!' }, idempotencyKey: 'reserva-8842-confirmacao' }); console.log(res.id); // msg_01JB8Y7QK2M4NPXV console.log(res.status); // queued
res = fluxo.messages.send(
instance="inst_01JB8XR4KP2M9TVQ",
to="+5519991802264",
type="text",
text={"body": "Sua reserva está confirmada!"},
idempotency_key="reserva-8842-confirmacao",
)
print(res.id) # msg_01JB8Y7QK2M4NPXV
print(res.status) # queued
{
"id": "msg_01JB8Y7QK2M4NPXV",
"status": "queued",
"wamid": "wamid.HBgNNTUxOTk5MTgwMjI2NBUCABEYEjc…",
"conversation_id": "conv_01JB8Y2K7PMD4XRA",
"billable": false,
"created_at": "2026-08-26T14:29:03-03:00"
}
{
"error": {
"code": "window_closed",
"message": "O contato não escreve há mais de 24h.",
"last_inbound_at": "2026-08-24T09:12:44-03:00",
"hint": "Use POST …/messages/template"
}
}
// Retry-After: 3 { "error": { "code": "rate_limited", "message": "Vazão do número no limite.", "scope": "phone_number", "limit": 80, "retry_after": 3 } }
Enviar modelo
Único jeito de iniciar conversa com quem não escreveu nas últimas 24 horas. O modelo precisa estar aprovado pela Meta — aprovação leva de minutos a algumas horas.
pt_BR.billable na resposta.{
"to": "+5519991802264",
"template": {
"name": "lembrete_embarque",
"language": "pt_BR",
"variables": {
"1": "Marina",
"2": "5 de setembro",
"3": "06h40"
}
},
"idempotency_key": "lembrete-8842-d1"
}
{
"id": "msg_01JB8YB3TC7K9WQD",
"status": "queued",
"billable": true,
"category": "utility",
"estimated_cost_brl": 0.0410
}
Listar mensagens
Mensagens de todas as conversas, das mais recentes para as mais antigas. Útil para auditoria e para reconciliar o que sua base perdeu.
inbound ou outbound.queued, sent, delivered, read, failed. Filtre por failed para achar o que não entregou.
O campo sent_from_phone distingue o que saiu pela API do que o dono
digitou no celular. Sua automação precisa dele para não responder por cima de uma resposta humana.
{
"data": [
{
"id": "msg_01JB8Y7QK2M4NPXV",
"conversation_id": "conv_01JB8Y2K7PMD4XRA",
"direction": "outbound",
"type": "text",
"text": "Por pessoa, Marina. No casal fica 6.960…",
"status": "read",
"sent_from_phone": true,
"wamid": "wamid.HBgNNTUxOTk5MTgwMjI2NBUCABEYEjc…",
"at": "2026-08-26T14:29:03-03:00"
}
],
"next_cursor": null
}
Listar conversas
Devolve conversas ordenadas pela última mensagem. Paginação por cursor — nunca por offset, que pula ou repete registros quando chegam mensagens durante a leitura.
open, pending, closed. Aceita múltiplos separados por vírgula.next_cursor da página anterior.GET /v1/conversations
?instance_id=inst_01JB8XR4KP2M9TVQ
&status=open,pending
&limit=25
{
"data": [
{
"id": "conv_01JB8Y2K7PMD4XRA",
"instance_id": "inst_01JB8XR4KP2M9TVQ",
"contact": {
"phone": "+5519991802264",
"name": "Marina Rocha"
},
"status": "open",
"unread": 3,
"window_expires_at": "2026-08-27T14:32:11-03:00",
"last_message": {
"text": "Perfeito! Pode reservar pra mim então",
"direction": "inbound",
"at": "2026-08-26T14:32:11-03:00"
},
"lead_id": "lead_01JB8Y3PQR6C8VNM"
}
],
"next_cursor": "eyJvIjoxNzU2MjIwNDAwfQ"
}
Histórico da conversa
Mensagens de uma conversa em ordem cronológica, com o histórico que já existia no celular antes de você conectar — a coexistência importa o passado, não só o futuro.
asc (padrão) ou desc.{
"conversation_id": "conv_01JB8Y2K7PMD4XRA",
"window_expires_at": "2026-08-27T14:32:11-03:00",
"data": [
{
"id": "msg_01JB8Y5A1BC2DEFG",
"direction": "inbound",
"text": "Oi! Vi o pacote de Bonito no Instagram…",
"at": "2026-08-26T14:02:07-03:00"
},
{
"id": "msg_01JB8Y6B2CD3EFGH",
"direction": "outbound",
"text": "Oi Marina, tudo bem? Temos sim…",
"status": "read",
"sent_from_phone": false,
"at": "2026-08-26T14:02:44-03:00"
}
],
"next_cursor": null
}
Listar oportunidades
Oportunidades do funil com etapa, valor e a conversa de origem. Cada lead aponta para a conversa que o gerou — é assim que seu CRM mostra o histórico junto do negócio.
ai, human ou unassigned — de quem é a bola agora.{
"data": [
{
"id": "lead_01JB8Y3PQR6C8VNM",
"contact": { "name": "Marina Rocha",
"phone": "+5519991802264" },
"stage": "proposta",
"value_brl": 6960.00,
"probability": 0.85,
"owner": "human",
"conversation_id": "conv_01JB8Y2K7PMD4XRA",
"source": "instagram",
"hours_in_stage": 3.4,
"updated_at": "2026-08-26T14:29:03-03:00"
}
],
"summary": {
"count": 47,
"open_value_brl": 214380.00,
"weighted_forecast_brl": 87640.00
}
}
Mover de etapa
Avança ou recua uma oportunidade. Dispara lead.stage_changed e grava quem moveu no histórico.
perdido. Alimenta o relatório de motivos de perda.409 se não bater.{
"stage": "fechamento",
"expected_stage": "proposta",
"value_brl": 6960.00
}
{
"id": "lead_01JB8Y3PQR6C8VNM",
"stage": "fechamento",
"previous_stage": "proposta",
"moved_by": "api",
"moved_at": "2026-08-26T14:41:22-03:00"
}
{
"error": {
"code": "stage_mismatch",
"message": "A oportunidade já saiu de 'proposta'.",
"current_stage": "ganho"
}
}
Receber notificações
Quando chega mensagem, muda status ou um lead avança, chamamos a URL que você cadastrou no painel.
Verifique a assinatura antes de processar e responda 200 rápido — deixe o
trabalho pesado para depois da resposta.
Verificar a assinatura
Mesmo esquema da API, na direção contrária: assinamos com o segredo whsec_ da sua instância.
Reenvio e duplicatas
Se o seu endpoint não responder 200, tentamos de novo em
0 s, 30 s, 2 min, 10 min, 1 h, 6 h e 24 h. Depois disso o evento vai para a fila de falhas,
visível no painel, com reenvio manual.
X-Fluxo-Event-Id e ignore os repetidos — grave o id
antes de executar o efeito, na mesma transação. É a única forma de não processar duas vezes.
Endpoint que falha 100% das entregas por 24 horas é desativado automaticamente e você é avisado por e-mail. Um endpoint morto consumindo tentativas atrasa as notificações que funcionam.
POST /hooks/fluxo X-Fluxo-Event-Id: evt_01JB8Y8M3RK5TQNC X-Fluxo-Timestamp: 1756220400 X-Fluxo-Signature: v1=a71e…,v1=3f9c… { "id": "evt_01JB8Y8M3RK5TQNC", "type": "message.received", "instance_id": "inst_01JB8XR4KP2M9TVQ", "created_at": "2026-08-26T14:32:11-03:00", "data": { "conversation_id": "conv_01JB8Y2K7PMD4XRA", "message_id": "msg_01JB8Y9F2QK7MRTX", "from": "+5519991802264", "type": "text", "text": "Perfeito! Pode reservar pra mim então", "sent_from_phone": false } }
app.post('/hooks/fluxo', express.raw({ type: 'application/json' }), // corpo BRUTO async (req, res) => { const ts = req.get('X-Fluxo-Timestamp'); const raw = req.body; // 1. carimbo dentro de 5 minutos if (Math.abs(Date.now()/1000 - ts) > 300) return res.sendStatus(401); // 2. alguma das assinaturas bate? const mine = crypto .createHmac('sha256', whsec) .update(ts + '\n' + raw) .digest(); const ok = req.get('X-Fluxo-Signature') .split(',') .some(p => crypto.timingSafeEqual( mine, Buffer.from(p.slice(3), 'hex'))); if (!ok) return res.sendStatus(401); // 3. dedup ANTES do efeito, na mesma transação const ev = JSON.parse(raw); const novo = await db.insertEventIfNew(ev.id); res.sendStatus(200); // responda já if (novo) queue.push(ev); // trabalhe depois });
@app.post("/hooks/fluxo") async def hook(request: Request): raw = await request.body() # BRUTO ts = request.headers["X-Fluxo-Timestamp"] if abs(time.time() - int(ts)) > 300: raise HTTPException(401) mine = hmac.new( whsec.encode(), (ts + "\n").encode() + raw, hashlib.sha256 ).hexdigest() sigs = request.headers["X-Fluxo-Signature"].split(",") if not any(hmac.compare_digest(mine, s[3:]) for s in sigs): raise HTTPException(401) ev = json.loads(raw) novo = await db.insert_event_if_new(ev["id"]) if novo: background.add_task(processar, ev) return Response(status_code=200)
Tipos de evento
Escolha quais você quer receber no painel, por instância.
| Evento | Quando dispara |
|---|---|
| message.received | alguém escreveu para o seu número |
| message.status | enviada, entregue, lida ou falhou |
| message.sent_from_phone | o dono respondeu pelo app do celular — coexistência |
| conversation.window_closing | faltam 2 horas para a janela de 24h fechar |
| lead.stage_changed | oportunidade mudou de etapa |
| instance.status_changed | número caiu, voltou ou mudou de qualidade |
| instance.access_expiring | 7 dias antes de o acesso vencer |
message.sent_from_phone só existe em instâncias com coexistência. É como seu sistema fica sabendo que o dono já respondeu pelo celular — sem ele, sua automação responde por cima e o cliente recebe duas respostas.
{
"type": "message.status",
"data": {
"message_id": "msg_01JB8Y7QK2M4NPXV",
"status": "read",
"rank": 3,
"at": "2026-08-26T14:30:02-03:00"
}
}
sent — a Meta aceitoudelivered — chegou no aparelhoread — o contato abriufailed — não entregourank e só aplique se for
maior que o atual — assim um delivered atrasado não desfaz um read.
Limites e cobrança
Dois limites diferentes, que se confundem com facilidade: vazão é quão rápido, volume é quantas pessoas.
| Limite | Valor | Escopo |
|---|---|---|
| Vazão | 80 msg/s (até 1.000) | por número |
| Volume 24h | 250 → 1k → 10k → 100k → ilimitado | por número |
| Requisições | conforme seu plano | por chave |
| Corpo | 1 MB | por requisição |
| Mídia | 16 MB (vídeo 100 MB) | por arquivo |
A Meta cobra por conversa de 24 horas, não por mensagem. Quem inicia
define a categoria e o preço: conversa iniciada pelo cliente é mais barata que conversa iniciada por modelo.
O campo billable na resposta diz se aquele envio abriu uma conversa cobrada.
429, respeite o Retry-After e aplique
recuo exponencial com jitter. Repetir imediatamente derruba a sua vazão para todos os números da conta.X-RateLimit-Limit: 80 X-RateLimit-Remaining: 18 X-RateLimit-Reset: 1756220403 X-Fluxo-Window-Left: 85847 // seg. da janela 24h
const wait = Math.min( 30_000, 2 ** tentativa * 500 + Math.random() * 300 );
Erros
Todo erro devolve o mesmo formato, com code estável para você tratar em código e message em português para mostrar a quem opera.
| Código | HTTP | O que fazer |
|---|---|---|
| invalid_signature | 401 | confira a string assinada e o relógio do servidor |
| stale_timestamp | 401 | sincronize por NTP — passou dos 300 s |
| nonce_reused | 409 | gere um nonce novo a cada requisição |
| window_closed | 422 | use um modelo aprovado |
| template_not_approved | 422 | aguarde a aprovação da Meta |
| rate_limited | 429 | respeite Retry-After, com recuo |
| instance_reconnecting | 503 | tente de novo — nada foi perdido |
| instance_suspended | 403 | número bloqueado pela Meta; veja o painel |
Toda resposta traz X-Request-Id. Guarde no seu log — é por ele que o suporte
encontra a requisição exata.
{
"error": {
"code": "invalid_signature",
"message": "A assinatura não confere.",
"doc": "https://docs.fluxointeligenteia.com/#auth",
"request_id": "req_01JB8YC5KD3M7NPQ"
}
}
SDKs
Os SDKs cuidam da assinatura, do recuo em 429 e da paginação por cursor. Recomendados — a assinatura é onde mais se erra.
npm i @fluxoapi/node
pip install fluxoapi
composer require fluxoapi/php
Conecte um agente de IA direto às suas conversas.
import { Fluxo } from '@fluxoapi/node'; const fluxo = new Fluxo({ key: process.env.FLUXO_KEY, secret: process.env.FLUXO_SECRET }); await fluxo.messages.send({ instance: 'inst_01JB8XR4KP2M9TVQ', to: '+5519991802264', type: 'text', text: { body: 'Oi! Tudo certo com sua reserva.' }, idempotencyKey: crypto.randomUUID() });