Paginación y filtros
Las listas usan paginación por cursor con limit, after y before, y aceptan filtros en la query string.
#Cómo funciona
Los endpoints que listan recursos (GET /emails, GET /domains, GET /logs…) devuelven los elementos del más reciente al más antiguo, una página cada vez:
{
"object": "list",
"has_more": true,
"data": [
{
"object": "email",
"id": "019a1b2c-51ab-7d30-8e11-2a3b4c5d6e7f",
"…": "…"
},
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"…": "…"
}
]
}
| Parámetro | Descripción |
|---|---|
limit | Elementos por página, de 1 a 100. Por defecto: 20. |
after | ID de un elemento: devuelve los elementos más antiguos que él. Usa el ID del último elemento de la página para ir a la siguiente. |
before | ID de un elemento: devuelve los elementos más recientes que él. Usa el ID del primer elemento para volver una página. |
has_more indica si hay más elementos en la dirección pedida. No se pueden usar after y before juntos (422). Como el cursor es el ID, los elementos creados mientras paginas no hacen que la lista salte ni repita elementos. El cursor debe ser el UUID de un elemento, y un limit fuera del rango de 1 a 100 también da 422.
#Recorrer todas las páginas
Para leer una lista completa, pide páginas de 100 y sigue el after hasta que has_more sea false:
cURL
# Primeira página
curl "https://api.postal.so/emails?limit=100" \
-H "Authorization: Bearer $POSTAL_API_KEY"
# Próxima página: o ID do último item recebido
curl "https://api.postal.so/emails?limit=100&after=019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b" \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
async function listAll(path) {
const items = [];
let after;
do {
const url = new URL(path, 'https://api.postal.so');
url.searchParams.set('limit', '100');
if (after) url.searchParams.set('after', after);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.POSTAL_API_KEY}` },
});
const page = await response.json();
items.push(...page.data);
after = page.has_more ? page.data.at(-1).id : undefined;
} while (after);
return items;
}
const bounces = await listAll('/emails?status=bounced');
Python
import os
import requests
def list_all(path, **filters):
items, after = [], None
while True:
params = {"limit": 100, **filters}
if after:
params["after"] = after
page = requests.get(
f"https://api.postal.so{path}",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params=params,
timeout=10,
).json()
items.extend(page["data"])
if not page["has_more"]:
return items
after = page["data"][-1]["id"]
bounces = list_all("/emails", status="bounced")
PHP
function listAll(string $path, array $filters = []): array
{
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$items = [];
$after = null;
do {
$page = json_decode($client->get($path, [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [...$filters, 'limit' => 100, ...($after ? ['after' => $after] : [])],
])->getBody(), true);
array_push($items, ...$page['data']);
$after = $page['has_more'] ? end($page['data'])['id'] : null;
} while ($after !== null);
return $items;
}
$bounces = listAll('emails', ['status' => 'bounced']);
Para grandes volúmenes, prefiere las exportaciones en CSV: usan los mismos filtros y no gastan tu límite de solicitudes.
#Filtros
Varias listas aceptan filtros en la query string, combinados con la paginación. Los filtros con varios valores aceptan una lista separada por comas, como status=bounced,complained.
| Endpoint | Filtros |
|---|---|
GET /emails | search, status, domain_id, api_key_id, start_date, end_date |
GET /emails/receiving | inbox_id, unread, search |
GET /inboxes/{id}/threads | folder, label, unlabeled, assigned_to, unread, search |
GET /inboxes/{id}/drafts | thread_id |
GET /domains | search, status, region |
GET /api-keys | search, permission |
GET /templates | search, status |
GET /layouts | search, category |
GET /contacts | search, segment_id, topic_id, tag, unsubscribed, email_status, external_id (o user_id), source |
GET /segments | search, type (static, dynamic) |
GET /forms, GET /tags | search |
GET /forms/submissions, GET /forms/{id}/submissions | form_id (solo en la primera), search, status, start_date, end_date |
GET /broadcasts | search, status |
GET /broadcasts/{id}/recipients | status, search |
GET /events | name, contact |
GET /automations | search, status |
GET /automations/{id}/runs | status, contact |
GET /email-verifications | preview |
GET /logs | status, method, source, user_agent, api_key_id, search, start_date, end_date |
#Filtros de fecha
Las listas con start_date y end_date (correos, registros, respuestas de formularios y las exportaciones que usan esos filtros) conservan lo que se creó entre las dos fechas, ambas inclusive. Cada una acepta una fecha (2026-10-01) o una fecha y hora ISO 8601 (2026-10-01T09:30:00-03:00); sin zona horaria, vale UTC. Una end_date solo con la fecha cubre el día completo (hasta las 23:59:59 UTC), así que start_date=2026-10-01&end_date=2026-10-01 devuelve todo el día 1. Si start_date es posterior a end_date, o si una de las fechas no es válida, la respuesta es 422 validation_error.
Los endpoints de métricas y de análisis de los formularios tienen su propio período y una zona horaria (timezone o tz): en las métricas, una fecha sin hora vale 00:00 de esa zona (para incluir el último día, envía 2026-10-22T23:59:59), y el final nunca pasa de ahora.