2. Auth (Registro e Login)#
POST/v1/auth/register#
Cria uma nova empresa (tenant) com o usuário owner.
Auth: Nenhuma
Request:
{
"company_name": "Minha Empresa",
"owner_email": "dono@empresa.com",
"owner_name": "Joao Silva",
"password": "MinhaSenh@123"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
company_name |
string | sim | Nome da empresa |
owner_email |
string | sim | Email do owner |
owner_name |
string | sim | Nome do owner |
password |
string | sim | Senha (mínimo 12 caracteres) |
Resposta 201:
{
"company_id": 1,
"token": "bza_xxxx...xxxx",
"api_token": "bza_xxxx...xxxx",
"token_id": 1,
"last4": "xxxx",
"slug": "minha-empresa",
"email_verification_required": true,
"message": "company registered successfully"
}
| Campo | Tipo | Descricao |
|---|---|---|
company_id |
integer | ID numerico da empresa criada (ex: 1, 38, 42 — NÃO é string) |
token |
string | API token bootstrap bza_... — mesmo valor que api_token |
api_token |
string | Alias de token (mesmo valor, mantido por retrocompatibilidade) |
token_id |
integer | ID interno do token no banco |
last4 |
string | Últimos 4 caracteres do token (para exibição segura) |
slug |
string | Slug URL-safe derivado do company_name |
email_verification_required |
boolean | true quando o owner deve confirmar o email com o código 6 dígitos enviado por Resend (sempre que o registro usou senha). Integrações que não renderizam UI podem ignorar — POST /v1/instances e /v1/tokens e /v1/webhooks retornam 403 EMAIL_NOT_VERIFIED até que POST /v1/auth/verify-email seja chamado. |
message |
string | Mensagem de confirmação |
⚠️
company_idé INTEGER, não string. JSON retorna"company_id": 38(número sem aspas). Tipagens de DTO que usemstringpara este campo falharão silenciosamente nojson.Unmarshal/JSON.parsecom erro de tipo. Useint,int64, ounumber.
O
tokenretornado no registro e um API token bootstrap (bza_...), não um JWT de sessão do browser. Para chamadas subsequentes, envie viaX-API-Key: bza_...(ver seção Autenticação acima). Para iniciar a sessão web, chamePOST /v1/auth/login.
Erros:
| Status | Código | Body | Descricao |
|---|---|---|---|
400 |
VALIDATION_ERROR |
{"error":"password must be at least 12 characters","error_code":"VALIDATION_ERROR"} |
Campos faltando ou senha curta |
409 |
EMAIL_ALREADY_REGISTERED |
{"error":"email already registered","error_code":"EMAIL_ALREADY_REGISTERED"} |
Email já usado por outra empresa |
409 |
SLUG_TAKEN |
{"error":"company name already taken","error_code":"SLUG_TAKEN"} |
Slug do company_name já existe |
Integrações automatizadas (como o Catcher) que provisionam multiplas empresas com o mesmo email humano devem usar plus-addressing (
user+suffix@domain.com) ou emails únicos por tenant para evitarEMAIL_ALREADY_REGISTERED.
POST/v1/auth/login#
Autêntica um usuário e retorna JWT.
Auth: Nenhuma
Request:
{
"email": "dono@empresa.com",
"password": "MinhaSenh@123"
}
Resposta 200:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"company_id": 1,
"user_id": 1,
"role": "owner",
"email": "dono@empresa.com",
"is_superadmin": false
}
Cookies de sessão enviados no login bem-sucedido:
biazap_refresh_token:HttpOnly, usado para rotação de sessãobiazap_csrf_token: usado no headerX-CSRF-Tokenem requests mutaveis quando o cookie de refresh estiver presente
Protecoes adicionais:
- lockout por conta após 5 falhas em 15 minutos
- JWT HS256 com
jtie revogacao em logout - refresh token com rotação a cada uso
Erros:
401- Credenciais inválidas403- Empresa suspensa429- Conta temporariamente bloqueada por excesso de tentativas
GET/v1/auth/oauth/{provider}/start#
Inicia o login/cadastro social via OAuth 2.0. provider aceita google ou github.
Auth: Nenhuma
Query params:
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
mode |
string | não | login ou signup. Default: login |
Resposta 302: redireciona para o provedor com state, redirect_uri, client_id, response_type=code e escopos:
- Google:
openid email profile - GitHub:
read:user user:email
Erros JSON:
400 OAUTH_PROVIDER_INVALID- provider diferente degoogleougithub503 OAUTH_PROVIDER_DISABLED- provider semOAUTH_<PROVIDER>_ENABLED=true,CLIENT_IDouCLIENT_SECRET503 SERVICE_UNAVAILABLE- Redis indisponível para armazenar ostate
Quando o request e uma navegação de browser (Accept: text/html), esses erros redirecionam para /login?oauth_error=<ERROR_CODE> em vez de devolver JSON.
GET/v1/auth/oauth/{provider}/callback#
Callback configurado no Google/GitHub. O backend válida state, troca code por access token, busca o perfil e exige email verificado.
Auth: Nenhuma
Fluxo:
- Identidade OAuth existente: emite a mesma sessão do
POST /v1/auth/logine redireciona para/oauth/callback?provider=.... - Email verificado já cadastrado: vincula automaticamente o provider ao usuário existente, emite sessão e redireciona para
/oauth/callback?provider=.... - Email novo: cria um token pendente curto no Redis e redireciona para
/oauth/complete?provider=...&token=....
Cookies de sessão: quando o callback conclui login de usuário existente, envia biazap_refresh_token e biazap_csrf_token com as mesmas regras do login por senha.
Erros via redirect: falhas redirecionam para /login?oauth_error=<ERROR_CODE>&provider=<provider>.
| Código | Quando ocorre |
|---|---|
OAUTH_STATE_INVALID |
state ausente, expirado ou de outro provider |
OAUTH_EMAIL_UNVERIFIED |
Provider não retornou email verificado |
OAUTH_UPSTREAM_FAILED |
Falha no token exchange ou profile fetch |
OAUTH_PROVIDER_INVALID |
Provider desconhecido |
OAUTH_PROVIDER_DISABLED |
Provider desabilitado ou sem credenciais |
POST/v1/auth/oauth/complete-signup#
Finaliza cadastro social novo pedindo apenas o nome da empresa. Cria empresa, tenant DB, usuário owner e vinculo OAuth; em seguida emite sessão browser.
Auth: Nenhuma
Request:
{
"token": "pending_oauth_token",
"company_name": "Minha Empresa"
}
Resposta 200: mesmo payload de POST /v1/auth/login, mais cookies biazap_refresh_token e biazap_csrf_token.
Erros:
400 OAUTH_PENDING_INVALID- token pendente inválido ou expirado400 BAD_REQUEST-company_nameinválido409 EMAIL_ALREADY_REGISTERED- email foi cadastrado antes da conclusao409 CONFLICT- nome/slug da empresa já existe500 REGISTRATION_FAILED- falha ao provisionar empresa, usuário, token ou tenant
Variaveis de ambiente OAuth:
| Variavel | Descricao |
|---|---|
OAUTH_FRONTEND_REDIRECT_URL |
Base do Console para redirects (https://app.catcher.one) |
OAUTH_GOOGLE_ENABLED / OAUTH_GITHUB_ENABLED |
Habilita explicitamente cada provider (true/false) |
OAUTH_GOOGLE_CLIENT_ID / OAUTH_GOOGLE_CLIENT_SECRET |
Credenciais Google |
OAUTH_GOOGLE_REDIRECT_URL |
Callback cadastrado no Google (https://api.../v1/auth/oauth/google/callback) |
OAUTH_GITHUB_CLIENT_ID / OAUTH_GITHUB_CLIENT_SECRET |
Credenciais GitHub |
OAUTH_GITHUB_REDIRECT_URL |
Callback cadastrado no GitHub (https://api.../v1/auth/oauth/github/callback) |
*_AUTH_URL, *_TOKEN_URL, *_USER_INFO_URL e OAUTH_GITHUB_EMAILS_URL existem para testes/overrides; em produção os defaults oficiais são usados.
POST/v1/auth/refresh#
Rotaciona o refresh token e devolve um novo access token JWT.
Auth: Cookie biazap_refresh_token
Headers recomendados:
X-CSRF-Token: <valor_do_cookie_biazap_csrf_token>
Resposta 200: mesmo payload de POST /v1/auth/login.
Erros:
401- Refresh token ausente, inválido ou expirado403- Falha de CSRF quando houver cookie de refresh
POST/v1/auth/logout#
Revoga o JWT atual, inválida o refresh token e limpa os cookies de sessão.
Auth: JWT em Authorization: Bearer <token>
Headers recomendados:
X-CSRF-Token: <valor_do_cookie_biazap_csrf_token>
Resposta 204: Sem corpo.
POST/v1/auth/verify-email#
Válida o código de 6 dígitos enviado por email no momento do registro (ou por
resend). Ao sucesso, marca users.email_verified_at = NOW(), limpa a state de
verificação e reemite a sessão (mesmos cookies de POST /v1/auth/login). A
resposta é idêntica ao login, mais o campo email_verified: true.
Auth: JWT em Authorization: Bearer <token> (+ CSRF header quando via
browser)
Request:
{ "code": "428193" }
Resposta 200: Idêntico ao login.
{
"token": "<JWT>",
"company_id": 42,
"user_id": 118,
"role": "owner",
"email": "dono@empresa.com",
"is_superadmin": false,
"email_verified": true
}
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
EMAIL_VERIFICATION_CODE_INVALID |
Código com formato errado, ou não bate com o persistido. Contador email_verification_attempts é incrementado. |
400 |
EMAIL_VERIFICATION_CODE_EXPIRED |
Código expirou (TTL de 30 minutos). Peça resend e tente de novo. |
429 |
EMAIL_VERIFICATION_MAX_ATTEMPTS |
5 tentativas erradas consecutivas — a state é limpa, obrigando resend. |
409 |
EMAIL_ALREADY_VERIFIED |
Usuário já verificado (ex: outro tab). Cliente pode seguir direto. |
POST/v1/auth/verify-email/resend#
Gera um novo código (invalidando qualquer anterior) e dispara o email via Resend. Respeita cooldown de 60 segundos entre requisições.
Auth: JWT em Authorization: Bearer <token>
Resposta 202:
{
"message": "verification code sent",
"cooldown_secs": 60,
"expires_in": 1800
}
Erros:
| Status | Código | Quando |
|---|---|---|
429 |
EMAIL_VERIFICATION_RESEND_COOLDOWN |
Enviou faz menos de 60 segundos. Retry-After header diz quantos segundos faltam. |
409 |
EMAIL_ALREADY_VERIFIED |
Nada a reenviar. |
PATCH/v1/auth/me#
Edita o nome do usuário autenticado e/ou o nome da empresa. Ambos os campos são opcionais (sem nenhum = no-op 200). company_name só é honrado para role=owner.
Auth: JWT em Authorization: Bearer <token>
Request:
{
"name": "Renata Schelbauer",
"company_name": "Catcher LTDA"
}
Resposta 200:
{
"user": { "id": 12, "email": "renata@catcher.one", "name": "Renata Schelbauer", "role": "owner" },
"company": { "id": 9, "name": "Catcher LTDA", "slug": "catcher-original-slug" }
}
Slug não muda. Mesmo se você renomear a empresa, o
slugpermanece o original (compatibilidade com integrações externas).
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
BAD_REQUEST |
Campo vazio (após trim) ou maior que 255 caracteres |
403 |
OWNER_ONLY |
Usuário não-owner tentou setar company_name |
PATCH/v1/auth/me/password#
Troca a senha do usuário autenticado. Exige a senha atual + nova com mínimo 8 caracteres. Em sucesso, revoga todas as sessões ativas (refresh tokens) e bumpa password_changed_at (que inválida o JWT atual via claim pwd_changed_at). O JWT atual contínua válido até expirar (~15min); no próximo refresh o usuário é forçado a re-logar.
Auth: JWT em Authorization: Bearer <token>
Request:
{
"current_password": "MinhaSenhaAtual",
"new_password": "MinhaNovaSenha"
}
Resposta 200:
{ "message": "password updated", "requires_relogin": true }
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
PASSWORD_TOO_SHORT |
new_password com menos de 8 caracteres |
400 |
MISSING_FIELD |
current_password vazio |
401 |
INVALID_CURRENT_PASSWORD |
Senha atual incorreta |
POST/v1/auth/me/email/start#
Solicita troca de email. Exige o novo email + a senha atual. Envia código de 6 dígitos para o novo email + notificação de aviso para o email antigo. Token persiste em Redis por 30min; uma segunda chamada antes da confirmação sobrescreve a primeira (last-write-wins).
Auth: JWT em Authorization: Bearer <token>
Request:
{
"new_email": "renata.new@example.com",
"current_password": "MinhaSenhaAtual"
}
Resposta 202:
{ "message": "verification code sent to the new email", "expires_in": 1800 }
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
BAD_REQUEST |
Email inválido ou igual ao email atual |
400 |
MISSING_FIELD |
new_email ou current_password vazio |
401 |
INVALID_CURRENT_PASSWORD |
Senha atual incorreta |
409 |
EMAIL_ALREADY_TAKEN |
new_email já está em uso por outra conta |
POST/v1/auth/me/email/confirm#
Confirma o código de 6 dígitos recebido no novo email. Em sucesso, troca users.email no DB e revoga todos os refresh tokens. Cliente deve descartar a sessão e redirecionar para /login.
Auth: JWT em Authorization: Bearer <token>
Request:
{ "code": "123456" }
Resposta 200:
{ "message": "email updated — please log in again", "requires_relogin": true }
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
EMAIL_CHANGE_CODE_INVALID |
Código não bate. Token contínua válido até 5 tentativas. |
404 |
EMAIL_CHANGE_NOT_FOUND |
Sem start prévio ou TTL expirou |
429 |
EMAIL_CHANGE_LOCKED |
5 tentativas erradas; token foi destruído. Reinicie com start |