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:
- Add and verify the domain. We give you three DNS records and check for them ourselves.
- 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.
Authorization: Bearer dp_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://digitalpigeon.golux.tech/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://digitalpigeon.golux.tech/v1/emails \
-H "Authorization: Bearer dp_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."
}'
const response = await fetch("https://digitalpigeon.golux.tech/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DIGITALPIGEON_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();
require "net/http"
require "json"
uri = URI("https://digitalpigeon.golux.tech/v1/emails")
request = Net::HTTP::Post.new(uri, {
"Authorization" => "Bearer #{ENV.fetch('DIGITALPIGEON_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"]
import os, requests
response = requests.post(
"https://digitalpigeon.golux.tech/v1/emails",
headers={"Authorization": f"Bearer {os.environ['DIGITALPIGEON_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"]
{ "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.
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.
"scheduled_at": "2026-09-04T07:00:00Z"
Look up a message
curl https://digitalpigeon.golux.tech/v1/emails/01a06673-4d4b-7173-a296-487a2e8917b1 \ -H "Authorization: Bearer dp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"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.
{
"statusCode": 403,
"name": "not_found",
"message": "paypal.com is not one of your domains. Add it before sending from it."
}
| Status | Means | Retry? |
|---|---|---|
| 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
- One recipient per request. Send several requests, or use a broadcast for a list.
- Suppressed addresses are accepted and not sent. A hard bounce or spam complaint suppresses an address permanently — sending to it again is what gets senders blocked.
- Messages over 25 MB are refused.
Coming from somewhere else?
Moving from Resend · Sending from Lovable or Supabase · SMTP settings