DevZap · Guias de integração Voltar ao painel

OAuth 2.1: obter uma chave de API com o consentimento do usuário

Este documento é a fonte da verdade do contrato OAuth. Aplicações clientes devem implementar exatamente o que está aqui; mudanças no contrato exigem atualizar este arquivo.

Quando este fluxo é para você: se você está construindo uma aplicação que conecta contas de outras pessoas ao DevZap, o OAuth evita pedir que elas colem uma chave manualmente. Se você só vai integrar o seu próprio sistema ao seu próprio tenant, não precisa disto, pegue uma chave pelo painel, como descrito em Primeiros passos.

O access_token que este fluxo devolve é uma chave ten_ comum: use-a exatamente como descrito nos outros guias.

Os exemplos citam o cliente "Chatbot Abite", que foi a primeira aplicação a usar o fluxo. O contrato vale para qualquer cliente registrado, e o servidor aceita VÁRIOS clientes ao mesmo tempo: além do bloco legado (OAUTH_CLIENT_ID etc., com nome via OAUTH_CLIENT_DISPLAY_NAME, default "Chatbot Abite"), apps adicionais entram em blocos numerados OAUTH_CLIENT_1_ID, OAUTH_CLIENT_1_SECRET_HASH, OAUTH_CLIENT_1_DISPLAY_NAME, OAUTH_CLIENT_1_REDIRECT_URIS e OAUTH_CLIENT_1_ALLOWED_SCOPES (numeração contígua a partir de 1). Cada cliente tem sua própria allowlist de redirect e seu nome no consentimento; a chave emitida se chama "nome (OAuth)" do cliente que a pediu, e cada cliente só consegue revogar as próprias chaves.

Visão geral

Authorization Server OAuth 2.1 (Authorization Code + PKCE/S256) que permite ao app cliente confiável Chatbot Abite obter, com o consentimento do dono do tenant, uma API key ten_xxx escopada para provisionar/gerenciar instâncias de WhatsApp, sem nunca ver a senha do usuário (o login e o consentimento são hospedados no DevZap).

O access_token emitido É uma API key ten_ normal (revogável, escopada). O OAuth só substitui o antigo "colar a key manualmente" no chatbot.

Pré-requisitos:

Endpoints

GET /oauth/authorize

Inicia o fluxo. O usuário faz login (se preciso) e consente. Parâmetros (query):

Param Obrigatório Valor
response_type sim code
client_id sim o client_id registrado
redirect_uri sim match exato com a allowlist (HTTPS)
scope não default instances:provision
state sim opaco, devolvido na volta; o cliente DEVE validar
code_challenge sim base64url(SHA256(code_verifier))
code_challenge_method sim S256 (único aceito)

Comportamento:

O usuário precisa ser admin (org_admin/platform_admin) do workspace escolhido; se administra mais de um, escolhe na tela.

POST /oauth/token

Content-Type: application/x-www-form-urlencoded. Sem sessão/CSRF: autenticado pelo client_secret. Credenciais no corpo ou via HTTP Basic (Authorization: Basic base64(client_id:client_secret)).

Corpo:

Param Valor
grant_type authorization_code
code o code recebido no callback
redirect_uri o MESMO usado no authorize (binding)
client_id (se não usar Basic)
client_secret (se não usar Basic)
code_verifier o verifier do PKCE

Sucesso (200): header Cache-Control: no-store:

{
  "access_token": "ten_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "token_type": "bearer",
  "scope": "instances:provision",
  "tenant_id": "TENANT-XXXX",
  "tenant_name": "Empresa Exemplo",
  "api_key_id": "TKEY-XXXXXXXXXXXX"
}

O access_token É a API key ten_. Use-a no header Authorization: Bearer ten_xxx nas rotas /api/v1/*. É retornada uma única vez: guarde com segurança.

Erros (padrão RFC 6749 §5.2), Cache-Control: no-store:

HTTP error Quando
400 invalid_request faltou code/redirect_uri/code_verifier
401 invalid_client client_id/client_secret inválidos (com WWW-Authenticate: Basic)
400 invalid_grant code inválido/expirado/reusado, binding ou PKCE não conferem
400 unsupported_grant_type grant_type ≠ authorization_code
{ "error": "invalid_grant", "error_description": "..." }

O code é single-use e expira em ≤ 5 minutos. Uma segunda troca do mesmo code falha com invalid_grant.

POST /oauth/revoke (desconectar)

application/x-www-form-urlencoded, autenticado pelo client_secret (corpo ou Basic). Corpo: token=<a API key ten_ a revogar>.

Resposta sempre 200 (neutra; não vaza existência da key) quando o client é válido. client_secret inválido → 401 invalid_client. Após revogar, a key para de autenticar em /api/v1/*.

PKCE (obrigatório, S256)

  1. Gere um code_verifier: 43 a 128 chars do conjunto [A-Za-z0-9-._~] (ex.: 32 bytes aleatórios em base64url).
  2. code_challenge = base64url(SHA256(code_verifier)) (sem padding).
  3. Envie code_challenge + code_challenge_method=S256 no authorize.
  4. Envie o code_verifier no token. O servidor recusa (invalid_grant) se não bater.

Scopes

Cada rota da /api/v1 exige um scope, e a chave sem ele recebe 403 SCOPE_DENIED:

Scope Rotas
instances:read GET /instances, GET /instances/{id}, GET .../qr, GET .../limits
instances:manage POST /instances, DELETE /instances/{id}, POST .../disconnect; e disparar a conexão pelo GET .../qr (sem ele, o /qr só lê: devolve o QR que já houver ou o status, com connectionStarted: false)
webhooks:manage GET, POST e DELETE .../webhook, POST .../webhook/rotate-secret
messages:send POST .../messages/send, POST .../messages/queue/send
groups:read GET .../groups
queue:read GET .../messages/queue/{idempotencyKey}
messages:read GET .../messages, GET .../messages/{messageId}/media, GET /messages/search, GET /messages/conversation/{phone}, GET /messages/contacts
labels:read GET .../labels, GET .../labels/{labelId}/chats, GET .../chats/{chat}/labels
labels:manage PUT e DELETE .../labels/{labelId}/chats/{chat}
schedules:read GET /schedules, GET /schedules/runs, GET /schedules/{id}, GET /schedules/{id}/runs
schedules:manage POST /schedules, PATCH e DELETE /schedules/{id}

Mensagens recebidas (conversations:read). O DevZap só guarda o que um número recebe quando um administrador liga a gravação naquele número, no painel. As rotas de messages:read continuam abertas a quem tem esse scope, mas só devolvem as recebidas guardadas para a chave com conversations:read (a chave * do painel tem). Sem ele, a listagem, a busca, a conversa e os contatos saem só com o que a conta enviou, e pedir direction=in dá 403 SCOPE_DENIED. Nenhum outro scope traz conversations:read junto.

Os scopes de leitura vêm junto com o scope que gerencia o mesmo recurso: labels:manage dá labels:read, schedules:manage dá schedules:read, e messages:send dá groups:read e queue:read. Por isso a key emitida pelo OAuth alcança as leituras sem que elas apareçam na lista dela. Um scope de leitura sozinho existe para chaves só de consulta, como a de um assistente de IA.

GET /status só exige uma chave válida. A key emitida por OAuth vale apenas na /api/v1: nas rotas do painel e de cobrança ela recebe 403 SCOPE_DENIED.

Segurança (resumo para o integrador)

Registro do client (lado DevZap)

npm run gen:oauth-client -- https://chatbot.seu-dominio.com/oauth/callback

Desenvolvimento local (sem HTTPS): em dev o redirect_uri pode ser de loopback HTTP, ex.: npm run gen:oauth-client -- http://localhost:5173/admin/integrations/devzap/oauth/callback (ou http://127.0.0.1:5173/...). Fora de loopback o HTTPS continua obrigatório e, em produção, OAUTH_REDIRECT_URIS é validado como HTTPS no boot.

O comando imprime as linhas para o .env do DevZap (com o hash bcrypt do secret) e, uma única vez, o client_secret em claro, que vai na configuração do Chatbot Abite. Variáveis:

OAUTH_ENABLED=true
OAUTH_CLIENT_ID=abite_xxxxxxxxxxxxxxxx
OAUTH_CLIENT_SECRET_HASH=$2b$10$...
OAUTH_REDIRECT_URIS=https://chatbot.seu-dominio.com/oauth/callback
OAUTH_ALLOWED_SCOPES=instances:provision
# opcional: OAUTH_CODE_TTL_SECONDS=300 (teto 300)

OAUTH_REDIRECT_URIS aceita lista separada por vírgula. Vários ambientes → vários URIs (todos HTTPS em produção).

Exemplo de fluxo (curl)

# 1) Browser do dono do tenant abre /oauth/authorize?... → login → consente →
#    redireciona para: https://chatbot/oauth/callback?code=CODE&state=STATE

# 2) Troca server-to-server do code pela API key:
curl -s -X POST https://devzap.seu-dominio.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=authorization_code \
  -d code=CODE \
  -d redirect_uri=https://chatbot.seu-dominio.com/oauth/callback \
  -d client_id=abite_xxx \
  -d client_secret=SEGREDO \
  -d code_verifier=VERIFIER
# → { "access_token": "ten_...", "token_type": "bearer", ... }

# 3) Usar a key:
curl -s https://devzap.seu-dominio.com/api/v1/instances \
  -H 'Authorization: Bearer ten_...'

# 4) Desconectar:
curl -s -X POST https://devzap.seu-dominio.com/oauth/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d client_id=abite_xxx -d client_secret=SEGREDO -d token=ten_...

← Todos os guias