SuperdindaDocs

Envelope e erros

O formato único das respostas JSON, as exceções a ele e o catálogo completo de códigos de erro com o status HTTP de cada um.

O envelope

Toda resposta JSON vem no mesmo envelope:

{ "success": true, "data": { "uid": "…" } }
{ "success": false, "error": { "code": "SEAT_NOT_ACTIVE", "message": "…", "details": { } } }
  • Os handlers só montam o lado do sucesso, com reply.ok (200), reply.created (201), reply.accepted (202) e reply.noContent (204). Pra falhar, o handler lança um AppError, e só o errorHandler transforma isso em HTTP.
  • code é o contrato. É estável e o cliente decide por ele. message é texto pra gente, em inglês, e pode mudar.
  • details é opcional e traz dados de máquina (por exemplo retryAt, maxDays, retryAfterSeconds).
  • Uma página paginada põe nextCursor ao lado de data, ou dentro de data. Veja Paginação.

Exceções ao envelope

RotaPor quê
GET /healthSonda de infraestrutura, lida por orquestradores. Responde { status, uptime, services } cru, com 200 ou 503 (degraded)
GET /accounts/{uid}/events e GET /consent-requests/{token}/eventsSSE tem o próprio formato de frame (veja Tempo real)
GET /seats/{uid}/pairing/avatarBytes de imagem, pra ir direto num <img> (reply.bytes)
Respostas 204Sem corpo
Gateway (/webhooks/*)É outro processo, fala com provedores e não usa o envelope

A família AppError

backend/src/core/contracts/errors.ts define a base e as classes genéricas. Cada domínio lança essas classes ou um DomainError com código próprio.

ClasseStatuscode
BadRequestError400BAD_REQUEST
UnauthorizedError401UNAUTHORIZED (sempre a mesma mensagem: o motivo nunca é revelado)
ForbiddenError403FORBIDDEN
NotFoundError404NOT_FOUND
ConflictError409CONFLICT
DomainError422o código da regra (tabelas abaixo)
TooManyRequestsError429RATE_LIMITED, com details.retryAfterSeconds e header Retry-After
UnavailableError503UNAVAILABLE

Códigos que não saem de uma classe:

codeStatusQuem gera
VALIDATION400O defaultHook do OpenAPIHono quando o zod recusa o pedido. A mensagem lista cada problema (campo: motivo; …)
NOT_FOUND404notFoundHandler, quando a rota não existe
HTTP_ERRORo da exceçãoUm HTTPException do Hono
INTERNAL500Qualquer erro não previsto. É logado, e a resposta não diz nada
STOPPED_BY_DEPENDENT409AppError direto: a própria pessoa parou a proteção, e só um novo consentimento retoma
EVIDENCE_VERIFICATION_UNAVAILABLE503A verificação de selfie e áudio não rodou (provedor fora). Nada é guardado. Tente de novo

Códigos de domínio (422)

Contas, vagas e onboarding

codeQuando
EMAIL_DOMAIN_INVALIDO e-mail não recebe mensagens (details.suggestion)
OWNER_HAS_NO_RELATIONSHIPTentou definir vínculo pro owner
OWNER_NOT_REMOVABLETentou remover o owner
SEAT_NOT_PENDINGA operação é só pra vaga pendente
SEAT_NOT_ACTIVEA operação exige vaga ativa (habilitar canal, pausar)
SEAT_ACTIVENão dá pra arquivar uma vaga ativa
DEPENDENT_REQUIRED / DEPENDENT_INCOMPLETEFalta a pessoa, ou falta telefone ou documento
DEPENDENT_LOCKEDA pessoa já disse sim a esses dados, que não mudam mais
BIRTH_DATE_OUT_OF_RANGEA data não combina com o kind (minor precisa ter menos de 18)
CAMPAIGN_NOT_FOUND / CAMPAIGN_ALREADY_APPLIEDCampanha desconhecida ou já aplicada
PAUSE_UNTIL_PAST / PAUSE_TOO_LONGuntil no passado / mais de 30 dias (details.maxDays)

Consentimento

codeQuando
CONSENT_REQUIREDFalta o sim da pessoa (ativação D2, pareamento)
RELATIONSHIP_REFUTEDA checagem de vínculo refutou a relação declarada
CONSENT_ALREADY_GIVENJá existe sim: ative a vaga
CONSENT_EXPIREDO link venceu ou foi trocado
CONSENT_NOT_GIVENO link de proteção não tem consentimento dado por trás
PROTECTION_LINK_REPLACEDUm consentimento mais novo substituiu este link
RESEND_LOCKEDEspera depois de recusa ou parada (details.retryAt)
AUDIO_PHRASE_MISMATCHO áudio não bate com a frase (details: expected, heard, missing, score)
AUDIO_UNINTELLIGIBLENão deu pra entender nenhuma palavra
SELFIE_NO_FACENenhum rosto, ou mais de um (details.faces, details.quality)

Canais e pareamento

codeQuando
CHANNEL_UNAVAILABLEO canal ainda não está disponível
CHANNEL_NOT_ENABLEDO canal não foi habilitado na vaga
PAIRING_EXISTSJá existe um pareamento aberto
PAIRING_NOT_OPENNão há pareamento aberto pra renovar o desafio
PAIRING_NOT_AWAITING_CONFIRMATIONNão há confirmação de número pendente
CONFIRMATION_NOT_SUPPORTEDO canal nunca pede essa confirmação
PAIRING_NOT_SWITCHABLEJá digitaram ou escanearam algo, e não dá mais pra trocar o método
DEPENDENT_PHONE_IS_GUARDIANO telefone da pessoa é de um responsável
NO_PROXY_CAPACITYTodos os endereços de proxy estão cheios

Importação de histórico (fila): NOTHING_TO_SYNC, NO_OPEN_OFFER, OFFER_EXPIRED, ALREADY_ADMITTED, NOT_IN_QUEUE, SEAT_KEY_GONE (a vaga não tem chave: nunca foi ativada, ou o consentimento foi retirado).

Backoffice: SELF_INVITE, MEMBER_UNKNOWN, SELF_CHANGE, LAST_OWNER (membros operadores); TOO_MANY, PRODUCT_NOT_ALLOWED, BUDGET_EXCEEDED, ORDER_UNKNOWN, QUOTE_NOT_AN_EXTENSION, QUOTE_EXPIRED, PROXY_UNREACHABLE, PROVIDER_UNKNOWN, PROVIDER_NOT_CONFIGURED (proxies e fornecedores).

Login: os códigos OTP_* (401)

Todos respondem 401 em POST /auth/otp/verify (classe OtpRefusedError, backend/src/domains/auth/errors.ts):

codeQuandodetails
OTP_INVALIDCódigo errado. Um e-mail sem conta recebe exatamente a mesma respostaattemptsLeft
OTP_LOCKEDA 5ª tentativa errada e todas depois dela, mesmo com o código certo. Um pedido novo desbloqueia—
OTP_EXPIREDCódigo certo, mas venceu ou foi trocado por um mais novo—
OTP_USEDCódigo certo, mas já usado—
OTP_LINK_INVALIDO token não é de um link nosso—
OTP_LINK_EXPIREDO link venceu ou foi trocado—
OTP_LINK_USEDO link já foi usado—

RATE_LIMITED (429) vale no login (request, verify, resend), no link de consentimento e no pareamento pelo celular da pessoa. Veja Limites de taxa.

Lendo um erro

Decidindo pelo code
const res = await fetch(`${API}/seats/${seatUid}/activate`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${token}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({ guardianAttestation: true }),
});
const body = await res.json();
if (!body.success) {
  switch (body.error.code) {
    case "CONSENT_REQUIRED":
      return goTo("await_consent");
    case "RATE_LIMITED":
      return wait(body.error.details.retryAfterSeconds);
    default:
      return showGeneric(body.error.message);
  }
}

Em aberto

Não existe ainda uma união ErrorCode publicada pros fronts nem details.field nos erros de formulário (item G-19 do api-gaps.md). Os códigos aparecem hoje nas descrições das respostas 422 de cada rota no OpenAPI.

Fontes

  • backend/src/http/response.ts, backend/src/http/app.ts, backend/src/http/middleware/error-handler.ts
  • backend/src/core/contracts/errors.ts
  • DomainError/AppError em backend/src/domains/** (levantamento por grep nas classes e nas strings de código)
  • backend/src/domains/consents/verification/errors.ts, backend/src/domains/subscriptions/use-cases/monitoring.ts
  • backend/src/http/routes/system/health.route.ts
  • backend/src/domains/auth/errors.ts
  • .specs/features/redesign/api-gaps.md (G-19, G-42)

On this page