Eventos
Envie o que acontece no seu produto para iniciar automações e personalizar e-mails.
Eventos contam à Postal o que acontece no seu produto: um teste grátis que começou, um pedido pago, um carrinho abandonado. Cada evento pode estar ligado a um contato e dispara as automações com o gatilho daquele nome. Para enviar eventos, basta uma chave só de envio, a mesma do seu backend; para consultar, uma de acesso total. Os eventos ficam guardados por 90 dias.
O evento também pode criar ou atualizar o contato (por ID, e-mail ou external_id, com contact_properties) antes de iniciar as automações, então um só POST /events basta para cadastrar a pessoa e reagir ao que ela fez. Os SDKs de Node.js e PHP (events.send) e a CLI (postal events send) enviam eventos.
#Enviar um evento
POST/events
Registra o evento e, se ele tem contato, inicia as automações ativas cujo gatilho é este evento (e cujos filtros batem com as propriedades). Em contact, um e-mail que ainda não está na audiência cria o contato, o que dispara o webhook contact.created e as automações de “contato criado”. Sem contact, o evento fica registrado, mas não inicia automações.
Um endereço da lista de supressão nunca vira contato: a resposta é 422 email_suppressed e o evento não é registrado. Com contact_properties, o contato é atualizado antes de as automações começarem, então as automações de contact.property_changed e as de evento enxergam os valores novos.
Aceita Idempotency-Key: repetir a requisição com a mesma chave em até 24 horas devolve a mesma resposta, sem registrar o evento de novo nem iniciar outra execução.
Cabeçalhos
Idempotency-KeystringIdentificador único do evento no seu sistema, até 256 caracteres, comotrial-8812. Recomendado.X-Postal-SourcestringRótulo da origem da mudança no contato (vejasourceabaixo).
Corpo da requisição
namestringobrigatórioNome do evento, até 100 caracteres: letras, números,.,_,:e-, começando com letra ou número. Exemplos:trial.started,order_paid,checkout:abandoned.contactstring | objectID ou e-mail do contato. Um e-mail desconhecido cria o contato; um ID desconhecido gera404. Também aceitaexternal_id:<id>(ouuser_id:<id>), para achar o contato pelo ID dele no seu sistema, ou um objeto.idstringID do contato.emailstringE-mail do contato. Cria o contato se ele não existir.external_idstringID da pessoa no seu sistema, até 255 caracteres. Alias:user_id.first_namestringNome.last_namestringSobrenome.
external_idstringAtalho paracontact.external_id, até 255 caracteres (alias:user_id). O contato é procurado por ID, depois porexternal_ide depois por e-mail; umexternal_idsem contato correspondente e sem e-mail gera422.contact_propertiesobjectPropriedades do contato a atualizar, pela chave, como{"plan": "pro"}. A propriedade precisa existir em propriedades de contato e o valor segue o tipo dela; um valor vazio remove a propriedade.sourcestringRótulo de onde veio a mudança no contato, até 64 caracteres (letras minúsculas, números,.,_,:e-), comomeu-app. Vai no camposourcedos webhookscontact.*. Também pode ir no cabeçalhoX-Postal-Source; se vierem os dois, vale o do corpo. Rótulos da própria Postal (comodashboard,form,importeautomation) e os que começam comcrm:são reservados.propertiesobjectDados do evento, em JSON, até 16 KB. Ficam disponíveis nos filtros e condições das automações e como variáveis nos e-mails ({{event.plan}},{{event.items.0.name}}).occurred_atstringQuando aconteceu, em ISO 8601. Padrão: agora. Não pode passar de 24 horas no futuro.
Erros
- 422
validation_errorNome ausente ou inválido,propertiesque não é objeto ou passa de 16 KB,contactque não é ID nem e-mail,occurred_atno futuro,sourceinválido ou reservado,contact_propertiescom uma propriedade que não existe ou um valor do tipo errado, ou umexternal_idsem contato correspondente e sem e-mail. - 422
email_suppressedO e-mail está na lista de supressão e não pode virar contato. Remova-o de lá primeiro. - 404
not_foundNenhum contato com o ID decontact. - 403
plan_limit_reachedO contato seria criado, mas o plano não permite mais contatos. - 409
invalid_idempotent_requestA mesmaIdempotency-Keyfoi usada com outro corpo.
cURL
curl -X POST https://api.postal.so/events \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Idempotency-Key: trial-8812" \
-H "Content-Type: application/json" \
-d '{
"name": "trial.started",
"contact": "[email protected]",
"properties": {
"plan": "pro",
"source": "website",
"trial_days": 14
}
}'
Node.js
const response = await fetch('https://api.postal.so/events', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'trial-8812',
},
body: JSON.stringify({
name: 'trial.started',
contact: '[email protected]',
properties: {
plan: 'pro',
source: 'website',
trial_days: 14,
},
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/events",
headers={
"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
"Idempotency-Key": "trial-8812",
},
json={
"name": "trial.started",
"contact": "[email protected]",
"properties": {
"plan": "pro",
"source": "website",
"trial_days": 14,
},
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('events', [
'headers' => [
'Authorization' => 'Bearer '.getenv('POSTAL_API_KEY'),
'Idempotency-Key' => 'trial-8812',
],
'json' => [
'name' => 'trial.started',
'contact' => '[email protected]',
'properties' => [
'plan' => 'pro',
'source' => 'website',
'trial_days' => 14,
],
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "event",
"id": "019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}
{
"name": "order.paid",
"contact": {
"external_id": "user_42",
"email": "[email protected]"
},
"properties": {
"order_id": "10482",
"total": 199.9
},
"contact_properties": {
"plan": "pro"
},
"source": "meu-app"
}
#Listar eventos
GET/events
Do mais recente para o mais antigo, dos últimos 90 dias.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.namestringUm ou mais nomes separados por vírgula, comotrial.started,order.paid.contactstringID ou e-mail do contato.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual.
cURL
curl 'https://api.postal.so/events?name=trial.started&contact=ana%40exemplo.com.br' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/events?name=trial.started&contact=ana%40exemplo.com.br', {
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/events",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"name": "trial.started",
"contact": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('events', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'name' => 'trial.started',
'contact' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"object": "event",
"id": "019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e",
"name": "trial.started",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"contact_email": "[email protected]",
"properties": {
"plan": "pro",
"source": "website",
"trial_days": 14
},
"occurred_at": "2026-10-24T14:20:00.000000Z",
"created_at": "2026-10-24T14:20:01.000000Z"
}
]
}
#Listar nomes de eventos
GET/events/names
Os nomes de evento que o time enviou, do visto mais recentemente para o mais antigo (até 100), com quantos de cada há guardados. Útil para montar gatilhos e filtros.
cURL
curl https://api.postal.so/events/names \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/events/names', {
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/events/names",
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->get('events/names', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"name": "trial.started",
"count": 812,
"last_seen_at": "2026-10-24T14:20:01.000000Z"
},
{
"name": "order.paid",
"count": 4310,
"last_seen_at": "2026-10-24T14:02:47.000000Z"
}
]
}
#Obter um evento
GET/events/{id}
Parâmetros de caminho
idstringobrigatórioID do evento.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/events/019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/events/019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e', {
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/events/019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e",
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->get('events/019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "event",
"id": "019a1f40-5f60-7b72-9d8e-9f0a1b2c3d4e",
"name": "trial.started",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"contact_email": "[email protected]",
"properties": {
"plan": "pro",
"source": "website",
"trial_days": 14
},
"occurred_at": "2026-10-24T14:20:00.000000Z",
"created_at": "2026-10-24T14:20:01.000000Z"
}
#Boas práticas
- Envie do seu servidor, nunca do navegador: a chave de API não pode ficar exposta. Uma chave só de envio basta.
- Use uma
Idempotency-Keyderivada do fato (order-10482-paid), não da tentativa: repetir depois de um timeout não inicia a automação duas vezes. - Escolha nomes estáveis, no passado ou como fatos (
order.paid), e mande empropertiessó o que vai usar em filtros, condições e e-mails. Não envie senhas, números de cartão ou dados sensíveis. - Propriedades no primeiro nível (
plan) viram variáveis de template automaticamente ({{{plan}}}e{{{PLAN}}}); propriedades aninhadas são lidas com caminhos, comoitems.0.name.
cURL
# No seu backend, logo depois de liberar o teste grátis
curl -X POST https://api.postal.so/events \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Idempotency-Key: trial-8812" \
-H "Content-Type: application/json" \
-d '{"name": "trial.started", "contact": "[email protected]", "properties": {"plan": "pro", "source": "website"}}'
Node.js
import { Postal } from 'postal-so'
const postal = new Postal() // lê POSTAL_API_KEY
// No seu backend, logo depois de liberar o teste grátis
await postal.events.send(
{
name: 'trial.started',
contact: user.email,
properties: { plan: account.plan, source: account.source },
},
{ idempotencyKey: `trial-${account.id}` },
)
Python
import os
import requests
# No seu backend, logo depois de liberar o teste grátis
requests.post(
"https://api.postal.so/events",
headers={
"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
"Idempotency-Key": f"trial-{account.id}",
},
json={
"name": "trial.started",
"contact": user.email,
"properties": {"plan": account.plan, "source": account.source},
},
timeout=10,
)
PHP
use Postal\Client;
$postal = new Client(); // lê POSTAL_API_KEY
// No seu backend, logo depois de liberar o teste grátis
$postal->events->send([
'name' => 'trial.started',
'contact' => $user->email,
'properties' => ['plan' => $account->plan, 'source' => $account->source],
], 'trial-'.$account->id);