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:
| Propriedade | O que é |
|---|---|
externalId | O id do contato no seu sistema. É a que você deve preferir |
email | |
taxId | CPF ou CNPJ |
externalIdnã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.
| Propriedade | Tipo | Obrigatório | Descrição |
|---|---|---|---|
email | string | condicional | email ou phone é obrigatório — um contato sem nenhum dos dois não é alcançável. O formato de e-mail é validado. |
phone | string | condicional | E.164 (+5511999999999). Não é único — ver "Identidade" acima. |
name | string | não | Nome completo, um campo só. |
taxId | string | não | CPF 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. |
externalId | string | não | O id do contato no seu sistema. |
externalStudentId | string | não | O id de aluno no seu sistema, quando for diferente do externalId. |
birthDate | string | não | AAAA-MM-DD (1998-04-12), uma data real e não futura. Na resposta volta como timestamp ISO (1998-04-12T00:00:00.000Z). |
gender | enum | não | male, female, other. |
preferredLanguage | string | não | Tag BCP 47 (pt-BR). |
| (customizadas) | — | não | Pela 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
| HTTP | errorCode | Quando |
|---|---|---|
400 | — | Corpo malformado, nem email nem phone, ou email, taxId ou birthDate inválido. |
409 | CONTACT_IDENTITY_CONFLICT | Já existe contato com esse email, taxId ou externalId na instituição. details.property diz qual. Use o lote se a intenção era sincronizar. |
422 | CONTACT_PROPERTY_KEYS_UNKNOWN | Propriedade customizada inexistente no catálogo. details.keys lista as chaves. |
422 | PROPERTY_VALUE_NOT_IN_OPTIONS | Valor fora das opções de um SELECT ou MULTI_SELECT. |
422 | PROPERTY_INVALID_VALUE | Valor 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
| HTTP | errorCode | Quando |
|---|---|---|
400 | — | Corpo malformado, ou email, taxId ou birthDate inválido. |
404 | PROPERTY_CONTACT_NOT_FOUND | O contato não existe, ou é de outra instituição. |
409 | CONTACT_EXTERNAL_ID_MISMATCH | O contato já tem outro externalId gravado, diferente do enviado. |
422 | CONTACT_PROPERTY_KEYS_UNKNOWN, PROPERTY_VALUE_NOT_IN_OPTIONS, PROPERTY_INVALID_VALUE | Os 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" }
}
]
}'
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
inputs | array | sim | De 1 a 1.000 itens por chamada. |
inputs[].id | string | sim | O valor da propriedade de identidade. |
inputs[].idProperty | enum | sim | externalId, email ou taxId. |
inputs[].properties | object | sim | As 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.createdecontact.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 |
|---|---|
index | A posição do item no inputs que você enviou, começando em 0. |
id, idProperty | O seletor que você enviou, para casar o resultado com o seu registro. |
status | succeeded ou failed. |
operation | created ou updated. Só em succeeded. |
contactId | O id do contato. Só em succeeded. |
error | status (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.
| HTTP | errorCode | Quando |
|---|---|---|
403 | — | Módulo crm fora do escopo da API key (MODULE_NOT_IN_KEY_SCOPE) ou capability não habilitada (CAPABILITY_NOT_ENABLED). |
404 | CONTACT_BATCH_NOT_FOUND | batchId 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:
| HTTP | errorCode | Quando |
|---|---|---|
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á
DELETEaqui. - Não lista nem busca contatos.
GET /contactscom filtro está no roadmap; hoje você endereça um contato poridou poridProperty. - 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 HubSpot | Aqui |
|---|---|
POST /crm/v3/objects/contacts | POST /crm/contacts |
PATCH /crm/v3/objects/contacts/{id} | PATCH /crm/contacts/{contactId} |
POST /crm/v3/objects/contacts/batch/upsert — síncrono, 100 itens | POST /crm/contacts/batch/upsert — assíncrono, 1.000 itens |
?idProperty=email | Igual — mas também aceita externalId e taxId |
firstname + lastname | name, um campo só |
associations[] na criação | O vínculo nasce no Negócio: contact no POST /deals |
archived: false na resposta | Não existe: o Contato não tem arquivamento |
Medido em developers.hubspot.com em 17/09/2026.