Pular para o conteúdo

Contratos

Convenções

Datas, telefone, e-mail, dinheiro, campos personalizados e a regra de null vs ausente no PATCH.

As regras que valem em toda a API, e que quase sempre são descobertas tarde: formato de data, o que a gente faz com o telefone que você manda, como limpar um campo, e onde a validação existe — e onde ela não existe.

Datas

Saída: sempre ISO-8601 UTC

Toda data que a API devolve é ISO-8601 em UTC, com milissegundos e Z: 2026-09-13T18:04:11.231Z. Uma única exceção: lastDate, dentro de lessons[] e library[] de GET /platform/users/:id, vem como YYYY-MM-DD no fuso America/Sao_Paulo — é um agregado diário, não um instante.

Entrada: onde o fuso é obrigatório

startAt e endAt de reunião (POST /appointments e PATCH /appointments/:id/reschedule) exigem fuso explícito: 2026-09-20T14:00:00Z ou 2026-09-20T11:00:00-03:00. Sem fuso é 400 validation_error — porque 2026-09-20T14:00:00 cru viraria hora local do servidor, que não é a sua.

Já date, from e to de GET /calendars/:id/slots são YYYY-MM-DD (um dia local, não um instante) — qualquer outra coisa é 400. A resposta dessa rota continua em UTC.

Fuso horário

A API não converte fuso em lugar nenhum. O único parâmetro de fuso é tz em GET /calendars/:id/slots — identificador IANA (padrão America/Sao_Paulo, inválido é 400) — e ele define apenas qual dia local calcular. Os startAt/endAt dos horários livres voltam em UTC, com o timezone pedido ecoado na resposta.

Inclusividade dos filtros de data

Não é uniforme, e a diferença muda o resultado de um laço de sincronização. O inventário completo:

RotaParâmetroCompara
/contactscreatedAfter, updatedAfter> (não existe *Before aqui)
/opportunitiescreatedAfter, updatedAfter, wonAfter, lostAfter, stageChangedAfter>
/opportunitiescreatedBefore, updatedBefore, wonBefore, lostBefore<
/opportunities/summaryfrom / to>= / < — o intervalo é [from, to)
/opportunity-eventscreatedAfter / createdBefore> / < (sobre o momento da transição)
/appointmentsfrom / to>= / < — sobre o horário da reunião
/appointmentscreatedAfter, updatedAfter>
/interactionscreatedAfter / createdBefore> / <
/platform/usersactiveSince / activeBefore>= / <

Duas bordas que devolvem 400: from >= to em /opportunities/summary e to <= from em /appointments. E negócio sem wonAt/lostAt fica de fora de wonAfter/lostAfter por construção.

Telefone

As três operações têm regras diferentes, e é aí que mora a confusão:

  • Gravar. POST /contacts aceita até 20 caracteres no campo; POST /contacts/upsert aceita até 80. O mesmo valor com um espaço a mais pode passar num e dar 400 no outro.
  • Buscar. GET /contacts?phone= compara só os dígitos, dos dois lados, por igualdade exata (o parâmetro aceita de 3 a 40 caracteres). Como a gravação normalizou para 55…, buscar 11988887777 não acha o contato: busque 5511988887777.
  • Deduplicar. O /contacts/upsert usa a forma canônica brasileira — tira o DDI 55 e o nono dígito do celular — e, para formas não-BR, casa pelo sufixo comum de 10 dígitos. Aqui 5511988887777, 11988887777 e 1188887777 são o mesmo contato.

Recomendação que faz as três concordarem: padronize tudo em E.164 sem o sinal (5511988887777), na gravação e na busca.

E-mail

POST /contacts e PATCH /contacts/:id validam o formato: e-mail malformado é 400 validation_error.

CPF

Campo de texto livre, até 20 caracteres. Não validamos dígito verificador, não aplicamos máscara, não exigimos unicidade e nunca usamos CPF para deduplicar contato (a deduplicação é por telefone e depois por e-mail). Se você pretende cruzar por CPF depois, mande sempre no mesmo formato — só dígitos.

Dinheiro

valueCents é inteiro, em centavos (R$ 1.250,00 = 125000), entre 0 e 1.000.000.000. currency é campo de saída: o servidor devolve, você não envia.

Campos personalizados

Descubra as chaves válidas em GET /custom-fields?entity=contact|opportunity — a resposta traz fieldKey, label, type, required e options (só os valores aceitos). A lista mostra apenas campos ativos e de escopo do workspace.

No negócio: validado

PUT /opportunities/:id/custom-fields faz merge só das chaves enviadas e confere o que recebe: chave desconhecida ou valor fora das options respondem 400 validation_error nomeando a chave.

No contato: aceito sem conferir

Duas notas que valem para os dois lados: o merge nunca remove chave — mandar {"x": null} grava o valor null naquela chave, não a apaga. E o required do catálogo não é imposto à API: ele vale para quem preenche pela tela; uma chave de API pode criar registro sem o campo obrigatório, ou esvaziá-lo, e receber 200.

Ausente vs null no PATCH

Campo ausente do corpo = não mexe. null explícito = limpa. Nem todo campo aceita ser limpo:

RotaAceitam null (limpam)Não aceitam null
PATCH /contacts/:idphone, email, company, companyId, cpf, role, notesname, customFields
PATCH /opportunities/:idexpectedCloseAt, ownerUserIdtitle, valueCents, temperature, tagsAdd, tagsRemove

O que o PATCH não faz

  • Etiqueta de contato não se mexe por PATCH: use POST /contacts/:id/tags com mode: "add" | "remove" (o padrão é add). Na criação do contato, por outro lado, tags é aceito direto.
  • Etiqueta de negócio vai em tagsAdd/tagsRemove dentro do próprio PATCH.
  • Estágio de negócio nunca muda por PATCH: use POST /opportunities/:id/move. E repare que /win e /lose também movem o negócio — para o estágio de ganho ou de perda do funil.

Origem: origin, source e utm

Três campos parecidos que não querem dizer a mesma coisa:

  • origin — proveniência de sistema: inbox_whatsapp, inbox_instagram, inbox_facebook, inbox_phone, inbox_email, manual, import, ia, seed, automation, api, form.
  • source — o canal daquela entrada (youtube, lp, trafego…). Existe no negócio e na reunião.
  • utm — as UTMs daquela entrada. No contato é first-touch: gravado só na criação, e um upsert que encontra o contato não sobrescreve. No negócio a UTM é a daquela entrada, nunca herdada do contato.

Ids, arquivamento e tetos de corpo

Ids são strings opacas: não presuma formato, tamanho nem ordem alfabética.

Apagar não existe. DELETE /contacts/:id arquiva (resposta {"archived": true}), e desarquivar só pela tela — não há rota v1 para isso. DELETE /webhooks/:id é a única exclusão de verdade, e leva junto todo o histórico de entregas daquela assinatura.

Dois tetos de corpo, e o menor é o que morde. As escritas que passam pelo Núcleo aceitam corpo HTTP de até 256 KB, mas os argumentos da Ação — o que vai para a trilha de auditoria — não podem passar de 7,5 KB. Os dois respondem 413 payload_too_large, e o segundo chega bem antes. Campos grandes (notes, customFields) são os candidatos naturais a cortar.