Conexões de CRM
Mantenha os contatos da Postal e o seu CRM em sincronia, nos dois sentidos e sem loops: HubSpot, Pipedrive e RD Station CRM, com mapeamento de campos e etapas, registro e novas tentativas.
#Visão geral
Uma conexão de CRM mantém os contatos da Postal e as pessoas do seu CRM em sincronia, nos dois sentidos e sem loops. O que a pessoa faz no CRM (um contato novo, um negócio que muda de etapa) chega à Postal como contato, etiqueta, propriedade e evento, e pode iniciar uma automação. O que acontece na Postal (um contato criado ou alterado, um e-mail aberto, clicado, respondido ou devolvido) volta ao CRM como dados da pessoa e como nota na linha do tempo dela.
| Conexão | Credenciais | Do CRM para a Postal | Da Postal para o CRM |
|---|---|---|---|
| HubSpot | Token de um aplicativo privado (e, para webhooks, o client secret) | Webhooks de contatos e negócios, e a sincronização manual | Contatos criados ou atualizados; notas na linha do tempo |
| Pipedrive | Token da API e endereço da conta | Webhooks de pessoas e negócios, e a sincronização manual | Pessoas criadas ou atualizadas; notas na pessoa |
| RD Station CRM | Token da instância | Webhooks de contatos e negociações, e a sincronização manual | Contatos criados ou atualizados; atividades na negociação ou nota no contato |
| Webhook (outro CRM) | Nenhuma: um segredo de assinatura gerado pela Postal | Qualquer sistema que faça um POST com JSON assinado | POST assinado para a URL HTTPS do seu sistema |
As três conexões nativas usam o token que você cola (sem OAuth) e leem a API do CRM por você. A conexão por webhook serve para qualquer CRM ou sistema que envie e receba webhooks; ela tem a sua própria página.
As integrações com apps (Stripe, Supabase, Clerk, Auth0 e Firebase) usam as mesmas conexões, mas contam à parte, em app_integrations, e não sincronizam de volta.
Depende do plano
O número de conexões de CRM vem do plano de marketing (crm_connections; 0 é não incluído). Passar do limite responde 403 plan_limit_reached. Veja Limites; GET /crm/providers mostra o limite e quantas você já usa. As conexões e os contatos criados por elas contam nos limites do plano como qualquer outro.
#Criar uma conexão
No painel, abra Integrações › CRM, escolha o CRM e cole as credenciais. Elas ficam guardadas criptografadas e não aparecem de novo: a API devolve só os nomes dos campos guardados. Use Testar conexão para conferir o acesso. Pela API, é POST /crm-connections, com uma chave de acesso total.
cURL
curl -X POST https://api.postal.so/crm-connections \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "hubspot",
"name": "HubSpot",
"credentials": {
"access_token": "pat-na1-…",
"client_secret": "…"
},
"settings": {
"direction": "both"
}
}'
Node.js
const response = await fetch('https://api.postal.so/crm-connections', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'hubspot',
name: 'HubSpot',
credentials: {
access_token: 'pat-na1-…',
client_secret: '…',
},
settings: {
direction: 'both',
},
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/crm-connections",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"provider": "hubspot",
"name": "HubSpot",
"credentials": {
"access_token": "pat-na1-…",
"client_secret": "…",
},
"settings": {
"direction": "both",
},
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('crm-connections', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'provider' => 'hubspot',
'name' => 'HubSpot',
'credentials' => [
'access_token' => 'pat-na1-…',
'client_secret' => '…',
],
'settings' => [
'direction' => 'both',
],
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "crm_connection",
"id": "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"provider": "hubspot",
"name": "HubSpot",
"status": "active",
"settings": {
"direction": "both",
"field_mappings": [],
"stage": null,
"inbound": {
"items_path": null,
"event_field": null,
"allow_token": false
},
"outbound": {
"url": null,
"events": [
"contact.created",
"contact.updated",
"contact.deleted",
"email.opened",
"email.clicked",
"email.replied",
"email.bounced",
"email.complained"
]
},
"skip_sources": []
},
"credentials": ["access_token", "client_secret"],
"source": "crm:019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"webhook_url": "https://api.postal.so/integrations/crm/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/webhook",
"webhook_secret": "whsec_c2VncmVkby1kZS1leGVtcGxvLTEyMzQ1Ng==",
"last_sync_at": null,
"last_error": null,
"last_error_at": null,
"created_at": "2026-10-26T14:00:00.000000Z",
"updated_at": "2026-10-26T14:00:00.000000Z"
}
provider:hubspot,pipedrive,rdstation_crmougeneric(o webhook). As credenciais de cada um estão abaixo.statuscomeçaactive.paused(PATCH) para de receber e de enviar sem apagar nada;erroré só um aviso de que o último envio falhou de vez e volta aactiveno primeiro envio que der certo.webhook_urlé onde o CRM entrega as mudanças;webhook_secret(whsec_…) é o segredo que autentica a entrega. Aparecem ao criar, ao consultar uma conexão e ao gerar um segredo novo, não na lista.source(crm:<id>) é a origem de tudo que a conexão muda nos contatos.
#O que sincroniza, e em que sentido
settings.direction escolhe o sentido: both (padrão), inbound (só do CRM para a Postal) ou outbound (só da Postal para o CRM). Numa conexão só de envio, o webhook do CRM responde 409 connection_paused; numa só de recebimento, a Postal não envia nada.
Do CRM para a Postal
- Cada pessoa vira um contato criado ou atualizado. A Postal reconhece o contato pelo vínculo com o registro do CRM, depois pelo
external_ide por fim pelo e-mail; por isso uma troca de e-mail no CRM atualiza o mesmo contato. - Os campos mapeados viram nome, propriedades e etiquetas (veja Mapear campos e etapas). Quem está na lista de supressão não vira contato (a entrada fica como
skipped). - Quem se descadastrou nunca é reinscrito por um CRM:
unsubscribed: falsenão desfaz um descadastro (só a API de contatos, com a prova do novo consentimento). - Um evento do CRM, quando o mapeamento nomeia um (
inbound.event_field), é gravado no contato e pode iniciar uma automação com o gatilho de evento. - Exclusões no CRM não apagam o contato na Postal.
Da Postal para o CRM
A Postal envia as mudanças que o CRM assinou em settings.outbound.events. O padrão é:
| Evento | No CRM | Padrão |
|---|---|---|
contact.created, contact.updated | A pessoa é criada ou atualizada (nome, e-mail, campos mapeados). | Sim |
contact.deleted | Uma nota “Contato excluído no Postal”. O registro do CRM não é apagado. | Sim |
email.opened, email.clicked, email.replied, email.bounced, email.complained | Uma nota na linha do tempo da pessoa, com o assunto (e o link, o motivo do bounce…). | Sim |
email.sent, email.delivered, email.failed, email.suppressed | O mesmo, para quem quer o histórico completo. | Não |
Os eventos são os mesmos dos webhooks da Postal. Qualquer outro nome em outbound.events dá 422.
O texto das notas é sempre em português do Brasil, como as pessoas do CRM leem (por exemplo, “Postal: E-mail aberto”, “Assunto: …”). Os nomes de campos que a Postal grava no CRM não mudam com o idioma.
Sincronizar agora
O botão Sincronizar agora (POST /crm-connections/{id}/sync) roda em segundo plano: lê as pessoas do CRM, página a página (e, se houver mapeamento de etapas, os negócios em aberto), e depois envia todos os contatos da Postal, conforme o sentido. Cada pessoa aparece no registro, e uma entrada sync resume quantas pessoas foram importadas e enviadas e quantas falharam. Vale para a primeira carga, para recuperar o que um webhook perdeu ou quando o CRM não consegue enviar webhooks.
#Mapear campos e etapas
Sem mapeamento, a Postal lê e escreve no seu formato próprio. Nas conexões nativas, e-mail, nome, sobrenome e o ID do registro já vêm ligados, e o que você mapeia se soma a eles. Cada mapeamento liga um campo do CRM a um campo da Postal, com um sentido próprio (both, inbound ou outbound):
| Campo da Postal | O que é |
|---|---|
email | O e-mail do contato. |
external_id | O ID da pessoa no seu app (o mesmo external_id dos contatos). |
crm_id | O ID do registro no CRM. A Postal o guarda no vínculo da conexão, nunca no external_id. |
first_name, last_name | Nome e sobrenome. Um valor vazio no CRM limpa o campo. |
unsubscribed | Verdadeiro ou falso. Veja a regra do descadastro acima. |
tags | Etiquetas: uma lista, ou um texto separado por vírgula ou ponto e vírgula. |
properties.<chave> | Uma propriedade de contato que já existe (properties.company). Uma chave que não existe dá 422. |
O campo do CRM é um caminho no estilo JSONPath, de até 255 caracteres, com um subconjunto pequeno: $.person.email (ou person.email), items[0].name, $.data.items[*] (todos os elementos de uma lista) e $ (o documento todo). Um caminho que não existe lê null, e o campo é ignorado (menos nome e sobrenome, que ficam vazios). Nas conexões nativas, os caminhos apontam para o registro que a Postal monta do CRM (por exemplo, $.properties.company no HubSpot e $.custom.<chave> no Pipedrive e no RD Station CRM); GET /crm-connections/{id}/fields lista os campos do CRM que você pode escolher, com o tipo.
Etapas do funil
Quando um negócio muda de etapa, a pessoa dele ganha uma propriedade e/ou uma etiqueta da Postal, e dá para disparar uma automação nessa mudança. Em settings.stage, crm_field e pipeline_field dizem onde estão a etapa e o funil (nas conexões nativas, $.deal.stage e $.deal.pipeline já são o padrão), e cada item de mappings liga uma stage (e, opcionalmente, um pipeline) a uma property_key com property_value, a uma tag ou às duas. Os nomes são comparados sem diferenciar maiúsculas. Sem property_value, a propriedade recebe o nome da etapa. Ao enviar, a Postal não move negócios no CRM. GET /crm-connections/{id}/stages lista as etapas de cada funil.
{
"settings": {
"direction": "both",
"field_mappings": [
{
"crm_field": "$.properties.company",
"postal_field": "properties.company",
"direction": "both"
},
{
"crm_field": "$.properties.hs_lead_status",
"postal_field": "tags",
"direction": "inbound"
}
],
"stage": {
"crm_field": "$.deal.stage",
"pipeline_field": "$.deal.pipeline",
"mappings": [
{
"stage": "Closed won",
"pipeline": "Sales",
"property_key": "lifecycle",
"property_value": "customer",
"tag": "customer"
}
]
},
"outbound": {
"events": [
"contact.created",
"contact.updated",
"email.opened",
"email.clicked",
"email.replied"
]
},
"skip_sources": [
"my-app"
]
}
}
Neste exemplo, o campo company do HubSpot vai e volta para a propriedade company da Postal, o status de lead só entra como etiqueta, e um negócio em “Closed won” do funil “Sales” marca a pessoa com lifecycle = customer e a etiqueta customer. As propriedades company e lifecycle precisam existir antes. Só os eventos listados saem para o CRM, e as mudanças vindas de my-app não voltam para ele.
#Sem loops
Duas conexões que se alimentam poderiam trocar a mesma mudança para sempre. A Postal evita isso de dois modos:
- Tudo que uma conexão muda num contato leva a origem
crm:<id>(last_change_source), e essa conexão não envia de volta a mudança que ela mesma fez. As outras conexões do time recebem a mudança normalmente. settings.skip_sourceslista origens cujas mudanças a conexão não envia (até 20; letras minúsculas, números,.,_,:e-; um*no fim vale como prefixo). Use para o sistema que escreve na Postal e também no CRM, por exemplo o seu app, que manda contatos pela API com o rótulo de origemmy-app, ecrm:*para ignorar todas as conexões de CRM.
#Conexões nativas
As três funcionam do mesmo jeito: a Postal chama a API do CRM com o seu token (com o tempo limite de 10 segundos, sem seguir redirecionamentos), procura a pessoa pelo vínculo guardado e depois pelo e-mail, e cria ou atualiza o registro. Um CRM que responde 429 é esperado pelo tempo que ele pedir, e um 5xx é tentado de novo. Pessoas sem e-mail são ignoradas. A mensagem de erro do CRM aparece no registro e nunca traz o token.
Para receber mudanças do CRM é preciso configurar o webhook no próprio CRM, com a URL da conexão (webhook_url); sem webhook, a conexão ainda envia dados e lê o CRM quando você pede Sincronizar agora. Cada CRM autentica o webhook de um jeito:
| CRM | Como a Postal confere o webhook |
|---|---|
| HubSpot | Assinatura X-HubSpot-Signature-v3, calculada com o client secret do aplicativo. Sem o client secret nas credenciais, o HubSpot não envia mudanças para a Postal. |
| Pipedrive | Autenticação HTTP básica no webhook: usuário postal e senha igual ao webhook_secret da conexão. |
| RD Station CRM | O RD Station CRM não assina os webhooks: a URL leva ?token=<webhook_secret> (ou o cabeçalho X-Postal-Token). O token na URL é a proteção; não a compartilhe. |
Uma assinatura ou credencial errada responde 401 invalid_signature. Se a segurança do segredo for comprometida, gere outro com POST /crm-connections/{id}/webhook-secret/rotate e atualize o webhook no CRM: o antigo para de valer na hora.
HubSpot
- Credenciais:
access_token(obrigatório) eclient_secret(opcional, só para webhooks). No HubSpot, vá em Configurações › Integrações › Aplicativos privados e crie um aplicativo com permissão de contatos (leitura e escrita), negócios (leitura) e esquemas de contatos e de negócios (leitura); copie o token de acesso. O client secret é o do aplicativo que assina as notificações. - Webhook: no aplicativo, assine contatos (criação e mudança de propriedade) e negócios (criação e mudança de etapa) com a URL da conexão. Exclusões e mesclagens são ignoradas.
- Campos: o registro é o do HubSpot,
{"id", "properties": {…}}, então os caminhos são$.properties.<nome interno>. A Postal lê as propriedades que você mapeia, mais e-mail, nome, sobrenome e telefone. Para negócios,$.deal.stage,$.deal.pipeline,$.deal.status,$.deal.titlee$.deal.value. - Envio: o contato é atualizado pelo vínculo, ou procurado pelo e-mail e criado se não existir. A atividade de e-mail vira uma nota associada ao contato.
Pipedrive
- Credenciais:
api_tokenecompany_domain. No Pipedrive, abra o menu da conta › Preferências pessoais › API e copie o token pessoal. O endereço da conta é só a primeira parte do endereço que você usa no navegador:minhaempresaemminhaempresa.pipedrive.com(letras minúsculas, números e hifens). - Webhook: em Ferramentas e apps › Webhooks, crie um webhook para pessoas e outro para negócios (todos os eventos), com a URL da conexão e a autenticação HTTP básica acima. Exclusões são ignoradas. Uma mudança em um negócio lê a pessoa dele e os nomes da etapa e do funil.
- Campos:
$.email,$.first_name,$.last_name,$.name,$.phone,$.organization,$.id, os campos personalizados da pessoa em$.custom.<chave>e, para negócios,$.deal.stage,$.deal.pipeline,$.deal.status,$.deal.titlee$.deal.value. - Envio: a pessoa é atualizada pelo vínculo ou procurada pelo e-mail e criada se não existir; o e-mail e o telefone novos entram como principais sem apagar os outros. A atividade de e-mail vira uma nota na pessoa.
RD Station CRM
- Credenciais:
token, o token da instância. No RD Station CRM, abra o menu da conta › Configurações › Integrações › Token da API. - Webhook: crie um webhook para contatos e outro para negociações com a URL da conexão incluindo o
?token=(a tela da conexão mostra a URL pronta). Exclusões são ignoradas. - Campos:
$.email,$.first_name,$.last_name,$.name,$.phone,$.title,$.organization,$.id, os campos personalizados do contato em$.custom.<id>e, para negociações,$.deal.stage,$.deal.pipeline,$.deal.status,$.deal.titlee$.deal.value. - Envio: o contato é atualizado pelo vínculo ou procurado pelo e-mail e criado se não existir. A atividade de e-mail vira uma atividade na negociação em aberto do contato; sem negociação, uma linha nas anotações do contato (as mais antigas saem quando passam de 4.000 caracteres).
Permissões do token
Se o CRM recusar o token ou uma permissão (HTTP 401 ou 403), o registro mostra a mensagem e a conexão fica com o aviso de erro até um envio dar certo. Confira o token e as permissões e use Testar conexão.
#Registro de sincronização
Cada webhook recebido, pessoa sincronizada, envio ao CRM, teste e sincronização completa vira uma entrada do registro, em Integrações › CRM (na conexão) e em GET /crm-connections/{id}/logs. Uma entrada tem:
| Campo | Valores |
|---|---|
direction | inbound (do CRM) ou outbound (para o CRM). Filtro: ?direction=. |
object_type | webhook (a entrega recebida), person, activity (atividade de e-mail), test ou sync. |
status | pending (na fila ou esperando nova tentativa), success, failed ou skipped (não havia o que fazer: contato suprimido, sem correspondência, conexão pausada). Filtro: ?status=. |
action | O que aconteceu: created, updated, unchanged, upserted, activity, deleted ou um resumo (“2 pessoas”). |
error, attempts, next_attempt_at | A mensagem, quantas tentativas já houve e quando será a próxima. |
external_id | O ID do registro no CRM, quando se sabe. |
retryable, payload | Se dá para reenviar, e o que foi recebido ou enviado (cortado em 16 KB; um payload cortado não pode ser reenviado). |
As mensagens do registro são escritas em português do Brasil.
{
"object": "list",
"has_more": false,
"data": [
{
"object": "crm_sync_log",
"id": "019a2c10-5e6f-7a81-9b2c-3d4e5f6a7b8c",
"direction": "outbound",
"object_type": "person",
"event": "contact.updated",
"external_id": "51",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"action": null,
"status": "failed",
"error": "HubSpot recusou as credenciais ou a permissão (HTTP 403). Confira o token e as permissões dele.",
"attempts": 6,
"retryable": true,
"payload": {
"id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"email": "[email protected]",
"first_name": "Ana"
},
"next_attempt_at": null,
"created_at": "2026-10-26T14:05:10.000000Z",
"updated_at": "2026-10-26T14:05:11.000000Z"
}
]
}
Novas tentativas
- Envios ao CRM que falham são tentados de novo sozinhos, depois de 30 segundos, 2 minutos, 10 minutos, 30 minutos e 2 horas (6 tentativas ao todo). Acabadas, a entrada vira
failede a conexão mostra o avisoerror(last_error); ela continua funcionando e volta aactiveno primeiro envio que der certo. Se o CRM respondeu429, a Postal espera pelo menos o que ele pediu. - Webhooks recebidos são respondidos na hora com
202e processados em segundo plano. Se o CRM não pôde ser consultado (limite de requisições, fora do ar), a Postal tenta de novo, até 3 vezes. - Uma entrada
failedcom o payload completo se reenvia à mão, no painel ou emPOST /crm-connections/{id}/logs/{log_id}/retry, depois de corrigir a causa (um e-mail que faltava, uma propriedade que não existia, o token).
#Pela API
Tudo o que o painel faz está na API, com uma chave de acesso total (uma chave só de envio recebe 401 restricted_api_key). Os IDs são UUIDs, e as listas seguem a paginação por cursor.
| Endpoint | O que faz |
|---|---|
GET /crm/providers | Os conectores (generic, pipedrive, rdstation_crm, hubspot e as fontes de apps), o limite do plano e o uso de cada contador, e a plataforma parceira que gerencia o time, se houver. |
GET /crm-connections | Lista as conexões. ?limit_key=crm_connections (ou app_integrations) filtra por contador. |
POST /crm-connections | Cria. Corpo: provider, name, credentials e settings. |
GET /crm-connections/{id} | Uma conexão, com o webhook_secret. |
PATCH /crm-connections/{id} | Muda name, status (active ou paused), credentials (substituem todas: mande todas as obrigatórias) e settings (substituem as anteriores: mande o conjunto completo). |
DELETE /crm-connections/{id} | Exclui a conexão e o registro dela. O CRM deixa de sincronizar; os contatos ficam. |
POST /crm-connections/{id}/test | Confere o acesso ao CRM (nos nativos, uma chamada leve; no webhook, um postal.test). Grava uma entrada test no registro. |
POST /crm-connections/{id}/preview | Mostra o que um payload de exemplo mudaria (payload, mais settings opcionais para testar sem salvar), sem gravar nada. Feito para a conexão por webhook. |
GET /crm-connections/{id}/fields | Os campos do CRM para o mapeamento (key, label, type). Nos nativos, vêm do CRM. |
GET /crm-connections/{id}/stages | As etapas de cada funil (pipeline, stage, e status quando o CRM diz). Vazio nos conectores sem funil. |
POST /crm-connections/{id}/sync | Enfileira a sincronização (202). 422 se a conexão está pausada ou o sentido não permite. |
POST /crm-connections/{id}/webhook-secret/rotate | Gera um segredo novo; o anterior para de valer na hora. |
GET /crm-connections/{id}/logs | O registro. Filtros: status e direction. |
POST /crm-connections/{id}/logs/{log_id}/retry | Reenvia uma entrada failed com o payload completo. |
cURL
curl -X POST https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/test', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "crm_connection_test",
"ok": true,
"message": "Connected to HubSpot (12 contacts).",
"status_code": 200
}
Quando o CRM recusa, ok é false e message traz o motivo. fields, stages e a sincronização que não conseguem falar com o CRM respondem 502 crm_request_failed com a mensagem legível.
cURL
curl -X POST https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/sync', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "crm_connection",
"id": "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"sync": "queued"
}
cURL
curl 'https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs?status=failed' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs?status=failed', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "failed",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('crm-connections/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b/logs', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'failed',
],
]);
$data = json_decode($response->getBody(), true);
#Limites
| O quê | Limite |
|---|---|
| Conexões | crm_connections do plano (e app_integrations para as de apps) |
| Corpo de um webhook recebido | 256 KB (413 payload_too_large) |
| Pessoas por webhook recebido | 500 (as que passam disso são ignoradas) |
| Webhooks recebidos | 600 por minuto, por IP de origem |
| Mapeamentos de campos / de etapas | 100 / 100 |
Origens a ignorar (skip_sources) | 20 |
Exemplo de payload guardado (sample_payload) | 32 KB |
| Tempo limite das chamadas ao CRM | 10 segundos, sem seguir redirecionamentos |
| Payload guardado no registro | 16 KB (os webhooks recebidos ficam inteiros, até 256 KB) |
| Páginas lidas do CRM numa sincronização | 1.000 (de 100 a 200 pessoas cada) |
#Erros
| Status | Nome | Quando acontece |
|---|---|---|
401 | invalid_signature | A assinatura ou o token do webhook falta ou está errado, ou a data da assinatura passou de 5 minutos. |
401 | restricted_api_key | Uma chave só de envio tentou usar /crm-connections. |
403 | plan_limit_reached | O plano não inclui conexões de CRM ou já usou todas. |
404 | not_found | A conexão não existe neste time, ou o ID do webhook é de outra conexão. |
409 | connection_paused | A conexão está pausada, ou só envia (não recebe do CRM). |
413 | payload_too_large | O corpo do webhook passa de 256 KB. |
422 | validation_error | Credenciais ou configurações inválidas: sentido desconhecido, campo da Postal ou caminho inválido, propriedade que não existe, evento desconhecido, URL que não é HTTPS, corpo do webhook que não é JSON; ou um reenvio de entrada que não é elegível. |
502 | crm_request_failed | O CRM recusou a chamada da Postal ou não respondeu (campos, etapas e sincronização). |
O formato dos erros e os demais nomes estão em Erros.
#Veja também
- Conexão por webhook: para qualquer outro CRM ou sistema.
- Integre seu app: Stripe, Supabase, Clerk, Auth0 e Firebase, que usam as mesmas conexões.
- Webhooks: os eventos e a assinatura que a conexão reaproveita, e a origem das mudanças.
- Contatos, propriedades e automações.
- Servidor MCP: um assistente de IA também pode consultar e ajustar os contatos que o CRM sincroniza.