Sign in Create account
Getting started

Quickstart: send your first email

From zero to your first delivered email: verify a domain, create a key and make a POST /emails request.

#1. Verify a domain

Postal only sends from domains you have proven you control. In the dashboard, under Domains, add your domain (for example, acmestore.com) and create the records it shows at your DNS provider: a DKIM TXT record, plus an MX and a TXT record for the Return-Path. Verification usually takes a few minutes. Details in Domains and DNS.

#2. Create an API key

Under API keys, create a sending-only key and copy the token (it is shown once). Keep it in an environment variable:

Terminal
export POSTAL_API_KEY="ps_your_key"

#3. Send your first email

Replace the sender with an address on your verified domain and the recipient with your own email:

POST /emails

cURL

curl -X POST https://api.postal.so/emails \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme Store <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your order #10482 is out for delivery",
    "html": "<p>Order <b>#10482</b> is on its way.</p>"
  }'

Node.js

const response = await fetch('https://api.postal.so/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'Acme Store <[email protected]>',
    to: ['[email protected]'],
    subject: 'Your order #10482 is out for delivery',
    html: '<p>Order <b>#10482</b> is on its way.</p>',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/emails",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "from": "Acme Store <[email protected]>",
        "to": ["[email protected]"],
        "subject": "Your order #10482 is out for delivery",
        "html": "<p>Order <b>#10482</b> is on its way.</p>",
    },
    timeout=10,
)

data = response.json()

PHP

<?php

$ch = curl_init('https://api.postal.so/emails');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('POSTAL_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'from' => 'Acme Store <[email protected]>',
        'to' => ['[email protected]'],
        'subject' => 'Your order #10482 is out for delivery',
        'html' => '<p>Order <b>#10482</b> is on its way.</p>',
    ]),
]);

$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

echo $status === 200 ? $data['id'] : $data['message'];

Laravel

// config/services.php
'postal' => [
    'key' => env('POSTAL_API_KEY'),
],

// Anywhere in your application
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.postal.key'))
    ->withHeaders(['Idempotency-Key' => 'order-10482-shipped'])
    ->post('https://api.postal.so/emails', [
        'from' => 'Acme Store <[email protected]>',
        'to' => ['[email protected]'],
        'subject' => 'Your order #10482 is out for delivery',
        'html' => view('emails.order-shipped', ['order' => $order])->render(),
    ])
    ->throw();

$emailId = $response->json('id');

The response carries the email ID. If something is wrong, you get an error with the reason, such as 422 validation_error or 403 for an unverified domain.

200Response
{
  "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}

Laravel with the framework Mail

Prefer to keep using Mail::to() and Mailables? Configure the SMTP mailer with your key and nothing else changes in your code.

#4. Track the delivery

Fetch the email to see its status and events (sent, delivered, opened…):

GET /emails/{id}

cURL

curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', {
  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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
    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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);

In production, instead of polling, register a webhook and receive each event as it happens. To read the status through the API, the key needs full access.

#Next steps