Iniciar sesión Crear cuenta
Conceptos

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:

200Respuesta
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "object": "email",
      "id": "019a1b2c-51ab-7d30-8e11-2a3b4c5d6e7f",
      "…": "…"
    },
    {
      "object": "email",
      "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
      "…": "…"
    }
  ]
}
ParámetroDescripción
limitElementos por página, de 1 a 100. Por defecto: 20.
afterID 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.
beforeID 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:

Paginación

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.

EndpointFiltros
GET /emailssearch, status, domain_id, api_key_id, start_date, end_date
GET /emails/receivinginbox_id, unread, search
GET /inboxes/{id}/threadsfolder, label, unlabeled, assigned_to, unread, search
GET /inboxes/{id}/draftsthread_id
GET /domainssearch, status, region
GET /api-keyssearch, permission
GET /templatessearch, status
GET /layoutssearch, category
GET /contactssearch, segment_id, topic_id, tag, unsubscribed, email_status, external_id (o user_id), source
GET /segmentssearch, type (static, dynamic)
GET /forms, GET /tagssearch
GET /forms/submissions, GET /forms/{id}/submissionsform_id (solo en la primera), search, status, start_date, end_date
GET /broadcastssearch, status
GET /broadcasts/{id}/recipientsstatus, search
GET /eventsname, contact
GET /automationssearch, status
GET /automations/{id}/runsstatus, contact
GET /email-verificationspreview
GET /logsstatus, 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.