Pular para o conteúdo principal

Contatos

O Contato é a pessoa — candidato, aluno, responsável. Um cadastro só, que atravessa a jornada inteira. Esta página cobre criar, atualizar e sincronizar contatos a partir do seu sistema.

Todos os caminhos são relativos a API_BASE/public/v1/crm, e toda requisição leva a API key: Authorization: Bearer wf_sua_chave_aqui.

Identidade — leia antes de integrar​

O contato é único dentro da instituição, e a sua API key já diz qual é. Três propriedades identificam um contato de forma única, e são as que você pode usar para endereçar e para sincronizar:

PropriedadeO que é
externalIdO id do contato no seu sistema. É a que você deve preferir
emailE-mail
taxIdCPF ou CNPJ

externalId não é o id do CRM de onde você está migrando. É o id no sistema que está chamando esta API. Gravar o Record ID do HubSpot aqui faz a próxima integração apontar para o lugar errado.

phone não é único — mais de um contato pode ter o mesmo telefone, inclusive na mesma instituição, e por isso não está entre as propriedades de idProperty.

Criar um contato​

POST /contacts  ·  síncrono

curl -X POST "API_BASE/public/v1/crm/contacts" \
-H "Authorization: Bearer wf_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"name": "Ana Souza",
"email": "ana.souza@exemplo.com",
"phone": "+5511999999999",
"taxId": "12345678901",
"externalId": "aluno-4471",
"interesse_em_bolsa": "Sim"
}
}'

Tudo vai dentro de properties — nativas e customizadas, sem distinção de sintaxe.

PropriedadeTipoObrigatórioDescrição
emailstringcondicionalemail ou phone é obrigatório — um contato sem nenhum dos dois não é alcançável. O formato de e-mail é validado.
phonestringcondicionalE.164 (+5511999999999). Não é único — ver "Identidade" acima.
namestringnãoNome completo, um campo só.
taxIdstringnãoCPF ou CNPJ. Pontuação é aceita e removida: o contato guarda só os dígitos. CPF é validado pelo dígito verificador, CNPJ pelos 14 dígitos; inválido responde 400.
externalIdstringnãoO id do contato no seu sistema.
externalStudentIdstringnãoO id de aluno no seu sistema, quando for diferente do externalId.
birthDatestringnãoAAAA-MM-DD (1998-04-12), uma data real e não futura. Na resposta volta como timestamp ISO (1998-04-12T00:00:00.000Z).
genderenumnãomale, female, other.
preferredLanguagestringnãoTag BCP 47 (pt-BR).
(customizadas)—nãoPela chave da propriedade — interesse_em_bolsa.

Resposta 201:

{
"id": "clz9q2k8r0001s6vp2h4m7x1e",
"properties": {
"name": "Ana Souza",
"email": "ana.souza@exemplo.com",
"phone": "+5511999999999",
"taxId": "12345678901",
"externalId": "aluno-4471",
"interesse_em_bolsa": "Sim"
},
"createdAt": "2026-09-17T13:02:11.482Z",
"updatedAt": "2026-09-17T13:02:11.482Z"
}

A resposta traz só as propriedades preenchidas. Propriedade sem valor não vem como null — ela simplesmente não aparece.

Erros​

HTTPerrorCodeQuando
400—Corpo malformado, nem email nem phone, ou email, taxId ou birthDate inválido.
409CONTACT_IDENTITY_CONFLICTJá existe contato com esse email, taxId ou externalId na instituição. details.property diz qual. Use o lote se a intenção era sincronizar.
422CONTACT_PROPERTY_KEYS_UNKNOWNPropriedade customizada inexistente no catálogo. details.keys lista as chaves.
422PROPERTY_VALUE_NOT_IN_OPTIONSValor fora das opções de um SELECT ou MULTI_SELECT.
422PROPERTY_INVALID_VALUEValor que não serve para o tipo da propriedade. details.reason diz o motivo.

Os erros do gate (401, 403) e o formato de cada resposta estão em Erros.

Atualizar um contato​

PATCH /contacts/{contactId}  ·  síncrono

curl -X PATCH "API_BASE/public/v1/crm/contacts/clz9q2k8r0001s6vp2h4m7x1e" \
-H "Authorization: Bearer wf_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"phone": "+5511988888888",
"interesse_em_bolsa": "Não"
}
}'

É merge, não substituição. Só as chaves enviadas mudam; as demais ficam como estavam. Não existe forma de "zerar o contato" mandando um properties menor.

Para limpar uma propriedade, mande null:

{ "properties": { "interesse_em_bolsa": null } }

Mandar "" grava a string vazia, que é diferente de vazio. A distinção é de propósito: uma integração que envia "" por engano não apaga dado sem deixar rastro.

Exceção: taxId. Aqui "" limpa o documento, igual a null. Para preservar o taxId, omita a chave do corpo; envie "" ou null só quando quiser apagá-lo.

Resposta 200 — o contato inteiro, no mesmo formato do POST.

Erros​

HTTPerrorCodeQuando
400—Corpo malformado, ou email, taxId ou birthDate inválido.
404PROPERTY_CONTACT_NOT_FOUNDO contato não existe, ou é de outra instituição.
409CONTACT_EXTERNAL_ID_MISMATCHO contato já tem outro externalId gravado, diferente do enviado.
422CONTACT_PROPERTY_KEYS_UNKNOWN, PROPERTY_VALUE_NOT_IN_OPTIONS, PROPERTY_INVALID_VALUEOs mesmos do POST.

Endereçar sem saber o id​

Use idProperty para endereçar o contato por uma das propriedades de identidade:

curl -X PATCH "API_BASE/public/v1/crm/contacts/ana.souza@exemplo.com?idProperty=email" \
-H "Authorization: Bearer wf_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{ "properties": { "phone": "+5511988888888" } }'

idProperty aceita externalId, email ou taxId. Escape o valor na URL — um e-mail com + quebra se não for escapado.

Sem idProperty, o segmento da URL é sempre o id do Workfuse.

Ler um contato​

GET /contacts/{contactId}  ·  síncrono

curl "API_BASE/public/v1/crm/contacts/clz9q2k8r0001s6vp2h4m7x1e" \
-H "Authorization: Bearer wf_sua_chave_aqui"

Aceita o mesmo ?idProperty= do PATCH. Responde 404 (PROPERTY_CONTACT_NOT_FOUND) para contato de outra instituição — do seu lado é indistinguível de inexistente, e é de propósito.

Sincronizar em lote (upsert)​

POST /contacts/batch/upsert  ·  assíncrono

É o endpoint da sincronização periódica: existe, atualiza; não existe, cria. Evita o 409 do POST e a ida extra para descobrir o id.

curl -X POST "API_BASE/public/v1/crm/contacts/batch/upsert" \
-H "Authorization: Bearer wf_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{
"id": "aluno-4471",
"idProperty": "externalId",
"properties": { "name": "Ana Souza", "interesse_em_bolsa": "Sim" }
},
{
"id": "bruno.lima@exemplo.com",
"idProperty": "email",
"properties": { "phone": "+5511977777777" }
}
]
}'
CampoTipoObrigatórioDescrição
inputsarraysimDe 1 a 1.000 itens por chamada.
inputs[].idstringsimO valor da propriedade de identidade.
inputs[].idPropertyenumsimexternalId, email ou taxId.
inputs[].propertiesobjectsimAs propriedades a gravar — merge, igual ao PATCH.

Resposta 202 — o lote entrou na fila:

{
"batchId": "clzb4t7x90000s6vp8k2m1q3a",
"accepted": 2,
"receivedAt": "2026-09-17T13:02:11.482Z"
}

O que o 202 quer dizer​

Recebi e vou processar, não pronto. Três consequências que mudam como você integra:

  • Nenhum contato foi criado ainda quando você recebe essa resposta. Não encadeie uma chamada que dependa disso.
  • Para consultar o resultado, use GET /batches/{batchId} — retorna o progresso e o resultado por item (sucesso, erro de validação, conflito de identidade).
  • Para ser notificado contato a contato, assine contact.created e contact.updated. Item que falha ou que não muda nada não dispara.

O prazo, os limites e a política de retentativa estão em Limites e cotas. O resumo: até 5 minutos em condição normal, entrega at-least-once, e reenviar o mesmo lote é seguro e não repete webhook para dados iguais — o idProperty é o que torna o reprocessamento inofensivo.

Consultar o lote​

GET /batches/{batchId}

O batchId é o que veio no 202. O resultado fica disponível por 30 dias.

curl "API_BASE/public/v1/crm/batches/clzb4t7x90000s6vp8k2m1q3a" \
-H "Authorization: Bearer wf_sua_chave_aqui"

O status é um de três. Enquanto o lote processa, a resposta traz o progresso e nada mais:

{
"batchId": "clzb4t7x90000s6vp8k2m1q3a",
"status": "processing",
"total": 2,
"processed": 1,
"createdAt": "2026-10-06T13:02:11.482Z",
"completedAt": null
}

Quando conclui, vem o resultado de cada item, inclusive dos que falharam. Um item com erro não derruba os outros:

{
"batchId": "clzb4t7x90000s6vp8k2m1q3a",
"status": "completed",
"total": 2,
"processed": 2,
"createdAt": "2026-10-06T13:02:11.482Z",
"completedAt": "2026-10-06T13:02:40.100Z",
"results": [
{
"index": 0,
"id": "aluno-4471",
"idProperty": "externalId",
"status": "succeeded",
"operation": "created",
"contactId": "clz9q2k8r0001s6vp2h4m7x1e"
},
{
"index": 1,
"id": "bruno.lima@exemplo.com",
"idProperty": "email",
"status": "failed",
"error": {
"status": 422,
"errorCode": "CONTACT_BATCH_ITEM_INVALID",
"message": "Propriedade inexistente no catálogo da instituição",
"details": { "issues": [] }
}
}
]
}
Campo de results[]Descrição
indexA posição do item no inputs que você enviou, começando em 0.
id, idPropertyO seletor que você enviou, para casar o resultado com o seu registro.
statussucceeded ou failed.
operationcreated ou updated. Só em succeeded.
contactIdO id do contato. Só em succeeded.
errorstatus (HTTP do item), errorCode, message e, quando houver, details. Só em failed.

Se o lote inteiro falha (erro nosso, não de um item), o status é failed, sem results. Reenvie o lote: o idProperty torna o reenvio inofensivo.

HTTPerrorCodeQuando
403—Módulo crm fora do escopo da API key (MODULE_NOT_IN_KEY_SCOPE) ou capability não habilitada (CAPABILITY_NOT_ENABLED).
404CONTACT_BATCH_NOT_FOUNDbatchId inexistente, de outra instituição ou com mais de 30 dias — do seu lado é indistinguível, e é de propósito.

Erros​

accepted conta os itens que passaram na validação de forma. Um item malformado reprova a chamada inteira com 400 — nada entra na fila:

HTTPerrorCodeQuando
400—Algum item sem id, sem idProperty, com corpo malformado, nenhum item ou mais de 1.000.
403—Módulo crm fora do escopo da API key (MODULE_NOT_IN_KEY_SCOPE) ou capability não habilitada (CAPABILITY_NOT_ENABLED).

Erro de negócio de um item (propriedade inexistente, valor fora das opções, contato sem e-mail nem telefone na criação) fica gravado no resultado do item — consulte o lote.

O que esta API não faz​

  • Não apaga contato. Exclusão é operação da plataforma, feita no painel — não há DELETE aqui.
  • Não lista nem busca contatos. GET /contacts com filtro está no roadmap; hoje você endereça um contato por id ou por idProperty.
  • Não cria propriedade customizada. O catálogo é da instituição, em Configurações → Propriedades. Mandar uma chave que não existe lá devolve 422.
  • Não mescla duplicados. Deduplicação é feita no painel.
  • Não define dono. Contato não tem owner na API pública; só o Negócio aceita ownerId.

Se você vem do HubSpot​

No HubSpotAqui
POST /crm/v3/objects/contactsPOST /crm/contacts
PATCH /crm/v3/objects/contacts/{id}PATCH /crm/contacts/{contactId}
POST /crm/v3/objects/contacts/batch/upsert — síncrono, 100 itensPOST /crm/contacts/batch/upsert — assíncrono, 1.000 itens
?idProperty=emailIgual — mas também aceita externalId e taxId
firstname + lastnamename, um campo só
associations[] na criaçãoO vínculo nasce no Negócio: contact no POST /deals
archived: false na respostaNão existe: o Contato não tem arquivamento

Medido em developers.hubspot.com em 17/09/2026.