A Public API usa HTTP status codes padrão + um campo error no body pra identificar o problema de forma programática.
Toda resposta de erro tem este shape JSON:
"error" : " <código_machine_readable> " ,
"message" : " <descrição humana em pt-BR> " ,
"...campos contextuais específicos do erro"
Faça matching no error (estável, machine-readable). O message é amigável pra log/exibição mas pode mudar sem aviso.
Tabela de erros
400 — Bad Request
errorQuando validation_failedBody inválido (campo obrigatório ausente, tipo errado, formato inválido de email/phone). Inclui details com lista de erros por campo missing_required_fieldCampo obrigatório ausente (alternativa simplificada do anterior em alguns endpoints) invalid_formatValor com formato incorreto (ex: data fora do ISO 8601, telefone sem código de país)
401 — Unauthorized
errorQuando invalid_api_keyChave ausente, malformada, revogada ou expirada
403 — Forbidden
errorQuando insufficient_scopeKey autenticou mas falta scope pra esse endpoint. Inclui required e have arrays endpoint_not_exposedEndpoint não está disponível via API Key (só via UI/JWT) tenant_mismatchTentativa de acessar recurso de outro tenant (não deveria acontecer com keys bem isoladas)
404 — Not Found
errorQuando not_foundRecurso solicitado não existe ou foi deletado (soft-delete). Mensagem identifica qual recurso
409 — Conflict
errorQuando duplicate_emailTentativa de criar contato com email já existente no tenant duplicate_phoneTentativa de criar contato com telefone já existente stage_invalidstageId informado não pertence ao pipeline informado
422 — Unprocessable Entity
errorQuando business_rule_violationValidação passou no shape mas violou regra de negócio (ex: tentar mover deal pra stage de outro pipeline)
429 — Too Many Requests
500 — Internal Server Error
errorQuando internal_errorErro inesperado no servidor. Reportado automaticamente — se persistir, abra um issue com o request id
Exemplos
Validação falhou
Content-Type : application/json
Content-Type : application/json
"error" : " validation_failed " ,
"message" : " Validação dos campos falhou " ,
{ "field" : " firstName " , "issue" : " não pode ser vazio " },
{ "field" : " email " , "issue" : " formato de email inválido " }
Scope insuficiente
"error" : " insufficient_scope " ,
"message" : " Scope insuficiente. Necessário: contacts:write " ,
"required" : [ " contacts:write " ],
"have" : [ " contacts:read " ]
Duplicate
"error" : " duplicate_email " ,
"message" : " Já existe um contato com este email no tenant " ,
"existingContactId" : " ckl4z9w8v0001abcdef123456 "
Use o existingContactId pra decidir: PATCH em vez de POST se a integração precisa do “upsert” idiomático.
Tratamento programático
const res = await fetch ( url , opts )
const err = await res . json ()
case ' rate_limit_exceeded ' :
const wait = res . headers . get ( ' retry-after ' ) ?? 1
await new Promise ( r => setTimeout ( r , wait * 1000 ))
// Upsert: atualiza em vez de criar
return patch ( err . existingContactId , payload )
case ' insufficient_scope ' :
throw new ConfigError ( err . message ) // alerta humano — config errada
throw new Error ( ` API error ${ err . error } : ${ err . message } ` )
Request ID
Toda resposta inclui o header X-Request-Id (UUID) — guarde em logs do seu lado. Se precisar reportar um bug, mandar esse ID acelera muito a investigação.