FluxoAPI v1
Erros Limites Painel

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.

Base

https://api.fluxointeligenteia.com/v1

Autenticação

Chave + assinatura HMAC-SHA256 por requisição.

Formato

JSON em tudo. UTF-8. Datas em ISO 8601 com fuso.

Ambientes

Chaves fxk_test_ não enviam mensagem de verdade.

Todo endpoint é escopado à instância. A empresa nunca vai no caminho nem no corpo — ela vem da chave que assinou a requisição.

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.

Cabeçalhos
X-Fluxo-Keyobrigatóriostring
Sua chave pública. Começa com fxk_live_ ou fxk_test_. Pode aparecer em log.
X-Fluxo-Timestampobrigatóriointeger
Unix em segundos. Aceito numa janela de ±300 s. Relógio adiantado é a causa nº 1 de 401 — sincronize por NTP.
X-Fluxo-Nonceobrigatóriostring
Valor único por requisição, 16 a 64 caracteres. Repetir um nonce dentro de 10 minutos devolve 409.
X-Fluxo-Signatureobrigatóriostring
HMAC-SHA256 em hexadecimal, prefixado por 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.

Assine o corpo bruto, byte a byte. Se o seu framework fizer parse do JSON e re-serializar antes de assinar, a ordem das chaves muda e a assinatura não bate.
String assinada
"v1"            // versão do esquema
timestamp       // 1756220400
nonce           // 01JB8XQ2M5K7VN9F
method          // POST
path            // /v1/instances/…/messages
sha256(body)    // hex do corpo bruto
Assinar
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

GET /v1/instances assinada

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.

Query
statusenum
connected, pending, disconnected, suspended.
modeenum
coexistence ou api_only. Instâncias em coexistência também recebem mensagens enviadas pelo celular do dono.
Resposta 200
{
  "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

GET /v1/instances/{instance_id} assinada

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.

Não faça polling deste endpoint em laço curto. Assine instance.status_changed e instance.access_expiring.
Resposta 200
{
  "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

POST /v1/instances/{instance_id}/messages assinada

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.

Corpo
toobrigatóriostring
Telefone no formato E.164, com + e código do país. Ex.: +5519991802264.
typeobrigatórioenum
text, image, document, audio, video ou location.
text.bodystring
Até 4096 caracteres. Obrigatório quando type é text.
media.urlstring
URL pública do arquivo. Baixamos e reenviamos pela Meta. Limite de 100 MB para vídeo, 16 MB para o resto.
idempotency_keystring
Envie sempre. Se a conexão cair e você repetir a chamada, a mesma chave devolve a mesma mensagem em vez de enviar — e cobrar — duas vezes. Vale por 7 dias.
reply_tostring
wamid da mensagem que está sendo respondida. Aparece como citação no WhatsApp.
Sem idempotency_key, um timeout de rede seguido de retry envia a mensagem duas vezes. O cliente recebe duplicado e a Meta cobra duas conversas.
Requisição
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
Resposta
{
  "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
  }
}
Status possíveis
202aceita e enfileirada
401assinatura, carimbo ou nonce inválido
409nonce já usado nos últimos 10 min
422janela de 24h fechada
429vazão ou plano no limite
503instância reconectando — nada foi perdido

Enviar modelo

POST /v1/instances/{instance_id}/messages/template assinada

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

Corpo
template.nameobrigatóriostring
Nome exato do modelo aprovado, como aparece no painel.
template.languageobrigatóriostring
Código do idioma. Para português do Brasil, pt_BR.
template.variablesobject
Valores das variáveis por posição. A quantidade tem que bater com o modelo aprovado, senão a Meta rejeita.
Modelo sempre abre uma conversa cobrada, mesmo que o contato não responda. Confira billable na resposta.
Requisição
{
  "to": "+5519991802264",
  "template": {
    "name": "lembrete_embarque",
    "language": "pt_BR",
    "variables": {
      "1": "Marina",
      "2": "5 de setembro",
      "3": "06h40"
    }
  },
  "idempotency_key": "lembrete-8842-d1"
}
Resposta 202
{
  "id": "msg_01JB8YB3TC7K9WQD",
  "status": "queued",
  "billable": true,
  "category": "utility",
  "estimated_cost_brl": 0.0410
}

Listar mensagens

GET /v1/messages assinada

Mensagens de todas as conversas, das mais recentes para as mais antigas. Útil para auditoria e para reconciliar o que sua base perdeu.

Query
conversation_idstring
Restringe a uma conversa.
directionenum
inbound ou outbound.
statusenum
queued, sent, delivered, read, failed. Filtre por failed para achar o que não entregou.
since · untilstring
Intervalo em ISO 8601. Janela máxima de 90 dias por consulta.

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.

Resposta 200
{
  "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

GET /v1/conversations assinada

Devolve conversas ordenadas pela última mensagem. Paginação por cursor — nunca por offset, que pula ou repete registros quando chegam mensagens durante a leitura.

Query
instance_idstring
Filtra por instância. Sem ele, devolve de todas as suas instâncias.
statusenum
open, pending, closed. Aceita múltiplos separados por vírgula.
sincestring
ISO 8601. Use para reconciliar depois de uma queda de conexão em tempo real.
cursorstring
Valor de next_cursor da página anterior.
limitinteger
1 a 100. Padrão 25.
Requisição
GET /v1/conversations
      ?instance_id=inst_01JB8XR4KP2M9TVQ
      &status=open,pending
      &limit=25
Resposta 200
{
  "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

GET /v1/conversations/{conversation_id}/messages assinada

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.

Query
sincestring
Traz só o que chegou depois deste instante. É assim que você reconcilia após uma queda: guarde o horário da última mensagem que processou e peça daí em diante.
orderenum
asc (padrão) ou desc.
limitinteger
1 a 200. Padrão 50.
Ordene sempre pelo campo at, nunca pela ordem de chegada das notificações — elas podem chegar fora de sequência.
Resposta 200
{
  "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

GET /v1/leads assinada

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.

Query
stagestring
Chave da etapa. Aceita várias separadas por vírgula.
ownerenum
ai, human ou unassigned — de quem é a bola agora.
min_value · max_valuenumber
Valor em reais.
stale_hoursinteger
Só oportunidades sem movimento há mais de N horas. Use para achar o que está esfriando.
Resposta 200
{
  "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

POST /v1/leads/{lead_id}/stage assinada

Avança ou recua uma oportunidade. Dispara lead.stage_changed e grava quem moveu no histórico.

Corpo
stageobrigatóriostring
Chave da etapa de destino.
reasonstring
Obrigatório ao mover para perdido. Alimenta o relatório de motivos de perda.
value_brlnumber
Atualiza o valor junto da mudança de etapa.
expected_stagestring
Se informado, a mudança só acontece se a etapa atual for exatamente esta. Evita que duas automações movam a mesma oportunidade ao mesmo tempo — devolve 409 se não bater.
Use expected_stage sempre que a chamada vier de uma automação. Sem ele, duas mensagens do mesmo contato chegando juntas podem avançar o lead duas etapas.
Requisição
{
  "stage": "fechamento",
  "expected_stage": "proposta",
  "value_brl": 6960.00
}
Resposta 200
{
  "id": "lead_01JB8Y3PQR6C8VNM",
  "stage": "fechamento",
  "previous_stage": "proposta",
  "moved_by": "api",
  "moved_at": "2026-08-26T14:41:22-03:00"
}
Conflito 409
{
  "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.

Durante uma troca de segredo mandamos duas assinaturas separadas por vírgula, por 24 horas. Aceite se qualquer uma bater — assim você rotaciona sem downtime.

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.

A mesma notificação pode chegar mais de uma vez. Guarde o 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.

Requisição recebida
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
  }
}
Verificar
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.

EventoQuando dispara
message.receivedalguém escreveu para o seu número
message.statusenviada, entregue, lida ou falhou
message.sent_from_phoneo dono respondeu pelo app do celular — coexistência
conversation.window_closingfaltam 2 horas para a janela de 24h fechar
lead.stage_changedoportunidade mudou de etapa
instance.status_changednúmero caiu, voltou ou mudou de qualidade
instance.access_expiring7 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.

message.status
{
  "type": "message.status",
  "data": {
    "message_id": "msg_01JB8Y7QK2M4NPXV",
    "status": "read",
    "rank": 3,
    "at": "2026-08-26T14:30:02-03:00"
  }
}
Ordem dos status
1sent — a Meta aceitou
2delivered — chegou no aparelho
3read — o contato abriu
9failed — não entregou
Eventos podem chegar fora de ordem. Compare o rank 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.

LimiteValorEscopo
Vazão80 msg/s (até 1.000)por número
Volume 24h250 → 1k → 10k → 100k → ilimitadopor número
Requisiçõesconforme seu planopor chave
Corpo1 MBpor requisição
Mídia16 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.

Ao receber 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.
Cabeçalhos de limite
X-RateLimit-Limit:     80
X-RateLimit-Remaining: 18
X-RateLimit-Reset:     1756220403
X-Fluxo-Window-Left:   85847  // seg. da janela 24h
Recuo recomendado
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ódigoHTTPO que fazer
invalid_signature401confira a string assinada e o relógio do servidor
stale_timestamp401sincronize por NTP — passou dos 300 s
nonce_reused409gere um nonce novo a cada requisição
window_closed422use um modelo aprovado
template_not_approved422aguarde a aprovação da Meta
rate_limited429respeite Retry-After, com recuo
instance_reconnecting503tente de novo — nada foi perdido
instance_suspended403nú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.

Formato do erro
{
  "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.

Node.js

npm i @fluxoapi/node

Python

pip install fluxoapi

PHP

composer require fluxoapi/php

MCP

Conecte um agente de IA direto às suas conversas.

Começar em 30 segundos
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()
});