Iniciar sesión Crear cuenta
Guías

SDKs: Node.js, PHP y Laravel

Bibliotecas oficiales para enviar correos electrónicos y eventos, mantener contactos y comprobar webhooks, con errores tipados, idempotencia y reintentos.

#Visión general

Las bibliotecas oficiales se encargan de lo que escribirías a mano: autenticación, JSON, errores tipados, clave de idempotencia, reintentos con espera creciente y la comprobación de la firma de los webhooks. Los campos tienen los mismos nombres que la API, así que la referencia vale directamente para el código.

LenguajePaqueteRequisitosInstalación
Node.js y TypeScriptpostal-soNode 18+, ESM, con tipos, sin dependenciasnpm install postal-so
PHP y Laravelpostal-so/postal-phpPHP 8.2+, ext-curl, sin dependencias obligatoriascomposer require postal-so/postal-php

El código está en postal-node y postal-php. El paquete de Node incluye también la CLI postal y una skill para agentes de IA. ¿Quieres usar otro lenguaje? La API es HTTP con JSON: consulta los ejemplos en cURL, Python y PHP de cada endpoint.

¿Ya usas el SDK de Resend?

Los SDKs de Resend también funcionan: apunta la URL base a https://api.postal.so y usa tu clave de Postal. Consulta Migrar desde Resend. En Laravel, el transporte postal de este paquete (ver abajo) se encarga de Mail::to() sin otro paquete.

#Clave de API

Crea una clave en Claves de API (empieza por ps_) y guárdala en POSTAL_API_KEY. Los dos SDKs leen esa variable cuando no pasas la clave. Para sistemas que solo disparan correos electrónicos y eventos, usa una clave solo de envío. Consulta Autenticación.

.env
POSTAL_API_KEY=ps_sua_chave

Solo en el servidor

Nunca pongas la clave en el navegador, en apps ni en el repositorio: cualquier persona podría copiarla.

#Enviar un correo electrónico

El dominio del remitente debe estar verificado. La respuesta trae el id del correo; sigue el estado con emails.get o con webhooks.

Enviar un correo

Node.js

import { Postal } from 'postal-so';

const postal = new Postal(); // lê POSTAL_API_KEY

const { id } = await postal.emails.send(
  {
    from: 'Minha empresa <[email protected]>',
    to: '[email protected]',
    subject: 'Bem-vindo!',
    html: '<p>Que bom ter você por aqui.</p>',
    text: 'Que bom ter você por aqui.',
  },
  { idempotencyKey: 'welcome-42' },
);

PHP

use Postal\Client;

$postal = new Client(); // lê POSTAL_API_KEY

$result = $postal->emails->send([
    'from' => 'Minha empresa <[email protected]>',
    'to' => '[email protected]',
    'subject' => 'Bem-vindo!',
    'html' => '<p>Que bom ter você por aqui.</p>',
    'text' => 'Que bom ter você por aqui.',
], 'welcome-42'); // chave de idempotência

echo $result['id'];

Con una plantilla publicada, envía template: { id, variables } en lugar de html y text. También funcionan emails.batch (hasta 100 correos en una solicitud), emails.update y emails.cancel (reprogramar y cancelar), emails.get y emails.list. Consulta Correos.

Para correos con código de verificación, otpTokenHeader(code, origin) (Node.js) y Postal\OtpToken::header($code, $origin) (PHP) montan el valor del encabezado OTP-Token con el escape correcto y comprueban el origen antes de llamar a la API. Consulta Encabezado OTP-Token.

#Contactos y eventos

external_id es el ID de la persona en tu sistema. contacts.upsert crea o actualiza el contacto buscando por id, luego external_id, luego email; repetir la llamada es seguro. Los eventos inician las automatizaciones con ese disparador y funcionan con clave solo de envío.

Contactos y eventos

Node.js

// Cria ou atualiza o contato pelo ID dele no seu sistema
await postal.contacts.upsert({
  external_id: 'user_42',
  email: '[email protected]', // obrigatório para criar
  first_name: 'Fulano',
  properties: { plan: 'free' },
});

// Conta ao Postal o que aconteceu: inicia as automações do gatilho "signup"
await postal.events.send(
  {
    name: 'signup',
    contact: 'external_id:user_42',
    properties: { plan: 'free', source: 'site' },
  },
  { idempotencyKey: 'signup-42' },
);

PHP

// Cria ou atualiza o contato pelo ID dele no seu sistema
$postal->contacts->upsert([
    'external_id' => 'user_42',
    'email' => '[email protected]', // obrigatório para criar
    'first_name' => 'Fulano',
    'properties' => ['plan' => 'free'],
]);

// Conta ao Postal o que aconteceu: inicia as automações do gatilho "signup"
$postal->events->send([
    'name' => 'signup',
    'contact' => 'external_id:user_42',
    'properties' => ['plan' => 'free', 'source' => 'site'],
], 'signup-42');

Más métodos: contacts.get, getByExternalId, update, delete, list e iterate (recorre todas las páginas), segments, topics y domains (list, get, verify). Consulta Contactos, Eventos y Dominios.

#Verificar webhooks

verifyWebhook (Node.js) y Webhook::verify (PHP) comprueban la firma Standard Webhooks de la entrega, rechazan mensajes con más de 5 minutos de diferencia y devuelven el evento. Pasa el cuerpo crudo, exactamente como llegó, y el signing_secret (whsec_…). Si la firma no coincide, lanzan una excepción: responde 400. Los detalles del esquema están en Webhooks.

Verificar una entrega

Node.js

import express from 'express';
import { verifyWebhook, PostalWebhookVerificationError } from 'postal-so';

const app = express();

// O corpo precisa chegar cru (Buffer) para a assinatura bater.
app.post('/webhooks/postal', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook(req.body, req.headers, process.env.POSTAL_WEBHOOK_SECRET);

    if (event.type === 'email.bounced') {
      // descarte o endereço
    }

    res.sendStatus(200);
  } catch (error) {
    if (error instanceof PostalWebhookVerificationError) {
      return res.sendStatus(400);
    }

    throw error;
  }
});

PHP

use Postal\Exceptions\WebhookVerificationException;
use Postal\Webhook;

try {
    $event = Webhook::verify(
        file_get_contents('php://input'), // corpo cru
        getallheaders(),
        getenv('POSTAL_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}

if ($event['type'] === 'email.bounced') {
    // descarte o endereço
}

http_response_code(200);

#Errores y reintentos

Toda respuesta de error se convierte en una excepción con el statusCode, el name (el código estable de la lista de errores, como validation_error) y el message de Postal. Trata el error por el name, no por el mensaje. Los fallos de red y el tiempo agotado se convierten en errores con statusCode 0 y name network_error o timeout.

Tratar errores

Node.js

import { PostalError } from 'postal-so';

try {
  await postal.emails.send(params);
} catch (error) {
  if (error instanceof PostalError) {
    console.error(error.statusCode, error.name, error.message);
    // 422 validation_error Missing `subject` field.

    if (error.name === 'daily_quota_exceeded') {
      // a cota diária acabou; volta às 00:00 UTC
    }
  }

  throw error;
}

PHP

use Postal\Exceptions\PostalException;

try {
    $postal->emails->send($params);
} catch (PostalException $e) {
    $e->getStatusCode(); // 422
    $e->getErrorName();  // 'validation_error'
    $e->getMessage();    // 'Missing `subject` field.'

    if ($e->getErrorName() === 'daily_quota_exceeded') {
        // a cota diária acabou; volta às 00:00 UTC
    }
}

Reintentos e idempotencia

  • Por defecto, los SDKs reintentan hasta 2 veces, con una espera de 500 ms que se duplica en cada intento (máximo 30 s). Cámbialo con maxRetries (Node.js) o max_retries (PHP); 0 lo desactiva.
  • 429 rate_limit_exceeded se reintenta siempre, esperando el Retry-After. La cuota agotada (daily_quota_exceeded, monthly_quota_exceeded) no se reintenta.
  • Los 5xx y los fallos de red se reintentan en lecturas, en eliminaciones, en contacts.upsert y en envíos con clave de idempotencia. Un POST sin clave, como contacts.create, no se reintenta, para no duplicar el efecto.
  • En emails.send, emails.batch y events.send, el SDK genera una clave de idempotencia para la llamada y la reutiliza en los reintentos. Para proteger también tu repetición (un job que se ejecuta de nuevo), pasa una clave que identifique el motivo del envío, como order-10482-shipped.

#Laravel

El paquete de PHP se registra solo en Laravel (auto-discovery). Crea el Postal\Client en el contenedor, leyendo POSTAL_API_KEY, y un transporte de correo llamado postal: con él, Mail::to(…)->send(…), las notificaciones y los Mailables salen por la API de Postal, sin cambiar el código de la aplicación.

Define el mailer en el .env y declara el transporte en config/mail.php:

.env
MAIL_MAILER=postal
[email protected]
MAIL_FROM_NAME="Minha empresa"
POSTAL_API_KEY=ps_sua_chave
config/mail.php
// config/mail.php
'mailers' => [
    'postal' => [
        'transport' => 'postal',
        // 'key' => env('POSTAL_API_KEY'), // opcional: uma chave só deste mailer
    ],
],

El dominio de MAIL_FROM_ADDRESS debe estar verificado. Se envían también los adjuntos, las imágenes incrustadas, Cc, Bcc, Reply-To, los encabezados adicionales y las etiquetas (tag() y metadata() del Mailable se convierten en etiquetas del envío). El ID del correo en Postal queda en $sentMessage->getMessageId().

Para eventos y contactos, inyecta el cliente:

app/Listeners/SendSignupEvent.php
use Postal\Client;

class SendSignupEvent
{
    public function __construct(private Client $postal) {}

    public function handle(Registered $event): void
    {
        $user = $event->user;

        $this->postal->contacts->upsert([
            'external_id' => (string) $user->id,
            'email' => $user->email,
            'first_name' => $user->name,
        ]);

        $this->postal->events->send(
            ['name' => 'signup', 'contact' => 'external_id:'.$user->id],
            'signup-'.$user->id,
        );
    }
}

Quien prefiera SMTP puro puede usar el SMTP de Postal en Laravel; el transporte postal usa la API y devuelve el ID del correo.

#Agentes de IA

El paquete de Node.js incluye una skill (skills/postal/SKILL.md) que enseña a Claude Code, Codex, Cursor y otros agentes a integrar Postal en tu app: instalar el SDK, guardar la clave, enviar correos electrónicos, mandar eventos de registro y pago, sincronizar contactos y verificar webhooks. Copia la carpeta skills/postal a la carpeta de skills de tu herramienta (por ejemplo, .claude/skills/) o pide al agente que lea el archivo.

Hay también un /llms.txt, con el resumen de la documentación y los enlaces de las páginas, para agentes que leen la documentación directamente. Y, para que el asistente de IA use tu cuenta por conversación (enviar, consultar, organizar contactos), existe el servidor MCP.