Base URL: http://localhost:3000/api
Documentação interativa: http://localhost:3000/api/docs
Todas as rotas exigem o header abaixo, exceto /health, /auth/register e /auth/login:
Authorization: Bearer <token>
O token é obtido em POST /auth/login e vale 8 horas por padrão.
Sucesso:
{
"success": true,
"message": "Ticket aberto com sucesso",
"data": { },
"meta": { }
}meta aparece apenas em listagens paginadas.
Erro:
{
"success": false,
"error": {
"message": "Descrição legível do problema",
"details": [{ "field": "title", "message": "title deve ter no mínimo 5 caracteres" }]
}
}details aparece apenas em erros de validação (422).
| Código | Quando |
|---|---|
200 |
Sucesso com corpo |
201 |
Recurso criado |
204 |
Sucesso sem corpo (exclusão efetiva) |
400 |
Requisição inválida (referência inexistente, regra de dados) |
401 |
Não autenticado — token ausente, inválido ou expirado |
403 |
Autenticado, mas sem permissão |
404 |
Recurso não encontrado (ou fora do seu escopo) |
409 |
Conflito com o estado atual (transição inválida, duplicidade) |
422 |
Falha de validação de entrada |
503 |
Banco de dados indisponível |
401 vs 403:
401é problema de identidade ("não sei quem você é").403é problema de permissão ("sei quem você é, e você não pode").
Aceita ?page= e ?limit= (padrão: 1 e 10; teto de 100, aplicado silenciosamente).
"meta": {
"page": 2, "limit": 10, "total": 42, "totalPages": 5,
"hasPreviousPage": true, "hasNextPage": true
}Público. Verifica a API e a conexão com o banco.
{
"success": true,
"data": { "status": "ok", "database": "ok", "uptime": 128.4, "timestamp": "2026-08-07T16:17:26.979Z" }
}Retorna 503 com "database": "unreachable" se o MySQL estiver fora.
Público. Cria sempre um usuário CLIENTE.
{ "name": "João da Silva", "email": "joao@exemplo.com", "password": "Senha@123" }Senha: mínimo 8 caracteres, com maiúscula, minúscula e número.
201:
{
"success": true,
"message": "Cadastro realizado com sucesso",
"data": {
"user": { "id": 6, "name": "João da Silva", "email": "joao@exemplo.com", "role": "CLIENTE" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Enviar
"role": "ADMIN"no corpo não tem efeito: o campo não existe no schema de validação. Proteção contra mass assignment.
Erros: 409 e-mail já cadastrado · 422 validação
Público. Limitado a 10 tentativas por IP a cada 15 minutos.
{ "email": "admin@helpdesk.com", "password": "Senha@123" }200: mesmo formato do register.
Erros: 401 credenciais inválidas ou usuário desativado · 429 rate limit
A mensagem de erro é idêntica para e-mail inexistente e senha errada, para não permitir enumeração de usuários.
Retorna o perfil do usuário autenticado, com dados atuais do banco (não os do token).
{ "currentPassword": "Senha@123", "newPassword": "NovaSenha@456" }Exigir a senha atual protege contra sequestro de sessão: quem roubou o token não consegue tomar a conta.
Erros: 401 senha atual incorreta · 409 nova senha igual à atual
Escopo automático por papel:
| Papel | O que vê |
|---|---|
CLIENTE |
Apenas os próprios tickets — filtro forçado no servidor |
ATENDENTE |
Todos |
ADMIN |
Todos |
Query params
| Param | Exemplo | Descrição |
|---|---|---|
page, limit |
?page=2&limit=20 |
Paginação |
status |
?status=ABERTO,EM_ATENDIMENTO |
Múltiplos por vírgula |
priority |
?priority=URGENTE,ALTA |
Múltiplos por vírgula |
categoryId |
?categoryId=3 |
|
agentId |
?agentId=2 |
Ignorado para CLIENTE |
assignedToMe |
?assignedToMe=true |
Atalho da tela do atendente |
unassigned |
?unassigned=true |
Fila: tickets sem responsável |
search |
?search=pedido |
Busca em título e descrição |
createdFrom / createdTo |
?createdFrom=2026-08-01 |
Formato YYYY-MM-DD |
sortBy |
?sortBy=createdAt |
createdAt | updatedAt | priority | status |
sortOrder |
?sortOrder=asc |
asc | desc |
Ordenação padrão: prioridade URGENTE primeiro, depois mais recentes.
200:
{
"success": true,
"data": [{
"id": 2,
"title": "Cobrança duplicada no cartão",
"description": "Fui cobrado duas vezes pelo mesmo pedido #8842.",
"status": "ABERTO",
"priority": "URGENTE",
"category": { "id": 2, "name": "Pagamento" },
"client": { "id": 5, "name": "Elisa Cliente", "email": "elisa@cliente.com" },
"agent": null,
"createdAt": "2026-08-05 13:19:00",
"updatedAt": "2026-08-05 13:19:00",
"resolvedAt": null,
"closedAt": null
}],
"meta": { "page": 1, "limit": 10, "total": 5, "totalPages": 1, "hasPreviousPage": false, "hasNextPage": false }
}
"agent": nullsignifica "está na fila" — um estado real do negócio.
Erros: 422 valor inválido em filtro ou sortBy fora da whitelist
| Papel | Comportamento |
|---|---|
CLIENTE |
Abre para si mesmo |
ADMIN |
Pode informar clientId (chamado aberto por telefone) |
ATENDENTE |
403 — atendentes não abrem chamados, atendem |
{
"title": "Meu pedido ainda não chegou",
"description": "Comprei no dia 02 e o prazo era de 3 dias úteis.",
"categoryId": 3,
"priority": "ALTA",
"clientId": 4
}| Campo | Obrigatório | Regra |
|---|---|---|
title |
sim | 5–150 caracteres |
description |
sim | 10–5000 caracteres |
categoryId |
sim | Categoria existente e ativa |
priority |
não | Padrão MEDIA |
clientId |
não | Só ADMIN; deve ser usuário CLIENTE ativo |
Efeitos automáticos:
- Status inicial
ABERTO, sem responsável. - A descrição vira a primeira mensagem do ticket (na mesma transação).
URGENTEpedido por umCLIENTEé rebaixado paraALTA— senão todo mundo marcaria urgente.
Erros: 400 cliente inválido / categoria desativada · 403 atendente ou clientId de terceiro · 404 categoria inexistente · 422 validação
Retorna o ticket completo.
Um
CLIENTEacessando ticket alheio recebe404, não403. Um403confirmaria que o recurso existe.
{ "title": "...", "description": "...", "categoryId": 2, "priority": "ALTA" }Todos os campos são opcionais, mas ao menos um é obrigatório.
| Papel | Pode alterar | Quando |
|---|---|---|
CLIENTE |
title, description |
Só enquanto ABERTO |
ATENDENTE / ADMIN |
Todos | Enquanto não FECHADO |
Erros: 403 cliente tentando mudar prioridade/categoria · 409 ticket não está ABERTO (cliente) ou está FECHADO
{ "status": "RESOLVIDO" }Transições permitidas:
| De | Para |
|---|---|
ABERTO |
EM_ATENDIMENTO, FECHADO |
EM_ATENDIMENTO |
RESOLVIDO, ABERTO, FECHADO |
RESOLVIDO |
FECHADO, EM_ATENDIMENTO (reabertura) |
FECHADO |
(nenhuma — estado final) |
Quem pode:
| Papel | Transições |
|---|---|
CLIENTE |
Fechar (de qualquer estado permitido) e reabrir um RESOLVIDO |
ATENDENTE / ADMIN |
Todas as válidas |
Efeitos automáticos:
→ EM_ATENDIMENTOsem responsável: quem executou assume o ticket;resolved_ateclosed_atsão limpos.→ RESOLVIDO: carimbaresolved_at.→ FECHADO: carimbaclosed_at.
Erros:
{
"success": false,
"error": { "message": "Transição inválida: ABERTO -> RESOLVIDO. A partir de ABERTO só é possível ir para: EM_ATENDIMENTO, FECHADO." }
}403 papel sem permissão para a transição · 409 transição ilegal ou status já é o atual
ATENDENTE e ADMIN apenas.
{ "priority": "URGENTE" }{ "agentId": 2 }"agentId": null devolve o ticket à fila (volta para ABERTO).
| Papel | Pode |
|---|---|
ATENDENTE |
Atribuir apenas a si mesmo; devolver à fila um ticket que é seu |
ADMIN |
Atribuir a qualquer atendente ou admin |
Regras:
- O responsável nunca pode ter papel
CLIENTE→400 - Não pode ser um usuário desativado →
400 - Atendente não "rouba" ticket de colega →
409 - Se o ticket estava
ABERTO, passa paraEM_ATENDIMENTO
Atalho para "assumir": equivale a assign com o próprio id. Sem corpo.
ADMIN apenas. 204. As mensagens são removidas em cascata pelo banco.
O fluxo normal é fechar, que preserva o histórico. Excluir é exceção (spam, duplicidade).
Ordem cronológica. Aceita page e limit.
{
"success": true,
"data": [{
"id": 12,
"ticketId": 1,
"content": "Olá! Vou verificar o status do pedido.",
"isInternal": false,
"author": { "id": 2, "name": "Bruno Atendente", "role": "ATENDENTE" },
"createdAt": "2026-08-07 13:19:00"
}],
"meta": { }
}Mensagens com
isInternal: truenunca são retornadas para umCLIENTE. O filtro é aplicado no SQL.
{ "content": "Localizamos a cobrança e o estorno foi solicitado.", "isInternal": false }| Campo | Obrigatório | Regra |
|---|---|---|
content |
sim | 1–5000 caracteres |
isInternal |
não | Padrão false; só ATENDENTE/ADMIN |
Efeitos automáticos:
| Situação | O que acontece |
|---|---|
CLIENTE responde ticket RESOLVIDO |
Reabre para EM_ATENDIMENTO e limpa resolved_at |
Equipe responde publicamente ticket ABERTO sem dono |
Assume o ticket e move para EM_ATENDIMENTO |
| Nota interna | Não dispara nenhum dos efeitos acima |
Erros: 403 cliente tentando nota interna · 404 ticket fora do seu escopo · 409 ticket FECHADO
Qualquer usuário autenticado. Retorna apenas ativas; ?includeInactive=true funciona só para ADMIN.
{ "name": "Financeiro", "description": "Cobranças e faturas" }409 se o nome já existir.
{ "name": "...", "description": "...", "isActive": true }| Situação | Resposta |
|---|---|
| Sem tickets vinculados | 204 — removida de fato |
| Com tickets vinculados | 200 — apenas desativada, com mensagem explicando |
A API responde com honestidade sobre o que realmente aconteceu, em vez de dizer "deletado" nos dois casos.
Todas exigem ADMIN, exceto /users/agents.
Filtros: role, isActive, search (nome ou e-mail), page, limit.
ATENDENTE e ADMIN. Lista atendentes e admins ativos — alimenta a tela de atribuição.
Diferente do register público: aqui role é aceito.
{ "name": "Carla", "email": "carla@helpdesk.com", "password": "Senha@123", "role": "ATENDENTE" }{ "name": "...", "email": "...", "role": "ATENDENTE", "isActive": true }{ "role": "ATENDENTE" }Bloqueios:
| Regra | Resposta |
|---|---|
| Admin alterando a própria role | 403 |
| Operação deixaria o sistema sem nenhum admin ativo | 400 |
Rebaixar para CLIENTE alguém com tickets vinculados |
409 |
{ "newPassword": "NovaSenha@123" }| Situação | Resposta |
|---|---|
| Sem histórico | 204 — removido |
| Com tickets ou mensagens | 200 — desativado |
| Removendo a si mesmo | 403 |
| Último admin ativo | 400 |
Query param opcional: ?days=30 (janela da série temporal, 1–365, padrão 14).
{
"success": true,
"data": {
"resumo": {
"total": 5, "abertos": 2, "emAtendimento": 1,
"resolvidos": 1, "fechados": 1, "urgentes": 2, "naFila": 2
},
"porPrioridade": [{ "priority": "URGENTE", "total": 2 }],
"porCategoria": [{ "categoryId": 3, "category": "Entrega", "total": 1, "emAberto": 1 }],
"porAtendente": [{
"agentId": 2, "agent": "Bruno Atendente",
"totalAtribuidos": 2, "emAtendimento": 1, "concluidos": 1,
"tempoMedioResolucaoMinutos": 1680
}],
"metricas": {
"totalResolvidos": 2,
"tempoMedioHoras": 28,
"tempoMinimoHoras": 28,
"tempoMaximoHoras": 28,
"atendenteDestaque": { "agentId": 2, "agent": "Bruno Atendente", "concluidos": 1 },
"taxaResolucaoPercentual": 40
},
"ticketsPorDia": [{ "dia": "2026-08-05", "total": 5, "urgentes": 2 }]
}
}As sete consultas rodam em paralelo (Promise.all), então o tempo total é o da mais lenta, não a soma.
{
"success": true,
"data": { "totalAtribuidos": 2, "emAtendimento": 1, "concluidos": 1, "urgentesPendentes": 0 }
}Login e captura do token:
curl -s -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d "{\"email\":\"admin@helpdesk.com\",\"password\":\"Senha@123\"}"Listar tickets urgentes em aberto:
curl -s "http://localhost:3000/api/tickets?status=ABERTO&priority=URGENTE" -H "Authorization: Bearer SEU_TOKEN"Abrir um chamado:
curl -s -X POST http://localhost:3000/api/tickets -H "Authorization: Bearer SEU_TOKEN" -H "Content-Type: application/json" -d "{\"title\":\"Não consigo acessar minha conta\",\"description\":\"O login retorna erro 500 desde ontem à noite.\",\"categoryId\":4}"Assumir e resolver:
curl -s -X POST http://localhost:3000/api/tickets/1/claim -H "Authorization: Bearer SEU_TOKEN"curl -s -X PATCH http://localhost:3000/api/tickets/1/status -H "Authorization: Bearer SEU_TOKEN" -H "Content-Type: application/json" -d "{\"status\":\"RESOLVIDO\"}"