3. 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": "ctc_xxxx...xxxx",
"api_token": "ctc_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 ctc_... — 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 (ctc_...), não um JWT de sessão do browser. Para chamadas subsequentes, envie viaX-API-Key: ctc_...(ver seção Autenticação acima). Para iniciar a sessão web, chamePOST /v1/auth/login.
Erros:
| Status | Código | Descricao |
|---|---|---|
400 |
INVALID_JSON |
Corpo não e JSON válido |
400 |
MISSING_FIELD |
Campo obrigatório ausente (company_name, owner_email, owner_name, password) |
400 |
BAD_REQUEST |
company_name com caracteres inválidos, owner_email inválido, ou senha com menos de 12 caracteres |
409 |
CONFLICT |
Nome/slug da empresa já existe (company name already taken) |
409 |
EMAIL_ALREADY_REGISTERED |
Email já usado por outra conta |
500 |
REGISTRATION_FAILED |
Falha ao provisionar plano, empresa, usuário, token ou tenant DB |
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/quick-register#
Cria uma empresa (tenant) a partir de apenas um email. Gera automaticamente nome da empresa, nome do owner, senha e API key, e marca o email como verificado na criação (sem o código de 6 digitos) — receber o email com as credenciais já prova a posse do endereço. Desenhado para onboarding dirigido por agente LLM.
Auth: Nenhuma
Request:
{
"email": "dono@empresa.com"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
email |
string | sim | Email do owner (deve ser válido) |
Resposta 201:
{
"company_id": 42,
"api_key": "ctc_xxxx...xxxx",
"email": "dono@empresa.com",
"password": "a1b2c3d4e5f6g7h8",
"base_url": "https://api.catcher.one",
"message": "account created — credentials sent to your email"
}
| Campo | Tipo | Descricao |
|---|---|---|
company_id |
integer | ID numerico da empresa criada |
api_key |
string | API token bootstrap ctc_... — usar em X-API-Key |
email |
string | Email cadastrado |
password |
string | Senha gerada automaticamente (16 chars hex) — também enviada por email |
base_url |
string | Base URL da API |
message |
string | Mensagem de confirmação |
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
INVALID_JSON |
Corpo não e JSON válido |
400 |
BAD_REQUEST |
Email ausente ou inválido |
409 |
EMAIL_ALREADY_REGISTERED |
Email já usado por outra conta |
500 |
INTERNAL_ERROR |
Falha ao provisionar plano, empresa, token ou tenant DB |
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",
"name": "Joao Silva",
"is_superadmin": false,
"email_verified": true
}
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:
| Status | Código | Quando |
|---|---|---|
400 |
INVALID_JSON |
Corpo não e JSON válido |
400 |
MISSING_FIELD |
email ou password ausente |
401 |
INVALID_CREDENTIALS |
Email ou senha incorretos |
429 |
ACCOUNT_LOCKED |
Conta bloqueada após 5 falhas em 15 minutos |
503 |
SERVICE_UNAVAILABLE |
Serviço de autenticação indisponível (Redis) |
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:
| Status | Código | Quando |
|---|---|---|
401 |
REFRESH_FAILED |
Refresh token ausente, inválido, expirado, ou usuário não existe |
403 |
CSRF_INVALID |
Header X-CSRF-Token ausente ou divergente do cookie (rejeitado no middleware quando ha cookie de refresh) |
500 |
INTERNAL_ERROR |
Falha ao reemitir a sessão |
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/forgot-password#
Solicita a recuperacao de senha. Sempre devolve 202 independentemente de o
email existir (anti-enumeracao). Quando o email pertence a um usuário, gera um
token de uso único, persiste apenas o hash SHA256 dele e envia um link
/reset-password?token=... por email. O token bruto nunca e persistido.
Auth: Nenhuma
Request:
{
"email": "dono@empresa.com"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
email |
string | sim | Email da conta |
Resposta 202:
{ "message": "if an account exists for that email, a reset link was sent" }
A resposta e sempre
202, mesmo para um email inexistente — não ha como diferenciar pelo retorno se a conta existe. O link de reset expira conformePASSWORD_RESET_TTL(default 30 minutos) e e de uso único.
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
INVALID_JSON |
Corpo não e JSON válido |
400 |
MISSING_FIELD |
email ausente |
500 |
INTERNAL_ERROR |
Falha ao gerar ou persistir o token de reset |
POST/v1/auth/reset-password#
Consome o token de uso único recebido por email e define uma nova senha. O
token e marcado como usado imediatamente (replay impossível). Em sucesso,
revoga todas as sessões ativas (refresh tokens) e bumpa
password_changed_at, invalidando qualquer JWT pendente.
Auth: Nenhuma
Request:
{
"token": "abc123...def",
"password": "MinhaNovaSenh@123"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
token |
string | sim | Token bruto recebido no link de email |
password |
string | sim | Nova senha (mínimo 12 caracteres) |
Resposta 200:
{ "message": "password updated — you can now log in" }
Erros:
| Status | Código | Quando |
|---|---|---|
400 |
INVALID_JSON |
Corpo não e JSON válido |
400 |
MISSING_FIELD |
token ou password ausente |
400 |
BAD_REQUEST |
password com menos de 12 caracteres |
400 |
RESET_TOKEN_INVALID |
Token desconhecido, já usado, ou usuário não existe |
400 |
RESET_TOKEN_EXPIRED |
Token expirou — solicite um novo |
500 |
INTERNAL_ERROR |
Falha ao validar token ou atualizar senha |
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",
"name": "Joao Silva",
"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 continua 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 continua 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 |