Search the docs

Find a page, a section, or an endpoint.

Sending and receiving

Sending from an address you own, with attachments and replies.

from has to be an address you own, and in_reply_to keeps the conversation together. Sends go through the same abuse checks and the same daily cap as the web composer — there is one set of rules, and the API is not a way around it.

Attachments

Upload the file, then name its id in the send. Two calls, because a ten-megabyte PDF is thirteen megabytes of base64 and putting it in the JSON body would make every proxy between you and us carry it that way.

curl https://pidgeon.ai/api/v1/attachments \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-F identity_id=0c939bd7-2d69-4150-a2ac-de994f0e4887 \
-F file=@invoice.pdf

An upload expires in a day. It is a file no message refers to yet, so the window is long enough for a retry after a failed send and short enough that a crashed script does not leave its files with us.

An id belongs to the address that uploaded it. Naming somebody else's is a 404, which is also what a 404 means everywhere else here: not yours, or not there, and we do not say which.

A send takes all the ids or none. If one has expired the whole call is refused and nothing is consumed, so the ones that are still good can be used in the retry. Half a message is worse than a refusal.

Two sizes, both from your plan: one file, and all the files on one message. Going over either is refused before the message is built rather than truncated after, because a silently shortened attachment is worse than a send that did not happen.

An HTML body

Pass html beside text, not instead of it.

curl https://pidgeon.ai/api/v1/messages \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "receipts@acme.com", "to": "customer@example.com",
      "subject": "Your receipt", "text": "Thanks.",
      "html": "<p>Thanks.</p>" }'

text stays required. A message with no plain-text part renders as nothing in a client that refuses HTML, and sending one is a deliverability decision nobody should make by leaving a field out.

Send a reply

curl https://pidgeon.ai/api/v1/messages -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{
  "from": "hello@yourdomain.com",
  "to": "billing@supplier.com",
  "subject": "Re: Invoice 4417",
  "text": "Received, thank you.",
  "in_reply_to": "af08b0a1-284f-464a-a36e-8e15bb7c2eb8"
}'

in_reply_to sets the In-Reply-To and References headers, which is what makes the reply land in the same conversation in the recipient's client as well as in yours.

Rejected, not raised

Recipients that fail a check come back in rejected with a reason, and the rest are sent. A suppressed address, a domain that does not resolve, or a recipient over a limit does not lose the other four.

cURL
{
"data": {
  "id": "2a41309b-eefd-447c-a844-7c10a2469d6a",
  "thread_id": "463ec73a-699f-4ea8-a8fa-bb736ae1601e",
  "rejected": [{ "address": "gone@example.com", "reason": "suppressed" }]
}
}

An address that hard-bounced or reported spam is suppressed automatically and stays suppressed until it is cleared. Sending to it again is refused rather than attempted, because the reputation cost falls on the domain rather than on the message.

A recipient your own rules forbid is not this. That refuses the whole send — 403 with blocked when a rule on the address says no, and not_permitted when its send policy does — and nothing goes to anybody. The two are separated on purpose: rejected is the provider declining an address at hand-off, which can leave a send partly done and is worth reading for. Your own rules are checked before anything leaves, and dropping one recipient silently is how send the invoice to accounts and to Marcus reaches one of them.

What limits a send

Three things, and the effective limit is the lowest of them: your plan's daily allowance, the account's trust tier, and the per-minute API rate limit. A new domain starts with a small allowance and earns its way up as its mail is delivered and read — nobody can make a new domain trustworthy overnight, and a large limit on day one is a reputation burnt on day two.