Eventos
Envía lo que ocurre en tu producto para iniciar automatizaciones y personalizar correos electrónicos.
Los eventos le cuentan a Postal lo que ocurre en tu producto: una prueba gratis que empezó, un pedido pagado, un carrito abandonado. Cada evento puede estar vinculado a un contacto y activa las automatizaciones con el disparador de ese nombre. Para enviar eventos, basta una clave solo de envío, la misma de tu backend; para consultar, una de acceso total. Los eventos se guardan durante 90 días.
El evento también puede crear o actualizar el contacto (por ID, correo electrónico o external_id, con contact_properties) antes de iniciar las automatizaciones, así que un solo POST /events basta para registrar a la persona y reaccionar a lo que hizo. Los SDKs de Node.js y PHP (events.send) y la CLI (postal events send) envían eventos.
#Enviar un evento
POST/events
Registra el evento y, si tiene contacto, inicia las automatizaciones activas cuyo disparador es este evento (y cuyos filtros coinciden con las propiedades). En contact, un correo electrónico que aún no está en la audiencia crea el contacto, lo que dispara el webhook contact.created y las automatizaciones de «contacto creado». Sin contact, el evento queda registrado, pero no inicia automatizaciones.
Una dirección de la lista de supresión nunca se convierte en contacto: la respuesta es 422 email_suppressed y el evento no se registra. Con contact_properties, el contacto se actualiza antes de que empiecen las automatizaciones, así que tanto las de contact.property_changed como las de evento ven los valores nuevos.
Acepta Idempotency-Key: repetir la solicitud con la misma clave dentro de 24 horas devuelve la misma respuesta, sin registrar el evento de nuevo ni iniciar otra ejecución.
Cabeceras
Idempotency-KeystringIdentificador único del evento en tu sistema, hasta 256 caracteres, comotrial-8812. Recomendado.X-Postal-SourcestringEtiqueta del origen del cambio en el contacto (consultasourcemás abajo).
Cuerpo de la solicitud
namestringobligatorioNombre del evento, hasta 100 caracteres: letras, números,.,_,:y-, empezando por letra o número. Ejemplos:trial.started,order_paid,checkout:abandoned.contactstring | objectID o correo electrónico del contacto. Un correo desconocido crea el contacto; un ID desconocido genera404. También aceptaexternal_id:<id>(ouser_id:<id>), para encontrar el contacto por su ID en tu sistema, o un objeto.idstringID del contacto.emailstringCorreo electrónico del contacto. Crea el contacto si no existe.external_idstringID de la persona en tu sistema, hasta 255 caracteres. Alias:user_id.first_namestringNome.last_namestringSobrenome.
external_idstringAtajo decontact.external_id, hasta 255 caracteres (alias:user_id). El contacto se busca por ID, luego porexternal_idy luego por correo electrónico; unexternal_idsin contacto correspondiente y sin correo electrónico genera422.contact_propertiesobjectPropiedades del contacto que se van a actualizar, por clave, como{"plan": "pro"}. La propiedad debe existir en propiedades de contacto y el valor sigue su tipo; un valor vacío elimina la propiedad.sourcestringEtiqueta de dónde vino el cambio en el contacto, hasta 64 caracteres (letras minúsculas, números,.,_,:y-), comomeu-app. Va en el camposourcede los webhookscontact.*. También puede ir en el encabezadoX-Postal-Source; si llegan los dos, vale el del cuerpo. Las etiquetas propias de Postal (comodashboard,form,importyautomation) y las que empiezan porcrm:están reservadas.propertiesobjectDatos del evento, en JSON, hasta 16 KB. Quedan disponibles en los filtros y condiciones de las automatizaciones y como variables en los correos electrónicos ({{event.plan}},{{event.items.0.name}}).occurred_atstringCuándo ocurrió, en ISO 8601. Valor predeterminado: ahora. No puede superar las 24 horas en el futuro.
Errores
- 422
validation_errorNombre ausente o no válido,propertiesque no es un objeto o supera los 16 KB,contactque no es un ID ni un correo electrónico,occurred_aten el futuro,sourceno válido o reservado,contact_propertiescon una propiedad que no existe o un valor del tipo equivocado, o unexternal_idsin contacto correspondiente y sin correo electrónico. - 422
email_suppressedEl correo electrónico está en la lista de supresión y no puede convertirse en contacto. Quítalo de ahí primero. - 404
not_foundNingún contacto con el ID decontact. - 403
plan_limit_reachedSe crearía el contacto, pero el plan ya no permite más contactos. - 409
invalid_idempotent_requestLa mismaIdempotency-Keyse usó con otro cuerpo.
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
Del más reciente al más antiguo, de los últimos 90 días.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.namestringUno o más nombres separados por comas, comotrial.started,order.paid.contactstringID o correo electrónico del contacto.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál.
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 nombres de eventos
GET/events/names
Los nombres de evento que envió el equipo, del visto más recientemente al más antiguo (hasta 100), con cuántos hay guardados de cada uno. Útil para armar disparadores y 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"
}
]
}
#Obtener un evento
GET/events/{id}
Parámetros de ruta
idstringobligatorioID del evento.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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"
}
#Buenas prácticas
- Envía desde tu servidor, nunca desde el navegador: la clave de API no puede quedar expuesta. Basta una clave solo de envío.
- Usa una
Idempotency-Keyderivada del hecho (order-10482-paid), no del intento: repetir después de un timeout no inicia la automatización dos veces. - Elige nombres estables, en pasado o como hechos (
order.paid), y manda enpropertiessolo lo que vayas a usar en filtros, condiciones y correos electrónicos. No envíes contraseñas, números de tarjeta ni datos sensibles. - Las propiedades de primer nivel (
plan) se convierten automáticamente en variables de plantilla ({{{plan}}}y{{{PLAN}}}); las propiedades anidadas se leen con rutas, 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);