PigeonAtlas

Documentation

Send email over HTTP or SMTP. The request and response shapes match Resend's, so code written against them (including code an AI assistant wrote for you) works here after changing the base URL and the key.

If an assistant is writing that code, hand it one of the prompts instead of this page: each one carries the whole contract, so it has nothing left to guess about.

Quickstart: your first email in four steps

Transactional email works on every plan, Free included. Each step below is also a button in your dashboard's setup guide, which fills in your own domain and address.

  1. 1

    Add your sending domain

    In the dashboard, add a subdomain such as mail.yourcompany.com. Newsletters need a separate one, such as news.yourcompany.com: each domain sends one kind of mail and we enforce it, so complaints about a newsletter can never hurt your password resets.

  2. 2

    Publish three DNS records

    We show a DKIM key, an SPF record and a bounce (MAIL FROM) record. Add them at your DNS provider; we check every few minutes and email you when the domain is verified. You can only send from a verified domain: that is what stops anyone sending as someone else.

  3. 3

    Create an API key

    Name it after where it lives (Production, Staging). It is shown once and we store only a hash, so copy it into your server's environment as PIGEONATLAS_API_KEY.

  4. 4

    Send

    Make one POST to /v1/emails, as below. The same request works with Resend's SDKs if you change the base URL.

Newsletters to a list are a separate product, included from the Growth plan. How newsletters work.

Authentication

Your key goes in the Authorization header on every request.

Header
Authorization: Bearer pa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The same key is the password for SMTP. Keep it server-side: anyone holding it can send as your verified domains.

Send an email

POST https://pigeonatlas.com/v1/emails. The response comes back as soon as the message is queued, so a slow recipient never slows your request.

curl -X POST https://pigeonatlas.com/v1/emails \
  -H "Authorization: Bearer $PIGEONATLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mail.acme.com>",
    "to": ["customer@example.com"],
    "subject": "Order #4471 confirmed",
    "html": "<p>Thanks for your order.</p>",
    "text": "Thanks for your order."
  }'
const response = await fetch("https://pigeonatlas.com/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PIGEONATLAS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "Acme <hello@mail.acme.com>",
    to: ["customer@example.com"],
    subject: "Order #4471 confirmed",
    html: "<p>Thanks for your order.</p>",
    text: "Thanks for your order.",
  }),
});

const { id } = await response.json();
import os, requests

response = requests.post(
    "https://pigeonatlas.com/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['PIGEONATLAS_API_KEY']}"},
    json={
        "from": "Acme <hello@mail.acme.com>",
        "to": ["customer@example.com"],
        "subject": "Order #4471 confirmed",
        "html": "<p>Thanks for your order.</p>",
        "text": "Thanks for your order.",
    },
)

message_id = response.json()["id"]
require "net/http"
require "json"

uri = URI("https://pigeonatlas.com/v1/emails")
request = Net::HTTP::Post.new(uri, {
  "Authorization" => "Bearer #{ENV.fetch('PIGEONATLAS_API_KEY')}",
  "Content-Type" => "application/json"
})
request.body = {
  from: "Acme <hello@mail.acme.com>",
  to: ["customer@example.com"],
  subject: "Order #4471 confirmed",
  html: "<p>Thanks for your order.</p>",
  text: "Thanks for your order."
}.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
id = JSON.parse(response.body)["id"]
Response
{ "id": "01a06673-4d4b-7173-a296-487a2e8917b1" }

Send text alongside html. Filters weight HTML-only mail as more likely to be bulk, and some clients render nothing without it.

Not sending twice

Add an Idempotency-Key header. A repeated request with the same key returns the original message id instead of sending again, for 24 hours.

Header
Idempotency-Key: order-4471-confirmation

Use something derived from what you are sending about, not a random value because a retry has to produce the same key to be any use. This matters more than it sounds: retry logic is where duplicate receipts come from.

Scheduling

Pass scheduled_at as an ISO 8601 timestamp and we hold the message until then. Your app does not need a cron job for it.

Body
"scheduled_at": "2026-09-04T07:00:00Z"

Look up a message

curl
curl https://pigeonatlas.com/v1/emails/01a06673-4d4b-7173-a296-487a2e8917b1 \
  -H "Authorization: Bearer pa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response
{
  "id": "01a06673-4d4b-7173-a296-487a2e8917b1",
  "from": "Acme <hello@mail.acme.com>",
  "to": ["customer@example.com"],
  "subject": "Order #4471 confirmed",
  "last_event": "delivered",
  "created_at": "2026-09-03T08:47:07Z"
}

Errors

Every error has the same shape, and the message says what to change.

Shape
{
  "statusCode": 403,
  "name": "not_found",
  "message": "paypal.com is not one of your domains. Add it before sending from it."
}
StatusMeansRetry?
401 Key missing, wrong, or revoked. No. Fix the key.
402 Out of allowance on your plan, and no top-up credits. After a top-up, an upgrade, or when the month resets.
403 Sending from a domain you have not verified, one past what your plan includes, or a newsletter domain. No. Verify it, reorder your domains, change plan, or send from a transactional domain.
422 Something in the request is malformed. No. The message says what.
5xx Our problem. Yes, with backoff.

402 rather than 403 when you run out so your code can tell "pay and this works" apart from "you are not allowed": the difference between a retry and an alarm at 3am.

Limits

Who you may send to

Only people who asked to hear from you. Bought, rented or scraped lists are not permitted, and neither is reusing addresses collected for something else. We suppress an address automatically once it hard bounces or files a complaint, and that suppression is checked on every send, but the consent behind the first message is yours to hold, and we will ask you to show it.

Acceptable Use Policy · How we prevent abuse

Coming from somewhere else?

Moving from Resend · Sending from Lovable or Supabase · SMTP settings