Pigeon Atlas

API

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.

Before you can send

Two things, once per domain:

  1. Add and verify the domain. We give you three DNS records and check for them ourselves.
  2. Create an API key. Shown once. We store only a hash, so it cannot be shown again.

You can only send from a domain you have verified. This is why the service cannot be used to send as someone else, and it is not optional.

Use a subdomain like mail.yourcompany.com, and a separate one such as news.yourcompany.com for newsletters — complaints about a newsletter should never be able to hurt delivery of your password resets.

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
curl -X POST https://pigeonatlas.com/v1/emails \
  -H "Authorization: Bearer pa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Prodavnica <hello@mail.prodavnica.rs>",
    "to": ["kupac@example.com"],
    "subject": "Narudžbina #4471 je potvrđena",
    "html": "<p>Hvala na narudžbini.</p>",
    "text": "Hvala na narudžbini."
  }'
Node
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: "Prodavnica <hello@mail.prodavnica.rs>",
    to: ["kupac@example.com"],
    subject: "Narudžbina #4471 je potvrđena",
    html: "<p>Hvala na narudžbini.</p>",
    text: "Hvala na narudžbini.",
  }),
});

const { id } = await response.json();
Ruby
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: "Prodavnica <hello@mail.prodavnica.rs>",
  to: ["kupac@example.com"],
  subject: "Narudžbina #4471 je potvrđena",
  html: "<p>Hvala na narudžbini.</p>",
  text: "Hvala na narudžbini."
}.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
id = JSON.parse(response.body)["id"]
Python
import os, requests

response = requests.post(
    "https://pigeonatlas.com/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['PIGEONATLAS_API_KEY']}"},
    json={
        "from": "Prodavnica <hello@mail.prodavnica.rs>",
        "to": ["kupac@example.com"],
        "subject": "Narudžbina #4471 je potvrđena",
        "html": "<p>Hvala na narudžbini.</p>",
        "text": "Hvala na narudžbini.",
    },
)

message_id = response.json()["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 — 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": "Prodavnica <hello@mail.prodavnica.rs>",
  "to": ["kupac@example.com"],
  "subject": "Narudžbina #4471 je potvrđena",
  "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. After upgrading, or when the month resets.
403 Sending from a domain you have not verified. No — verify it first.
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