Introducao
A API do ZapDesk permite integrar contatos, deals, conversas, mensagens e automacoes com qualquer sistema externo. Use API Keys por workspace para autenticar seus requests e webhooks de saida para reagir a eventos em tempo real.
Base URL
https://app.vennocrm.com.br/api/v1/- Acesse Configuracoes > Integracoes > API Keys.
- Gere uma nova chave com as permissoes necessarias.
- Guarde a chave criada em um cofre seguro. Ela so aparece uma vez.
- Envie a chave no header Authorization: Bearer zdk_live_xxxxx.
Autenticacao
Toda rota da API publica usa API Key. Sessao Supabase e cookies do app nao sao aceitos em /api/v1/.
curl --request GET \ --url https://app.vennocrm.com.br/api/v1/contacts?page=1&limit=20 \ --header "Authorization: Bearer zdk_live_xxxxx"
401 INVALID_API_KEY
A chave e invalida, foi revogada ou expirou.
403 INSUFFICIENT_PERMISSIONS
A chave nao tem as permissoes necessarias para a operacao.
403 INSUFFICIENT_SCOPE
A rota exige um escopo criado depois da chave (ver Escopos). A mensagem diz qual falta. A saida e gerar uma chave nova com ele marcado.
Escopos
Toda rota exige um acesso (read ou write) mais o assunto (contacts, products...). Os dois sao marcados na criacao da chave, em Configuracoes > Integracoes.
Chave antiga precisa de escopo novo
Uma chave so tem os escopos que existiam quando foi criada. Os escopos de 09/10/2026 (products, custom_fields, tags, team:read, tasks, portfolio:read, portfolio:admin) nao entram sozinhos nas chaves anteriores: a rota responde 403 INSUFFICIENT_SCOPE dizendo qual falta. Gere uma chave nova com o escopo marcado e troque no integrador. Tudo que a chave antiga fazia continua funcionando igual.
Acesso
O que a chave pode fazer. Toda rota exige um destes mais o assunto.
readConsultar registros.writeCriar, editar e desativar registros.
Atendimento e CRM
Contatos, negócios, conversas, tarefas e webhooks.
contactsCadastro de contatos, notas e etiquetas aplicadas.dealsNegócios, funis e movimentação de etapa.messagesConversas, envio de mensagem e canais.webhooksWebhooks de saída do workspace.tasksListar, criar e concluir tarefas.
Cadastro
O que um ERP ou conector precisa para preparar o workspace.
productsCatálogo de produtos, importação por SKU e listas de preço.custom_fieldsDefinições de campo de contato e de negócio.tagsCriar, editar e apagar etiquetas. Aplicar continua em Contatos e Negócios.
Equipe
Quem trabalha aqui, só leitura.
team:readMembros e equipes, sem senha, token ou permissão detalhada.
Carteira Ativa
Compras, financeiro e situação de cada cliente.
purchases(so com o modulo ligado)Enviar compras e posição financeira do ERP.portfolio:read(so com o modulo ligado)Situação dos clientes, compras, financeiro, relatórios e quarentena.portfolio:admin(so com o modulo ligado)Apagar registros em quarentena.
Pedidos
Ler, criar, enviar, operar e decidir alçada em nome de quem criou a chave (ou de actor_email).
orders(so com o modulo ligado)Listar, ler, criar, editar, simular, enviar e cancelar os próprios pedidos; catálogo com o preço do cliente.orders:manage(so com o modulo ligado)Ver todos, aceitar, bloquear, faturar e cancelar pedidos de terceiros; receber o resultado do ERP.orders:approve(so com o modulo ligado)Liberar e rejeitar pedido fora da alçada, em nome de um aprovador (actor_email).
Venno Campo
Visitas do vendedor de rua: check-in, check-out, tempo no cliente e resultado.
visits:read(so com o modulo ligado)Visitas de campo com check-in, check-out, local, tempo no cliente, resultado e motivo.
Relatórios
Em breve.
reports:read(reservado, em breve)Dashboard e relatórios.
Agenda
Em breve.
appointments(reservado, em breve)Agendamentos.
Rate Limiting
Cada API Key pode fazer ate 100 requests por minuto e 1000 por hora.
X-RateLimit-Limitmostra o teto aplicado na janela.X-RateLimit-Remainingmostra quantas requisicoes ainda restam.X-RateLimit-Resetinforma quando a janela atual sera reiniciada.- Ao exceder o limite, a API responde
429 RATE_LIMIT_EXCEEDED.
Formato de Resposta
Sucesso
{
"success": true,
"data": {},
"pagination": {
"page": 1,
"limit": 20,
"total": 125
}
}Erro
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API Key invalida ou expirada"
}
}Paginacao e sincronizacao
As listas antigas (/contacts, /deals, /conversations) paginam por page e limit e devolvem total. As listas que crescem com o ERP (produtos e as que vierem) paginam por cursor: nao contam, nao pulam nem repetem linha quando alguem grava no meio da leitura.
Cursor
Peca a primeira pagina sem cursor. A resposta traz pagination.next_cursor; mande-o de volta em ?cursor= ate vir null. O cursor e opaco: guarde e devolva como veio. limit vai de 1 a 100 (padrao 50); fora da faixa a resposta e 400 INVALID_LIMIT, e cursor corrompido e 400 INVALID_CURSOR.
updated_since
Nas listas com cursor, ?updated_since=2026-10-01T00:00:00Z devolve so o que mudou desde entao, em ordem crescente de updated_at. Para sincronizar, guarde o maior updated_at que recebeu e volte com ele na proxima rodada. Data que nao se le e 400 INVALID_UPDATED_SINCE.
Lotes
Rotas de lote aceitam ate 500 itens por chamada e respondem received, inserted, updated e rejected[], cada rejeitado com o index que tinha no corpo e o motivo. Um item torto nunca derruba os outros. Lote vazio, acima do teto ou sem a lista: 422 com EMPTY_BATCH, BATCH_TOO_LARGE ou INVALID_BODY.
{
"success": true,
"data": [ ... ],
"pagination": {
"limit": 50,
"next_cursor": "eyJ0IjoiMjAyNi0xMC0wOVQxMjowMDowMC4xMjM0NTYrMDA6MDAiLCJpIjoiLi4uIn0",
"has_more": true
}
}Sincronizacao incremental nas listas antigas
Desde 10/10/2026, GET /contacts e GET /deals tambem sincronizam por delta, e PATCH /contacts/:id e PATCH /deals/:id aceitam o carimbo de versao. Tudo aditivo: quem nao manda os parametros novos recebe exatamente a resposta de antes (page, limit, total).
updated_since liga o modo cursor
Mande ?updated_since=2026-10-01T00:00:00Z (ou ?cursor=) em /contacts ou /deals e a lista passa a vir em ordem crescente de updated_at, sem total, com pagination.next_cursor e has_more, como nas listas novas (Paginacao). Os filtros de sempre (search, tag, source, pipeline_id, stage_id) valem nos dois modos. Para a carga inicial sem recorte, use updated_since=1970-01-01T00:00:00Z. No modo cursor, limit vai de 1 a 100 (padrao 50) e fora da faixa e 400 INVALID_LIMIT.
expected_updated_at (409)
Opcional no corpo do PATCH /contacts/:id e do PATCH /deals/:id: devolva o updated_at que voce leu. Se o registro mudou desde entao, nada e gravado e a resposta e 409 RESOURCE_CONFLICT: releia e reenvie. Sem o campo, o ultimo a gravar vence, como sempre foi. A resposta do PATCH ja traz o updated_at final (inclusive depois de aplicar etiquetas), e e esse que deve voltar na proxima chamada.
Precisao: o banco guarda microssegundos (2026-10-09T12:00:00.123456+00:00), mas o carimbo vale com a precisao que voce mandar. .123Z (milissegundo, o toISOString() do JavaScript) ou 12:00:00Z (sem fracao) casam com o registro cujo updated_at comeca assim; mais de seis casas sao arredondadas para o microssegundo. Mande ISO 8601 com hora; sem fuso, UTC e assumido.
Rotina de sincronizacao
1) Leia /contacts?updated_since=<ultimo> ate has_more: false, seguindo o next_cursor. 2) Guarde o maior updated_at recebido e use-o como updated_since da proxima rodada. 3) Para ser avisado em vez de perguntar, assine os webhooks; o payload de cada um traz o id, e a lista com updated_since traz o registro inteiro.
GET /contacts?updated_since=2026-10-09T00:00:00Z&limit=100
GET /contacts?cursor=eyJ0IjoiMjAyNi0xMC0wOVQxMjowMDowMC4xMjM0NTYrMDA6MDAiLCJpIjoiLi4uIn0
PATCH /deals/:id
{ "value": 1500, "expected_updated_at": "2026-10-09T12:00:00.123456+00:00" }
HTTP 409 { "success": false, "error": { "code": "RESOURCE_CONFLICT", "message": "..." } }Endpoints - Contatos
GET /contacts
Lista contatos com pagina, busca por nome/telefone, tag e origem. Parametros: page, limit, search, tag, source. Cada contato traz o endereço em address (ver Endereço do contato).
curl --request GET \ --url 'https://app.vennocrm.com.br/api/v1/contacts?search=maria&tag=vip&limit=50' \ --header 'Authorization: Bearer zdk_live_xxxxx'
GET /contacts/:id
Retorna um contato completo, incluindo custom_fields e o endereço em address (o endereço e o ponto confirmado do cliente, ou null).
GET /contacts/search?phone=5511999998888
Busca um contato pelo telefone normalizado dentro do workspace autenticado.
POST /contacts
Cria contatos com tags, origem, custom_fields e o responsável pela carteira em owner_email. Se o telefone já existe, devolve o contato como está, sem aplicar owner_email nem os demais campos: o POST nunca altera contato existente. Para definir ou trocar o responsável de um contato que já existe, use o PATCH /contacts/:id. O owner_email é conferido nos dois casos, então um e-mail inválido volta 422 mesmo quando o contato já existe. A resposta traz address: null no contato novo e o endereço gravado no contato que já existia. O POST não grava endereço; use o PATCH. Se o corpo trouxer address, ele é ignorado e a resposta de sucesso traz "warnings": ["address é ignorado no POST; use PATCH /v1/contacts/:id para gravar o endereço."].
{
"phone": "5511999998888",
"name": "Maria Silva",
"email": "maria@empresa.com",
"tags": ["vip", "site"],
"source": "site",
"owner_email": "vendedor@empresa.com",
"custom_fields": {
"matricula": "2026-104",
"data_vencimento": "2026-05-10"
}
}PATCH /contacts/:id
Atualiza contato com merge dos campos enviados, inclusive merge profundo em custom_fields. tags acrescenta etiquetas (envie "tags_mode": "replace" para substituir a lista) e etiqueta nova aciona as automações de "contato recebeu etiqueta". address grava o endereço, também parcial (ver Endereço do contato), e a resposta sai com ele.
Responsável pela carteira (owner_email)
Aceito no POST /contacts e no PATCH /contacts/:id. É o e-mail de login de um membro ativo da equipe (maiúsculas e espaços nas pontas são ignorados). Envie null para deixar o contato sem responsável; sem o campo, o responsável atual não muda. E-mail que não é de um membro ativo é recusado com 422 OWNER_NOT_MEMBER e nada é gravado, e e-mail mal formado volta como 422 INVALID_OWNER. É o que permite ao ERP sincronizar a carteira de cada vendedor. No POST, só vale para contato novo; em contato que já existe, use o PATCH.
PATCH /contacts/:id
{ "owner_email": "vendedor@empresa.com" }
PATCH /contacts/:id
{ "owner_email": null }Endereço do contato (address)
Vem em todo contato de GET /contacts, GET /contacts/:id, POST /contacts e PATCH /contacts/:id: cep (8 números, sem traço), rua, numero, complemento, bairro, cidade, uf (a sigla do estado), ibge (o código do município, 7 números) e o ponto confirmado do cliente em lat, lng, precisao (exata) e fonte_do_ponto (ajuste_manual, checkin_confirmado, importacao ou api); os quatro vêm null quando o cliente não tem ponto confirmado. Contato sem endereço vem com address: null.
No PATCH /contacts/:id, address é parcial: o campo enviado muda, o ausente continua como está e o enviado como null é apagado. "address": null apaga o endereço inteiro, com o ponto. lat e lng vão juntas e dentro do Brasil (latitude de -34 a 6, longitude de -74 a -28) e gravam o ponto como exato (precisao: "exata") com fonte_do_ponto: "api"; reenviar o ponto que já está gravado não muda a origem dele, e sem lat e lng o ponto não muda. precisao e fonte_do_ponto são só de leitura: podem voltar no PATCH e são ignorados.
Campo inválido (CEP, UF, código IBGE, texto maior que o permitido, só lat sem lng, ponto fora do Brasil) ou desconhecido volta 422 INVALID_ADDRESS com a lista dos campos, e nada é gravado; endereço que ficaria sem CEP, rua, cidade e ponto também. Se o ambiente ainda não tem o endereço do contato, o PATCH com address volta 503 ADDRESS_PENDING sem gravar nada; sem address, funciona como sempre.
PATCH /contacts/:id
{ "address": { "cep": "87040-250", "numero": "123", "complemento": "Sala 2" } }
PATCH /contacts/:id
{ "address": { "lat": -23.4205, "lng": -51.9331 } }
PATCH /contacts/:id
{ "address": null }DELETE /contacts/:id
Remove o contato do workspace autenticado.
POST /contacts/:id/tags
Adiciona tags ao contato sem sobrescrever as existentes.
POST /contacts/:id/notes
Cria uma nota vinculada ao contato.
Endpoints - Deals
GET /deals
Filtre por pipeline_id, stage_id, pagina e limite.
GET /deals/:id
Retorna deal com contato, etapa e custom_fields.
POST /deals
Cria deal, dispara o webhook deal.created e as automações de "negócio criado". Criado direto numa etapa de ganho ou perda, já nasce fechado — aceita lost_reason opcional. assigned_to precisa ser membro do workspace (senão 404 ASSIGNEE_NOT_FOUND).
PATCH /deals/:id
Atualiza titulo, valor, tags, origem e custom_fields. tags acrescenta etiquetas; para substituir a lista inteira (ou limpar, com []), envie "tags_mode": "replace". Trocar o assigned_to avisa o novo responsável.
PATCH /deals/:id/move
Move o deal entre etapas, registra historico e dispara deal.moved e as automações de "entrou na etapa". Etapa de ganho fecha o negócio e aciona as automações de "negócio ganho"; etapa de perda fecha como perdido (envie lost_reason; sem ele fica "Fechado pela integração"); voltar para uma etapa comum reabre.
Endpoints - Pipelines
GET /pipelines
Retorna pipelines do workspace com suas respectivas etapas ordenadas.
Conversas e Mensagens
GET /conversations
Lista conversas por status, responsavel, pagina e limite. Parametros: status, assigned_to, page, limit.
GET /conversations/:id/messages
Retorna as mensagens da conversa com suporte a paginacao.
GET /channels
Lista os canais do workspace autenticado com id, display_name, provider e status. Use o id como channel_id no envio. Apenas canais com status: "connected" conseguem enviar. Requer escopos read + messages. Nao expoe tokens do provedor.
{
"success": true,
"data": {
"channels": [
{ "id": "uuid", "display_name": "Vendas", "provider": "uazapi", "status": "connected" }
]
}
}POST /messages/send
Envia mensagem WhatsApp e dispara message.sent. Registra a mensagem no historico da conversa (cria contato e conversa automaticamente se nao existirem). Requer escopos write + messages.
Passe channel_id para escolher o canal de envio (recomendado quando o workspace tem mais de um canal). Sem channel_id, usa o ultimo canal conectado do workspace. message_type aceita text, image, audio, video ou document; para midia, content recebe a URL publica do arquivo.
{
"channel_id": "uuid-do-canal",
"phone": "5511999998888",
"content": "Ola! Seu pedido saiu para entrega.",
"message_type": "text"
}POST /conversations/:id/typing
Liga o "digitando…" na conversa. Serve para quem responde por integracao propria e leva alguns segundos para formular a resposta — sem isso o contato fica olhando uma tela parada. Requer escopos write + messages.
Nao envia mensagem e nao grava nada no historico: e um sinal efemero. O indicador expira sozinho em ate 25s no WhatsApp e ~20s no Messenger; para manter alem disso, chame de novo (a cada ~8s e o intervalo seguro, inclusive no Instagram).
Disponivel nos canais oficiais (WhatsApp, Instagram, Messenger). Em canal de outro tipo a resposta e 200 com sent: false, nunca erro — o indicador e opcional e nao deve quebrar o seu fluxo. No WhatsApp ele tem um efeito colateral da Meta: marca a ultima mensagem recebida como lida.
{
"success": true,
"data": { "sent": true }
}GET /tags
Lista todas as tags distintas usadas no workspace autenticado.
Endpoints - Compras (Carteira Ativa)
O ERP (ou o forca de vendas) envia as compras de cada cliente, e o Venno calcula a recompra: ultima compra, intervalo medio, proxima compra prevista, situacao (novo, ativo, em risco, inativo, sem compra), valor faturado no periodo e mix de produtos. Exige o modulo Carteira Ativa ligado no workspace (senao 403 MODULE_DISABLED) e uma API Key com as permissoes write e purchases ("Compras"). As chaves existentes nao ganham acesso sozinhas.
POST /purchases
Lote de 1 a 500 compras, com ate 50 itens cada; o corpo inteiro precisa caber em 4,5 MB (com raw grande, mande lotes menores). Idempotente por source + external_id: reenviar a mesma nota atualiza (valor, status e itens sao substituidos), nunca duplica. kind: invoice (padrao), order ou return (devolucao, valor negativo ou zero; valor negativo em outro tipo e recusado). status: invoiced (padrao da nota), open (padrao do pedido) ou canceled. Pedido em aberto conta como atividade, mas nao como valor; cancelada, valor zero e devolucao nao contam como compra. Data mais de 7 dias no futuro e recusada.
Como o cliente e encontrado
Nesta ordem: external_customer_id no campo personalizado erp_id do contato; depois customer_document (CPF/CNPJ, com ou sem mascara) nos campos do tipo CPF/CNPJ; depois customer_phone. Quem nao for encontrado volta em orphans. Se o contato achado por documento ou telefone ja tiver outro erp_id, a compra volta como orfa com motivo conflict; se nao tiver nenhum, o erp_id enviado e gravado nele. Com "create_missing": true e telefone, o contato e criado (sem disparar automacoes de boas-vindas).
Pedido e nota da mesma venda
Quando o pedido e faturado, faca UMA de duas coisas: reenvie o pedido com o mesmo external_id e "kind": "invoice", "status": "invoiced"; ou mande a nota com order_external_id apontando para o pedido. Sem isso, pedido e nota contariam como duas compras. Por seguranca, um pedido em aberto seguido de nota do mesmo cliente em ate 10 dias tambem e tratado como a mesma venda.
Numeros
Envie valores como numero JSON (1520.5). Texto so e aceito sem separador de milhar e com ponto ou virgula decimal ("1520.50", "1520,50"); "1.234", "1,234.56" e "1.234,56" sao recusados, porque o separador e ambiguo.
POST /customers/financials
Lote de 1 a 500 clientes com credit_limit, credit_available, overdue_amount, overdue_count e oldest_overdue_at. Cada envio substitui a foto anterior do cliente. Mesmo casamento de cliente; aqui nunca se cria contato. Dois itens do lote para o mesmo cliente: vale o ultimo, e o descartado volta em rejected.
Erros por item
Uma compra invalida nao derruba o lote: volta em rejected com o indice e o motivo, e as outras sao gravadas. O mesmo vale para a linha que o banco recusar por limite de coluna. O lote inteiro so e recusado (422) quando o corpo nao tem a lista ou passa de 500 itens.
curl --request POST \
--url https://app.vennocrm.com.br/api/v1/purchases \
--header "Authorization: Bearer zdk_live_xxxxx" \
--header "Content-Type: application/json" \
--data '{
"create_missing": false,
"purchases": [{
"external_id": "NF-000123",
"source": "api",
"kind": "invoice",
"status": "invoiced",
"purchased_at": "2026-10-01",
"total_value": 1520.50,
"order_external_id": "PED-9981",
"external_customer_id": "CLI-0042",
"customer_document": "12.345.678/0001-90",
"customer_phone": "44999990000",
"customer_name": "Mercado Exemplo",
"seller_external": "V01",
"seller_email": "vendedor@empresa.com.br",
"items": [
{ "sku": "CX-10", "description": "Caixa 10 kg", "quantity": 12, "unit": "CX", "unit_price": 120, "total": 1440 },
{ "sku": "FRETE", "description": "Frete", "total": 80.50 }
]
}]
}'{
"success": true,
"data": {
"received": 1,
"inserted": 1,
"updated": 0,
"orphans": [],
"rejected": [],
"contacts_created": 0,
"duplicates_in_batch": 0,
"items_written": 2,
"recomputed": 1
}
}{
"customers": [{
"external_customer_id": "CLI-0042",
"credit_limit": 20000,
"credit_available": 4300,
"overdue_amount": 1250.00,
"overdue_count": 2,
"oldest_overdue_at": "2026-09-10"
}]
}Endpoints - Produtos e listas de preco
Catalogo do workspace: o que entra em proposta e em pedido. Escopo products (mais read ou write). Preco sempre em price_cents (inteiro) na resposta; na escrita, price_cents ou price decimal ("1234.56", "1.234,56" ou 1234.56), lido pela mesma regra da tela. O SKU e unico por workspace, sem diferenciar maiusculas.
GET /products
Lista por cursor, em ordem crescente de updated_at. Filtros: q (nome ou SKU), category, active (true por padrao, false, all; outro valor e 400 INVALID_ACTIVE) e updated_since.
GET /products/:id
Um produto. 404 PRODUCT_NOT_FOUND quando nao e deste workspace.
POST /products
Campos: name (obrigatorio, ate 200), sku (ate 60), description, category, unit (padrao un), price_cents ou price, currency (padrao BRL), is_active. SKU repetido: 409 SKU_ALREADY_EXISTS.
PATCH /products/:id
Edicao parcial: so o que vier no corpo muda. Mudar o preco nao altera proposta nem pedido ja feitos. Corpo vazio: 400 INVALID_INPUT.
DELETE /products/:id
Desativa, nunca apaga: propostas e pedidos antigos continuam apontando para o produto. Reativar e PATCH com is_active: true.
GET /price-lists
Listas de preco (id, nome, moeda, padrao, ativa). Exige o modulo Pedidos ligado, senao 403 MODULE_DISABLED. active: true por padrao, false ou all. So leitura.
POST /products/import
Lote de ate 500 produtos, casados pelo sku. SKU que existe e atualizado (so os campos enviados); que nao existe e criado, e ai name e obrigatorio. Mandar o mesmo lote de novo da inserted 0, updated N: e seguro sincronizar o catalogo inteiro do ERP a cada rodada. Linha torta volta em rejected com o index do corpo (SKU_REQUIRED, NAME_REQUIRED, INVALID_PRICE, INVALID_NAME, INVALID_SKU, INVALID_ITEM, PRODUCT_WRITE_ERROR) sem derrubar as outras. O mesmo SKU duas vezes no lote: vale a ultima, contada em duplicates_in_batch. Cada chamada que gravou algo dispara um products.imported (resumo e lista de id, sku e action), nunca um product.updated por produto.
Exemplo
curl --request POST \
--url https://app.vennocrm.com.br/api/v1/products/import \
--header "Authorization: Bearer zdk_live_xxxxx" \
--header "Content-Type: application/json" \
--data '{
"products": [
{ "sku": "PIC-1", "name": "Picanha", "price": "75,00", "unit": "kg", "category": "Cortes" },
{ "sku": "ALC-1", "name": "Alcatra", "price_cents": 4000, "unit": "kg" },
{ "sku": "OLD-9", "is_active": false }
]
}'Resposta
{
"success": true,
"data": {
"received": 3,
"inserted": 2,
"updated": 1,
"rejected": [],
"duplicates_in_batch": 0
}
}Erros
400 INVALID_INPUT, 404 PRODUCT_NOT_FOUND, 409 SKU_ALREADY_EXISTS, 503 CATALOG_NOT_MIGRATED (o catalogo ainda nao foi habilitado no ambiente; nao e falha passageira, nao adianta repetir) e 422 nos lotes invalidos.
Endpoints - Campos personalizados
As definicoes de campo de contato e de negocio (nome interno, rotulo, tipo, opcoes, secao). E como o conector descobre erp_id, dia_de_atendimento ou tabela_de_preco antes de gravar. Escopo custom_fields (mais read ou write). Os valores continuam em custom_fields de cada contato ou deal (ver Valores em custom_fields).
GET /custom-fields
?entity_type=contact (padrao) ou deal. Devolve as definicoes em ordem de posicao: id, name (a chave em custom_fields), label, field_type, options, group_label, is_required, default_value, position. Outro valor: 400 INVALID_ENTITY_TYPE.
POST /custom-fields
Obrigatorios: entity_type, label, field_type (um dos tipos abaixo). name e opcional: sem ele, vem do rotulo ("Tabela de preco" vira tabela_de_preco), ate 60 caracteres. options (lista de texto, para select e multiselect), group_label (secao, ate 60), is_required, default_value, is_visible_in_list, is_visible_in_card. Nome interno repetido na mesma entidade: 409 CUSTOM_FIELD_DUPLICATE.
PATCH /custom-fields/:id
Edicao parcial de label, field_type, options, group_label (null devolve a secao padrao), is_required, default_value, position e visibilidades. Nome interno e entidade nao mudam: o nome e a chave dos valores ja gravados. Tipo desconhecido: 400 INVALID_FIELD_TYPE; campo de outro workspace: 404 CUSTOM_FIELD_NOT_FOUND.
DELETE /custom-fields/:id
Apaga a definicao, como o painel. Nao apaga os valores: eles ficam gravados em cada contato ou negocio, fora da tela e das validacoes, e voltam se um campo com o mesmo nome interno for criado de novo.
Tipos
text, number, date, datetime, boolean, select, multiselect, url, email, phone, cpf, cnpj e currency.
Endpoints - Equipe
Quem trabalha no workspace, so leitura, escopo team:read (mais read). E como o conector descobre o id para assigned_to e o e-mail para owner_email. Nunca devolve senha, token, permissao detalhada, avatar nem status online. Convidar, desativar ou mudar papel continua so pelo painel.
GET /team/members
id (o id da pessoa, o que assigned_to e owner_id recebem), workspace_member_id, full_name, email, role (owner, admin, agent), role_name (papel customizado ativo, ou null), is_active e visibility_scope (own, team, all). active: true por padrao, false ou all (outro valor e 400 INVALID_ACTIVE). Quem saiu segue em all com is_active: false, para o ERP reconhecer um vendedor antigo numa venda antiga.
GET /team/teams
Equipes (setores): id, name, slug, color, description, is_active, members (id, nome, e-mail) e member_count. active como acima.
Endpoints - Etiquetas
O catálogo de etiquetas do workspace (id, nome, slug, cor, uso). Criar, renomear e apagar exigem write e o escopo tags. Aplicar e remover em contato continua com contacts, e em negócio com deals. A chave age com o papel de quem a criou: a opção "só administradores gerenciam etiquetas" do workspace vale para ela também, e apagar é sempre de owner/admin. Se quem criou a chave sair do workspace (ou deixar de ser admin), a chave passa a agir como agente e estas escritas respondem 403 TAG_PERMISSION_DENIED: gere outra chave com um administrador atual.
GET /tags
Mudou a fonte, não o formato. Até 09/10/2026 a lista vinha dos nomes gravados nos contatos; agora vem do catálogo, então aparecem também as etiquetas que só estão em negócios ou ainda não foram aplicadas, e a grafia é a do catálogo (quem criava "cliente-vip" e "Cliente VIP" por fora gerava duplicata que o catálogo casava depois). A resposta padrão continua sendo a lista de nomes em ordem alfabética. Com ?format=full vêm id, slug, color e usage (contagem por conversas, contatos e negócios), que é o que PATCH e DELETE precisam. Aceita chave com contacts (as de sempre) ou com tags.
POST /tags
{ "name": "Cliente VIP", "color": "#B45309" }. A cor é opcional (#RRGGBB; sem ela, uma da paleta). O nome é normalizado por slug: "CLIENTE VIP" e "Cliente VIP" são a mesma etiqueta, e a segunda tentativa responde 409 TAG_DUPLICATE dizendo qual já existe. Responde 201 com a etiqueta criada.
PATCH /tags/:id e DELETE /tags/:id
Renomear (name) reprojeta o nome novo em todos os contatos e negócios que usam a etiqueta, numa transação só; recolorir (color) idem. Apagar leva junto todos os vínculos, em conversas, contatos e negócios ao mesmo tempo, e não tem desfazer: leia o usage antes. 404 TAG_NOT_FOUND para id que não é deste workspace.
DELETE /contacts/:id/tags/:tag e DELETE /deals/:id/tags/:tag
O par de POST /contacts/:id/tags (que só acrescenta) e de tags em PATCH /deals/:id. :tag é o id do catálogo ou o nome (URL-encoded; grafia casada por slug). Idempotente: tirar o que não estava aplicado responde 200 com removed: false. A resposta traz a etiqueta e o registro já reprojetado; o webhook contact.updated/deal.updated só dispara quando algo saiu.
GET https://app.vennocrm.com.br/api/v1/tags?format=full
{
"success": true,
"data": [
{ "id": "6f1c...", "name": "Cliente VIP", "slug": "cliente-vip", "color": "#B45309",
"created_at": "2026-09-01T12:00:00+00:00", "updated_at": "2026-09-01T12:00:00+00:00",
"usage": { "conversations": 3, "contacts": 41, "deals": 7 } }
]
}Endpoints - Tarefas
Tarefas pela chave, com escopo tasks (read para listar e ler, write para criar, editar e concluir). Os alertas "passou do dia de pedir" da Carteira Ativa nascem como tarefa, e a régua externa precisa lê-los. Criar, editar e concluir passam pelos mesmos caminhos da tela: lembrete na fila, recorrência, webhooks task.created/task.completed/task.assigned, notificação ao responsável e Motor de Regras. O responsável vai e volta por assigned_email (membro ativo), nunca por id.
GET /tasks
Pagina por cursor (limit até 100), em ordem de updated_at, com updated_since para o delta. Filtros: status (pending, completed, canceled), assigned_email (ou none para as sem responsável), contact_id, deal_id, due_after e due_before (instante ISO, ou AAAA-MM-DD lido como o dia inteiro no fuso do workspace). A chave vê todas as tarefas do workspace.
GET /tasks/:id
A tarefa com o mínimo do vínculo (contact: id, nome, telefone; deal: id, título) e assigned_email. 404 TASK_NOT_FOUND para id que não é deste workspace.
POST /tasks
Obrigatórios: title e due_at (ISO 8601). Opcionais: description, task_type (call, email, meeting, follow_up, whatsapp, other), assigned_email, contact_id, deal_id (o contato do negócio é herdado se não vier), reminder_minutes_before (0 a 43200), recurrence_frequency e recurrence_interval. Contato ou negócio de outro workspace é 404; e-mail que não é de membro ativo é 422 OWNER_NOT_MEMBER. Valor torto em qualquer campo responde 422 com o código do campo, nunca cai no padrão em silêncio. Responde 201. O criador registrado é quem criou a chave.
Idempotency-Key
Header opcional em POST /tasks, no mesmo padrão de POST /messages/send: a resposta fica guardada por 24 horas, por workspace, e a retransmissão por timeout com a mesma chave devolve a mesma tarefa em vez de criar outra. Depende do Redis do ambiente: sem ele, o header é ignorado (a chamada funciona, mas não deduplica). Use um valor seu e estável, como o id do pedido que originou a tarefa.
PATCH /tasks/:id
Só o que vier no corpo muda; null em assigned_email, contact_id ou deal_id tira o vínculo. status aceita completed (mesmos efeitos de /complete), canceled (com cancel_reason) e pending (reabre e rearma o lembrete). Mudar o prazo, a antecedência ou o responsável também rearma o lembrete. Opcional: expected_updated_at com o updated_at que você leu; se alguém salvou no meio, responde 409 RESOURCE_CONFLICT em vez de sobrescrever.
POST /tasks/:id/complete
Conclui a tarefa: cria a próxima ocorrência se for recorrente, dispara task.completed e acorda o Motor de Regras. Idempotente: concluir de novo devolve a tarefa sem recriar nem redisparar nada. Quem pode mexer segue a regra do painel com o papel de quem criou a chave: admin e owner mexem em tudo; agente, só na tarefa que é dele, que ele criou ou que não tem dono (403 TASK_FORBIDDEN). Chave criada por admin mexe em tudo; se esse admin sair do workspace, a chave passa a agir como agente e deixa de concluir tarefa alheia: gere outra chave com um administrador atual.
curl --request POST \
--url https://app.vennocrm.com.br/api/v1/tasks \
--header "Authorization: Bearer zdk_live_xxxxx" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: pedido-9981-retorno" \
--data '{
"title": "Ligar para o Mercado Exemplo: passou do dia de pedir",
"description": "Ultima compra em 10/09. Proxima prevista era 02/10.",
"task_type": "call",
"due_at": "2026-10-15T14:00:00-03:00",
"assigned_email": "vendedor@empresa.com.br",
"contact_id": "8c0b...",
"reminder_minutes_before": 30
}'{
"success": true,
"data": {
"id": "2a7e...", "title": "Ligar para o Mercado Exemplo: passou do dia de pedir",
"description": "Ultima compra em 10/09. Proxima prevista era 02/10.",
"task_type": "call", "status": "pending", "due_at": "2026-10-15T17:00:00.000Z",
"completed_at": null, "canceled_at": null, "cancel_reason": null,
"assigned_to": "4d2f...", "assigned_email": "vendedor@empresa.com.br",
"contact_id": "8c0b...", "contact": { "id": "8c0b...", "name": "Mercado Exemplo", "phone": "5544999990000" },
"deal_id": null, "deal": null, "template_id": null, "parent_task_id": null,
"reminder_minutes_before": 30, "recurrence_frequency": null, "recurrence_interval": 1,
"created_by": "b1a9...", "created_at": "2026-10-09T13:00:00.000Z", "updated_at": "2026-10-09T13:00:00.000Z"
}
}Endpoints - Carteira Ativa (leitura)
A leitura da Carteira Ativa pela chave: a tela Carteira, o resumo por dono, o painel de compras de cada cliente, a posição financeira, os relatórios, os limiares e a quarentena. Exige o módulo Carteira Ativa ligado (403 MODULE_DISABLED, falha fechado) e uma chave com read e portfolio:read. Enquanto o módulo ainda está sendo preparado no ambiente, as rotas respondem 503 CARTEIRA_ATIVA_PENDING: trate como "tente mais tarde", não como erro. Tudo é agregado em SQL; nada aqui cresce com o tamanho da carteira.
GET /portfolio/customers
A lista da tela Carteira, com os mesmos filtros: situacao (lista separada por vírgula: novo, ativo, em_risco, inativo, sem_compra), dono (all, padrão; none = sem dono; ou o id do membro), owner_email (resolve o dono pelo e-mail, vence dono), dia (valor do campo de dia de atendimento), atrasados=1, q (busca por nome, telefone ou documento) e ordem (proxima, valor, dias, nome). Cada linha traz situação (status), primeira e última compra, próxima prevista, dias sem compra, quantidade e valor, intervalo médio, potencial do mês, crédito, vencidos, dia de atendimento, late e o dono por id e e-mail. Pagina por page e limit (25, 50 ou 100), com total: a chave não tem "minha carteira", então lê como gestor.
GET /portfolio/summary
O resumo por dono da tela: por responsável, quantos clientes em cada situação, quantos atrasados, limite e disponível somados, valor e clientes com título vencido; mais a soma de tudo em total. Mesmos filtros de dono, dia e q.
GET /contacts/:id/purchases/summary
A aba "Compras" da ficha: metrics (situação refeita para hoje, primeira e última compra, intervalo médio, próxima prevista, dias sem compra, valor na janela e potencial), financials, mix (produtos por valor e por frequência) e recent_purchases (as 10 últimas, com itens). 404 CONTACT_NOT_FOUND se o contato não é deste workspace.
GET /contacts/:id/financials
A posição financeira atual (a foto que POST /customers/financials grava): limite, disponível, valor e quantidade de vencidos, vencido mais antigo, origem e synced_at. 404 FINANCIALS_NOT_FOUND quando o ERP nunca mandou nada deste cliente.
GET /portfolio/reports/:tipo
Os relatórios da tela, com os mesmos parâmetros: safras (churn e retenção por safra mensal; meses, 1 a 24), ltv, faturado (por vendedor e por produto; inicio, fim, page, 20 por página) e relacionamento (grupo: compra_sem_conversa, conversa_sem_compra, compra_e_conversa, nenhum; 50 por página). Período padrão: do dia 1º do mês até hoje; máximo de dois anos. A resposta traz request (o pedido como foi lido), ltv_window_days e data.
GET /portfolio/settings e GET /portfolio/quarantine
settings: os limiares em vigor (settings), os padrões (defaults) e as faixas aceitas (ranges); o conector precisa de service_day_field e ltv_window_days. Só leitura: gravar continua pela tela. quarantine: quantos contatos removidos sem fusão reconhecida têm dados de compra guardados (total) e quantos já podem ser apagados (deletable). Apagar vem na próxima onda, com portfolio:admin, porque é LGPD e não tem volta.
GET https://app.vennocrm.com.br/api/v1/portfolio/customers?situacao=em_risco,inativo&atrasados=1&ordem=valor&limit=25
{
"success": true,
"data": {
"today": "2026-10-09",
"filters": { "dono": "all", "situacoes": ["em_risco", "inativo"], "atrasados": true, "dia": null, "q": null, "ordem": "valor" },
"service_day_field": { "name": "dia_atendimento", "label": "Dia de atendimento", "field_type": "select", "options": ["segunda", "terca"] },
"rows": [{
"contact_id": "8c0b...", "name": "Mercado Exemplo", "phone": "5544999990000",
"owner_id": "4d2f...", "owner_email": "vendedor@empresa.com.br",
"status": "em_risco", "first_purchase_at": "2025-03-02", "last_purchase_at": "2026-09-10",
"next_expected_at": "2026-10-02", "days_since_last": 29, "purchases_count": 18,
"total_value": 27410.5, "avg_interval_days": 22, "potential_month": 1520.5,
"credit_limit": 20000, "credit_available": 4300, "overdue_amount": 1250, "overdue_count": 2,
"service_day": "segunda", "late": true
}],
"summary": {
"total": { "owner_id": null, "customers": 212, "novo": 9, "ativo": 140, "em_risco": 31, "inativo": 20, "sem_compra": 12, "late": 27, "credit_limit": 0, "credit_available": 0, "overdue_amount": 18420.3, "overdue_customers": 14 },
"by_owner": [{ "owner_id": "4d2f...", "owner_email": "vendedor@empresa.com.br", "customers": 80, "novo": 3, "ativo": 55, "em_risco": 12, "inativo": 6, "sem_compra": 4, "late": 10, "credit_limit": 0, "credit_available": 0, "overdue_amount": 7210, "overdue_customers": 5 }]
}
},
"pagination": { "page": 1, "limit": 25, "total": 51 }
}Endpoints - Compras: consulta
A lista de compras gravadas por POST /purchases, para conciliação ERP × Venno e BI. Mesmas travas da carteira: módulo ligado e chave com read e portfolio:read.
GET /purchases
Pagina por cursor (limit até 100, next_cursor, has_more), em ordem crescente de updated_at: leia até o fim, guarde o maior updated_at e volte com ele em updated_since para pegar só o que mudou. Filtros: contact_id, source (api, bling, protheus, csv; e as que nascem dentro do Venno: pedidos, planilha, manual), external_id, since e until (dia da compra, AAAA-MM-DD, inclusivos) e include_items=true para trazer os itens de cada compra na mesma chamada. Valor torto é 400 com o código do filtro; a lista nunca é devolvida "inteira" no lugar do recorte pedido. raw não sai aqui.
GET /purchases/:id
Uma compra com os itens na ordem da linha. 404 PURCHASE_NOT_FOUND se não é deste workspace.
Compras de contato apagado (include_orphans)
As compras não têm chave estrangeira para o contato, de propósito (decisão das migrations 191 e 210): apagar um contato pela API ou pela tela deixa as compras dele no banco, invisíveis nas telas, até a rotina de reconciliação pô-las em quarentena e o administrador apagar. Por padrão esta lista as omite, e contact_id de contato apagado responde 404. Com include_orphans=true elas voltam com contact: null, que é como a conciliação descobre o que sobrou. Uma página pode vir com menos de limit itens quando houve órfã omitida: siga o next_cursor normalmente, nada é pulado.
Cancelar ou corrigir uma compra
Não há DELETE. O caminho oficial é reenviar a compra por POST /purchases com o mesmo source e external_id e "status": "canceled" (ou os valores corrigidos): é idempotente e recalcula a situação do cliente.
GET https://app.vennocrm.com.br/api/v1/purchases?updated_since=2026-10-08T00:00:00Z&include_items=true&limit=100
{
"success": true,
"data": [{
"id": "c3d1...", "external_id": "NF-000123", "source": "api", "kind": "invoice", "status": "invoiced",
"purchased_at": "2026-10-01", "total_value": 1520.5, "order_external_id": "PED-9981",
"seller_external": "V01", "owner_id": "4d2f...",
"contact_id": "8c0b...", "contact": { "id": "8c0b...", "name": "Mercado Exemplo", "phone": "5544999990000" },
"created_at": "2026-10-01T18:02:11.483+00:00", "updated_at": "2026-10-08T09:15:40.120+00:00",
"items": [
{ "sku": "CX-10", "description": "Caixa 10 kg", "quantity": 12, "unit": "CX", "unit_price": 120, "total": 1440, "product_id": "0583..." },
{ "sku": "FRETE", "description": "Frete", "quantity": null, "unit": null, "unit_price": null, "total": 80.5, "product_id": null }
]
}],
"pagination": { "limit": 100, "next_cursor": "eyJ0IjoiMjAyNi0xMC0wOFQwOToxNTo0MC4xMjArMDA6MDAiLCJpIjoiYzNkMS4uLiJ9", "has_more": true }
}Endpoints - Pedidos
O módulo Pedidos pela chave: o app do vendedor externo tira o pedido, o financeiro opera, o aprovador decide a alçada e o ERP devolve o que fez com ele. São as mesmas funções da tela: preço de tabela, totais, alçada, crédito e transições são decididos no servidor; o corpo traz só produto, quantidade e desconto. Todo valor é estimado, sem imposto; o total final só existe quando o ERP devolve (erp_total_cents). Exige o módulo Pedidos ligado (403 MODULE_DISABLED, falha fechado) e, enquanto o ambiente é preparado, responde 503 PEDIDOS_PENDING.
Quem age: o ator da chave
Pedidos decidem permissão pela pessoa (quem vê o quê, quem aprova, quem fica na trilha), e uma chave não é uma pessoa. Por isso toda chamada age em nome de alguém:
- Padrão: quem criou a chave. A chave herda as permissões dessa pessoa enquanto ela for membro ativo. Se ela saiu da equipe, a chamada responde
403 ACTOR_REQUIREDe diz o que fazer. actor_email(no corpo; na query, nosGET) age em nome de outro membro ativo: o pedido sai com a alçada e a visibilidade dele, e a trilha registra o nome dele. E-mail de quem não é membro ativo responde422 ACTOR_NOT_MEMBER; nunca cai no criador em silêncio.- Obrigatório em
liberarerejeitar(422 ACTOR_EMAIL_REQUIREDsem ele): alçada é decisão de pessoa, e a chave não aprova pedido por conta própria. - O ERP devolvendo resultado fica na trilha como
actor_kind: "connector", com o prefixo da chave nopayload, sem pessoa.
Escopo E permissão, os dois
Cada escopo da chave libera uma permissão do ator; o que o ator não tem no workspace, a chave não inventa. orders libera create_orders (listar, ler, criar, editar, simular, enviar e cancelar os próprios pedidos, catálogo com o preço do cliente). orders:manage libera manage_orders e view_all_orders (ver os pedidos da equipe, aceitar, bloquear, faturar, cancelar de terceiros, receber o resultado do ERP). orders:approve libera approve_order_discount (liberar e rejeitar). orders é a base; os outros dois somam a ela. Uma chave só com orders criada pelo dono lista só os pedidos do dono; para a integração do ERP ler todos, marque orders:manage.
GET /orders
Pagina por cursor (limit até 100), do mais novo para o mais antigo (ordem de criação, como a tela). Filtros: status (um ou vários, separados por vírgula: rascunho, enviado, aceito, bloqueado, liberado, faturado, cancelado), contact_id, seller_email, since/until (dia de criação, AAAA-MM-DD, no fuso do workspace), updated_since (ISO 8601, o delta para o BI), external_id (o id no ERP), approval=pendente (a fila de aprovação) e number. Cada linha traz situação, cliente, vendedor (id, nome e seller_email), total estimado, maior desconto, aprovação, motivo do bloqueio e datas. Valor torto é 400 com o código do filtro. Com updated_since a ordem continua sendo a de criação, não a de atualização: leia até has_more: false e, na rodada seguinte, mande como updated_since o instante em que a leitura anterior começou (não o maior updated_at visto, que aqui pode pular um pedido alterado no meio da leitura). Um pedido pode voltar em duas rodadas; grave por id.
GET /orders/:id
order (com version, seller_email, created_by_email, external_id e erp_total_cents), items, events (a trilha: quem fez o quê, com actor_kind user, system ou connector), actions (o que o ator da chave pode fazer agora), credit (limite, disponível, vencidos e os pedidos em aberto do cliente) e summary_text. 404 ORDER_NOT_FOUND para pedido de outro workspace ou que o ator não enxerga.
POST /orders
Cria o rascunho e, com "enviar": true, já envia. contact_id e items[] (product_id, quantity, discount_pct); opcionais order_type, delivery_date, payment_terms, payment_term_id, notes, seller_email (tirar em nome de outro vendedor exige que o ator opere pedidos), conversation_id, deal_id. Condição de pagamento: payment_term_id escolhe um item do cadastro de condições (os ativos vêm em GET /orders/settings, em rules.payment_terms_catalog) e o pedido grava o nome dele; id de outro workspace ou de condição desativada é 422 PAYMENT_TERM_NOT_FOUND. Sem ele, payment_terms em texto continua aceito como antes (até 120 caracteres). Idempotente por uma destas, nesta ordem: header Idempotency-Key, idempotency_key no corpo (8 a 100 letras, números, - ou _; fora do padrão, 422 INVALID_IDEMPOTENCY_KEY, nunca ignorado), ou external_id (o id do pedido no ERP, como chave natural; a chave vale por workspace). Repetir devolve o mesmo pedido com 200 e replayed: true, nunca um segundo, e o corpo da repetição é ignorado; a exceção é o rascunho ainda não enviado repetido com "enviar": true, que é atualizado com o corpo e enviado (a nova tentativa de um envio que caiu no meio). A mesma chave usada por outro ator (outro actor_email) responde 409 IDEMPOTENCY_KEY_REUSED. Sem nenhuma das três, 422 IDEMPOTENCY_KEY_REQUIRED. A criação responde 201 com id, número, situação, versão, totais estimados e block_reason quando o envio foi para aprovação (alcada, credito). Desconto acima do teto absoluto não grava nem como rascunho (DISCOUNT_ABOVE_MAX).
PATCH /orders/:id e POST /orders/simulate
PATCH edita o rascunho (mesmo corpo da criação) com expected_version obrigatório: se o pedido mudou, 409 VERSION_CONFLICT e nada é gravado; "enviar": true envia na mesma chamada. Pedido que já saiu do rascunho responde 409 ORDER_NOT_DRAFT. simulate não grava: devolve as linhas com o preço da tabela do cliente, totais, a alçada do vendedor (dentro, precisa_aprovar, bloqueado) e a pré-checagem do envio (data mínima, pedido mínimo, crédito), calculados pelas mesmas funções que gravam.
POST /orders/:id/actions
{ "action", "expected_version", "reason", "block_reason", "invoice_number", "invoiced_amount_cents", "submit_idempotency_key", "actor_email" }. Ações e escopo: enviar e cancelar (o próprio) com orders; aceitar, bloquear, faturar e cancelar (de terceiros) com orders:manage; liberar e rejeitar com orders:approve e actor_email. reason é obrigatório em cancelar, rejeitar e bloquear; enviar usa submit_idempotency_key (ou o header Idempotency-Key). A transição é compare-and-set: ação repetida devolve o estado (replayed: true); versão velha é 409 VERSION_CONFLICT; ação que não cabe no estado é 409 INVALID_TRANSITION; sem permissão do ator, 403 FORBIDDEN. Ninguém libera o próprio pedido. Faturar sem ERP vira compra na Carteira Ativa.
POST /orders/:id/erp-result
O ERP devolve o que decidiu sobre um pedido que leu em GET /orders (modo push: o ERP chama o Venno). status: accepted (de enviado ou liberado para aceito), rejected (vira bloqueado por regra do ERP; reason obrigatório) ou invoiced (faturado). Opcionais erp_order_id, invoice_number (vence erp_order_id em external_id), invoiced_amount_cents (o total que o ERP calculou; vai para erp_total_cents, o Venno nunca o calcula), invoiced_on e expected_version. Exige orders:manage. Idempotente: o mesmo resultado duas vezes responde 200 com replayed: true, sem segundo evento. Faturado registra a compra na Carteira Ativa; a nota que o ERP mandar depois por POST /purchases citando venno:<id> a substitui, sem contar a venda duas vezes.
GET /orders/catalog e GET /orders/settings
catalog?contact_id=&q=&limit=: busca no catálogo com o preço que este cliente paga (a tabela do contato, a lista padrão ou o preço base), até 50 por chamada; sem contact_id, price_list_id escolhe a tabela. settings: as regras do módulo, só leitura (alçada por papel, produto e categoria, tipos de pedido com a data mínima de hoje, pedido mínimo, crédito, condições de pagamento e o campo da tabela de preço); exige manage_settings do ator. As condições vêm em rules.payment_terms (os nomes, como sempre) e em rules.payment_terms_catalog (id, name, days_to_pay e installments, opcionais), os itens ativos do cadastro. Gravar continua pela tela.
DELETE /portfolio/quarantine
Apaga de vez os dados de compra dos contatos removidos sem fusão reconhecida (a contagem de GET /portfolio/quarantine). Exige write e portfolio:admin. Só apaga quem está em quarentena há mais de 1 hora, até 500 por chamada; responde deleted e remaining, e fica em activity_logs com quem apagou (o criador da chave) e o prefixo dela. Não tem volta.
curl --request POST \
--url https://app.vennocrm.com.br/api/v1/orders \
--header "Authorization: Bearer zdk_live_xxxxx" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: app-vendedor-7f3a9c" \
--data '{
"contact_id": "8c0b...",
"seller_email": "vera@empresa.com.br",
"order_type": "normal",
"delivery_date": "2026-10-14",
"payment_term_id": "5b2e...",
"items": [
{ "product_id": "0583...", "quantity": 12, "discount_pct": 3 },
{ "product_id": "9a1e...", "quantity": 2.5 }
],
"enviar": true
}'
{
"success": true,
"data": {
"id": "f41c...", "number": 1042, "status": "bloqueado", "version": 1, "replayed": false,
"totals": { "subtotal_cents": 185000, "discount_cents": 4320, "total_estimated_cents": 180680 },
"warnings": [], "block_reason": "alcada"
}
}POST https://app.vennocrm.com.br/api/v1/orders/f41c.../actions
{ "action": "liberar", "expected_version": 1, "reason": "Cliente antigo, margem ok", "actor_email": "gil@empresa.com.br" }
POST https://app.vennocrm.com.br/api/v1/orders/f41c.../actions
{ "action": "cancelar", "expected_version": 2, "reason": "Cliente desistiu" }curl --request POST \
--url https://app.vennocrm.com.br/api/v1/orders/f41c.../erp-result \
--header "Authorization: Bearer zdk_live_xxxxx" \
--header "Content-Type: application/json" \
--data '{
"status": "invoiced",
"erp_order_id": "PED-9981",
"invoice_number": "NF-000123",
"invoiced_amount_cents": 198748,
"invoiced_on": "2026-10-09",
"expected_version": 3
}'
{
"success": true,
"data": { "id": "f41c...", "status": "faturado", "version": 4, "replayed": false, "warnings": [] }
}Endpoints - Visitas de campo
As visitas do vendedor de rua (módulo Venno Campo): check-in e check-out no cliente, tempo de permanência, resultado e motivo de não venda. A localização é registrada só ao chegar e ao sair, nunca o trajeto. Exige o escopo visits:read (mais read) e o módulo ligado (403 MODULE_DISABLED, falha fechado); enquanto o ambiente é preparado, responde 503 CAMPO_PENDING. A chave lê como gestor: todas as visitas do workspace.
GET /visits
Pagina por cursor sobre (updated_at, id), em ordem crescente, com limit até 100 e updated_since (ISO 8601) para o delta. Filtros: seller_id ou seller_email, contact_id, status (aberta, concluida, cancelada), from/to (chegada; ISO 8601 ou AAAA-MM-DD no fuso do workspace). Valor torto é 400 com o código do filtro (INVALID_SELLER, INVALID_STATUS, INVALID_FROM...).
A visita
id, contact_id, seller_id, seller_email, status; na chegada e na saída, a hora do servidor (check_in_at, check_out_at) e a do aparelho (check_in_captured_at, check_out_captured_at), *_lat, *_lng, *_accuracy_m e *_distance_m (até o local do cliente; nulo se ele não tem local); duration_seconds (nulo na visita fechada automaticamente), outcome (pedido, sem_pedido, outro), reason_id, order_id, notes, flags e idempotency_key.
Marcações (flags)
Longe do cliente marca, não bloqueia. longe_do_cliente e saida_longe_do_cliente (acima da distância configurada, padrão 300 m), localizacao_imprecisa e saida_localizacao_imprecisa (precisão acima de 150 m), sem_localizacao e saida_sem_localizacao (quando o workspace não exige localização), cliente_sem_local (o cliente não tem local cadastrado) e fechada_automaticamente (ficou aberta além do limite de horas; sem duração).
Webhooks de Saida
Configure webhooks no painel de Integracoes para receber eventos como contact.updated, deal.moved, message.received, task.completed, product.updated, products.imported, purchases.imported, customer.status_changed e order.invoiced. Tudo que e lote sai como um evento por chamada, com a lista do que entrou; nunca um evento por item.
Payload
{
"event": "contact.updated",
"event_id": "6f1c2b7e-0d3a-4c7e-9a52-1b2c3d4e5f60",
"workspace_id": "uuid",
"timestamp": "2026-04-06T12:00:00.000Z",
"attempt": 1,
"data": {
"id": "contact_uuid",
"phone": "5511999998888"
}
}event_id identifica a ocorrencia: a retentativa reenvia o mesmo event_id com attempt maior. Guarde os ids processados e descarte a repeticao.
Headers enviados
Content-Type: application/jsonX-ZapDesk-EventeX-Venno-Event(o mesmo valor)X-ZapDesk-SignatureeX-Venno-Signaturecomsha256=...(o mesmo valor)- Headers customizados configurados no webhook
Retries
Em falhas ou timeout, o ZapDesk tenta novamente em 30 segundos, 2 minutos e 10 minutos. Apos muitas falhas consecutivas, o webhook e desativado automaticamente.
Eventos disponiveis
contact.created, contact.updated, contact.deleted, deal.created, deal.moved, deal.closed, deal.updated, conversation.created, conversation.assigned, conversation.resolved, message.received, message.sent, task.created, task.completed, task.assigned, proposal.created, proposal.sent, proposal.viewed, proposal.accepted, proposal.rejected, proposal.canceled, product.updated, products.imported, purchases.imported, customer.financials_updated, customer.status_changed, customers.status_changed, portfolio.alert_created, order.created, order.submitted, order.accepted, order.blocked, order.released, order.rejected, order.invoiced, order.canceled e visit.completed.
deal.closed sai quando o negocio e fechado pelo painel e, desde 10/10/2026, tambem quando PATCH /deals/:id/move o leva a uma etapa de ganho ou perda (data.outcome: won ou lost).
contact.updated traz o contato em data.contact e, desde 13/10/2026, o endereço em data.contact.address (cep, rua, numero, complemento, bairro, cidade, uf e ibge, ou null), sem latitude e longitude: a mesma regra de dado mínimo do visit.completed. O ponto do cliente fica em GET /contacts/:id.
product.updated sai ao criar, editar ou desativar um produto, pelo painel ou pela API. data.product traz o produto e data.action diz o que houve: created, updated ou deactivated.
products.imported sai uma vez por chamada de POST /products/import que gravou algo: data traz received, inserted, updated, rejected (contagem), duplicates_in_batch e products, a lista de { id, sku, action } do que foi gravado. Para o produto inteiro, leia GET /products?updated_since=.
Carteira Ativa (um evento por lote)
purchases.imported sai uma vez por chamada que gravou compra, pela API (POST /purchases), pela planilha ou pela venda registrada na ficha: data traz received, inserted, updated, orphans, rejected, contacts_created, recomputed e purchases, a lista de { id, external_id, source, contact_id, matched_by }. A compra inteira esta em GET /purchases?updated_since=.
customer.financials_updated sai uma vez por chamada de POST /customers/financials: received, upserted, orphans, rejected e customers, a posicao gravada de cada cliente (contact_id, limite, credito, vencidos, synced_at).
customer.status_changed e um cliente cuja situacao mudou (data.customer: contact_id, from, to, next_expected_at, days_since_last; from e null na primeira compra). Sai quando um recalculo muda ate 20 clientes numa chamada; acima disso, e sempre na revisao diaria do calendario, sai customers.status_changed com data.customers (ate 500 por evento) e data.origin (recompute ou daily_review). Assine os dois para ver toda mudanca.
portfolio.alert_created sai uma vez por workspace e execucao do cron que criou tarefa de "passou do dia de pedir": data.today, created e alerts, a lista de { contact_id, owner_id, task_id, cycle_date, alert_ids }.
Pedidos (uma transicao, um evento)
order.created (rascunho ou criado ja enviado), order.submitted, order.accepted, order.blocked, order.released, order.rejected (devolvido ao vendedor), order.invoiced e order.canceled. Cada um traz data.order (o pedido como ficou DEPOIS da transicao, no formato de GET /orders/:id), data.items, data.actor_kind (user, system ou connector, o mesmo da trilha), data.actor_id e data.from_status; bloqueio, devolucao e cancelamento trazem o motivo, e a decisao do ERP traz connector, external_id e external_status.
Criar ja enviando gera order.created e order.submitted (ou order.blocked). Repeticao idempotente (mesma chave, clique duplo) nao emite de novo. Valores em centavos e estimados; o total final vem do faturamento.
Visitas de campo (uma visita, um evento)
visit.completed sai uma vez por visita concluida: no check-out do vendedor ou quando a visita esquecida e fechada automaticamente (data.auto_closed: true). Traz data.visit (a visita como ficou, no formato de GET /visits, sem *_lat, *_lng e *_accuracy_m: a posição do vendedor não sai em webhook; a distância e as marcações vêm, e a coordenada fica em GET /visits com o escopo visits:read), data.contact (id, name) e data.reason (id, name, ou null). Repetir o check-out (mesma visita) nao emite de novo.
Verificacao da assinatura (Node.js)
import crypto from 'node:crypto'
const payload = JSON.stringify(body)
const signature = 'sha256=' + crypto
.createHmac('sha256', process.env.ZAPDESK_WEBHOOK_SECRET)
.update(payload)
.digest('hex')
const isValid = signature === request.headers['x-zapdesk-signature']Verificacao da assinatura (Python)
import hmac import hashlib payload = request_body.encode() secret = os.environ['ZAPDESK_WEBHOOK_SECRET'].encode() signature = 'sha256=' + hmac.new(secret, payload, hashlib.sha256).hexdigest() is_valid = signature == request.headers['X-ZapDesk-Signature']
Campos Customizaveis
Defina campos tipados para contatos e deals em Configuracoes > Integracoes > Campos Customizaveis. Os valores vivem em custom_fields e respeitam as validacoes do tipo configurado.
Número e moeda: envie como número
Em campos number e currency, envie o valor como número JSON ("limite": 5000), não como texto ("limite": "5000"). O texto continua sendo aceito na gravação, mas não entra nos filtros por faixa (maior, menor, entre) das campanhas.
Tipos suportados
text, number, date, datetime, boolean, select, multiselect, url, email, phone, cpf, cnpj e currency.
Leitura
Toda resposta de contato ou deal devolve os valores em custom_fields.
Escrita
Envie apenas os campos que deseja alterar. O backend faz merge e valida tipo por tipo.
Exemplo de payload
{
"custom_fields": {
"matricula": "2026-104",
"data_vencimento": "2026-05-10",
"ativo": true,
"score": 98,
"categorias": ["Vip", "Renovacao"]
}
}Mudancas recentes
- 13/10/2026. Endereço do contato.
GET /contacts,GET /contacts/:id,POST /contactsePATCH /contacts/:iddevolvemaddressem cada contato (o endereço e o ponto confirmado do cliente, ounull), e oPATCHaceitaaddressparcial, comlatelngopcionais (ver Endereço do contato). O webhookcontact.updatedpassou a trazerdata.contact.address, sem latitude e longitude. Requisição semaddressfunciona como antes. Nada do que já existia mudou. - 11/10/2026. Visitas de campo (Venno Campo). Escopo novo
visits:readeGET /visits(ver Visitas de campo), com cursor eupdated_since; evento novovisit.completed, um por visita concluida. Exige o módulo Venno Campo. Nada do que já existia mudou. - 10/10/2026. Pedidos pela chave (Onda 2, parte A). Os escopos
orders,orders:manageeorders:approvesaíram da reserva. EntramGET,POSTePATCH /orders,GET /orders/:id,POST /orders/simulate,GET /orders/catalog,GET /orders/settings,POST /orders/:id/actionsePOST /orders/:id/erp-result(ver Pedidos), eDELETE /portfolio/quarantinecomportfolio:admin. A chave age em nome de quem a criou, ou deactor_email(obrigatório em liberar e rejeitar); o resultado do ERP fica na trilha comoconnector. Nada do que já existia mudou. - 09/10/2026. Escopos novos e convencoes da Onda 1. A chave ganhou os escopos
products,custom_fields,tags,team:read,tasks,portfolio:readeportfolio:admin(ver Escopos). Chave criada antes nao os tem: a rota nova responde403 INSUFFICIENT_SCOPEdizendo qual falta. As listas novas paginam por cursor e aceitamupdated_since. Nada do que ja existia mudou. - 10/10/2026. Webhooks da Carteira Ativa e dos Pedidos, sincronizacao nas listas antigas. Eventos novos
purchases.imported,customer.financials_updated,customer.status_changed,customers.status_changed,portfolio.alert_createdeorder.createdaorder.canceled(ver Webhooks); tudo que e lote sai como um evento por chamada, edeal.closedpassou a sair tambem ao mover pela v1.GET /contactseGET /dealsganharamupdated_sincee cursor (aditivo) e os doisPATCHaceitamexpected_updated_at(ver Sincronizacao incremental). O servidor MCP ganhou leitura da carteira e dos pedidos. - 09/10/2026. Produtos e listas de preco.
GET,POST,PATCHeDELETE(desativa) em/products, importacao em lote por SKU emPOST /products/importeGET /price-lists(ver Produtos). Webhooks novosproduct.updated(um produto) eproducts.imported(um por chamada de importacao); todo payload de webhook passou a trazerevent_ideattempt, e os headersX-Venno-EventeX-Venno-Signatureacompanham os antigos. - 09/10/2026. Campos personalizados e equipe. CRUD das definicoes de campo em
/custom-fields(ver Campos personalizados) e leitura de membros e equipes em/team/memberse/team/teams(ver Equipe). Oassigned_tode negocio passou a exigir membro ativo, como oowner_emailde contato ja exigia. - 09/10/2026. Etiquetas, tarefas, carteira e compras na v1.Etiquetas:
GET /tagspassa a ler o catálogo (mesmo formato;?format=fulltraz id, cor e uso), e entramPOST/PATCH/DELETE /tagse a remoção de etiqueta em contato e negócio. Tarefas: listar, ler, criar, editar e concluir, comassigned_email,Idempotency-Keyeexpected_updated_at. Carteira Ativa: a tela Carteira, o resumo por dono, o painel de compras e o financeiro de cada cliente, os relatórios, os limiares e a quarentena, só leitura. Compras: a lista por cursor comupdated_sincee a regra das compras de contato apagado. Tudo aditivo; o formato do que já existia não mudou. - Contatos:
owner_email.POSTePATCHde/contactsaceitam o responsável pela carteira, e o e-mail agora é validado: precisa ser de um membro ativo da equipe, senão a chamada volta422e nada é gravado. Requisição sem o campo funciona como antes. - Campos
numberecurrency. Envie como número, não como texto, para o contato entrar nos filtros por faixa das campanhas. Valores já gravados como texto continuam salvos, mas ficam fora desses filtros até serem regravados como número.
Exemplos de Integracao
Make
Use um modulo HTTP para chamar a API publica e um webhook custom para receber eventos de contatos, deals ou mensagens.
n8n
Configure a API Key em credenciais seguras e combine os nodes HTTP Request + Webhook para sincronizar seu fluxo.
cURL
Ideal para testes rapidos, smoke tests e scripts simples de automacao no terminal.