Airpipe Mail API — send, read and organize mail from your systems
Send, read, search, organize and reply to email from your own systems, with one API key per mailbox.
1. Getting started
Base URL: http://127.0.0.1:8790/v1/mail. Today the API is reached from Airpipe's own servers only. It is not open on the internet, which is also why keys are locked to localhost by default.
Every request carries the key as a bearer token:
Authorization: Bearer apk_…
- One mailbox per key. A key only ever sees and sends from the mailbox it was created for.
- The key is shown once. Store it in a secret store. If it leaks, use Rotate or Revoke.
- IP lock. A key only works from the IPs you list (localhost by default). Other IPs get
403 ip_not_allowed. - Rate limit. 120 requests per minute per key by default. Above that you get
429 rate_limitedwithRetry-After.
Permissions
| Permission | Allows | Default |
|---|---|---|
send | Send mail, reply (together with read) | On |
status | Delivery status of sent mail | On |
webhooks | Register webhooks | On |
read — “Read mail” | List, search and read messages, list folders and sender addresses, download attachments | Off |
modify — “Organize mail” | Move, archive, labels, mark read or unread | Off |
A key without the needed permission gets 403 {"error":"scope_missing","detail":"the key needs scope: read"}.
2. Send
POST /v1/mail/send
{
"from": { "email": "invoice@example.se", "name": "Example AB" },
"to": { "email": "customer@example.com", "name": "Anna" },
"cc": [ { "email": "copy@example.com" } ],
"subject": "Invoice 1042",
"text": "Hi Anna, please find the invoice attached.",
"html": "<p>Hi Anna, please find the invoice attached.</p>",
"attachments": [ { "filename": "invoice-1042.pdf", "content_type": "application/pdf", "content_base64": "JVBERi0…" } ],
"track": true
}
Response: {"message_id":"msg_…","tracking_id":"…","submission_id":"…"}. Keep message_id, because status and webhooks refer to it.
- Choosing the sender (alias):
from.emailcan be the mailbox itself or one of its own aliases.GET /v1/mail/identitieslists the allowed addresses. Any other address gives403 from_not_allowed, and the response lists what is allowed. A missing alias is always an error, never a silent switch to another address. - Avoid sending twice: send an
Idempotency-Keyheader. Retries with the same key return the first answer. - Open tracking: on by default when there is HTML. Set
"track": falseto turn it off. - Size: attachments and inline images together up to about 15 MB.
- Threading:
in_reply_toandreferences(Message-IDs) put the mail in an existing conversation. The reply endpoint does this for you.
3. Read and search (Read mail)
Folders and labels
GET /v1/mail/mailboxes
→ { "mailboxes": [ { "id":"a", "name":"Inbox", "role":"inbox", "total":120, "unread":3 }, … ] }
Wherever a folder is expected you can give its id, its name ("Invoices") or its role: role:inbox, role:sent, role:archive, role:trash, role:junk or role:drafts.
List and search messages
GET /v1/mail/messages?mailbox=role:inbox&from=noreply@dom.se&after=2026-10-01&limit=50
| Parameter | Meaning |
|---|---|
mailbox | Folder (id, name or role). Leave out to search the whole mailbox. |
q | Free text (subject, body, addresses) |
from, to, subject | Match on that field |
after, before | Date or time, e.g. 2026-10-01 |
unread=true, has_attachment=true | Only unread mail, or only mail with attachments |
limit (1–200, default 50), position | Paging. total in the answer is the number of hits. |
Each message in the list has id, thread_id, from, to, cc, subject, received_at, preview, has_attachment, unread, flagged and mailboxes (which folders and labels it is in). Newest first.
Read one message
GET /v1/mail/messages/{id}
→ { …the list fields…, "text": "…", "html": "…", "message_id": "…", "in_reply_to": [], "references": [],
"attachments": [ { "blob_id":"…", "name":"order.pdf", "type":"application/pdf", "size":40213,
"download":"/v1/mail/messages/{id}/attachments/{blob_id}" } ] }
Download an attachment
GET /v1/mail/messages/{id}/attachments/{blob_id} → the file, with its content type and file name
The attachment must belong to that message. Ids from other messages are refused.
4. Organize (Organize mail)
| Action | Request |
|---|---|
| Archive | POST /v1/mail/messages/{id}/archive (creates an Archive folder if the mailbox has none) |
| Move | POST /v1/mail/messages/{id}/move {"mailbox":"role:trash"}. The message leaves its current folders. |
| Labels | POST /v1/mail/messages/{id}/labels {"add":["Eneas"],"remove":["To do"]} |
| Read / unread | POST /v1/mail/messages/{id}/read {"read":false} |
Labels vs. folders: a label is added on top. The message stays where it is, for example in the Inbox, and also appears under the label. A label that does not exist yet is created the first time you add it.
5. Reply (Read mail + send)
POST /v1/mail/messages/{id}/reply
{ "text": "Thanks, received.", "all": false, "from": { "email": "invoice@example.se" } }
- Goes to the sender, or to the Reply-To address if there is one. With
"all": trueeveryone in To and Cc is included as well, except your own addresses. - Replying to something you sent yourself goes to its original recipients.
- Subject becomes
Re: …, and the reply lands in the same conversation. fromis optional (default: the mailbox) and follows the same alias rules as sending.html,attachmentsandIdempotency-Keywork as for sending.
6. Delivery status and webhooks
GET /v1/mail/status?ids=msg_…,msg_…
Gives each message's delivery state (for example delivered, bounced, never_sent) and opens.
POST /v1/mail/webhooks { "url": "https://your-system/hooks/mail" }
→ { "id": 12, "signing_secret": "whsec_…" } (the secret is shown once)
Events: remote_accepted, delivered, bounced, failed, never_sent, spam, opened and (on request) deferred. Each call carries X-Airpipe-Signature: sha256=<hex>, which is HMAC-SHA256 of the raw body with your secret. Verify it before trusting the event.
message_ids you do not recognise.7. Errors
| Code | Meaning |
|---|---|
401 missing_bearer_token / invalid_api_key / api_key_revoked | Key missing, wrong or revoked |
403 ip_not_allowed | Request from an IP that is not on the key's list |
403 scope_missing | Switch on Read mail / Organize mail for the key |
403 from_not_allowed | Sender is not the mailbox or one of its aliases |
404 message_not_found / attachment_not_found | No such message or attachment in this mailbox |
400 unknown_mailbox / unknown_label | No folder with that id, name or role |
429 rate_limited | Wait for Retry-After seconds |
501 not_supported_for_imap_keys | Reading and organizing work for Airpipe mailboxes, not for external IMAP accounts |
502 send_failed | The message did not go out. It is recorded as never_sent with a message_id you can look up. |
8. Example: archive all mail from one sender and save the attachments
KEY=apk_… # Read mail + Organize mail switched on
B=http://127.0.0.1:8790/v1/mail
curl -s -H "Authorization: Bearer $KEY" "$B/messages?mailbox=role:inbox&from=noreply@dom.se&limit=200" \
| jq -r '.messages[].id' | while read id; do
curl -s -H "Authorization: Bearer $KEY" "$B/messages/$id" \
| jq -r '.attachments[] | "\(.download)\t\(.name)"' | while IFS=$'\t' read url name; do
curl -s -H "Authorization: Bearer $KEY" "http://127.0.0.1:8790$url" -o "$name"
done
curl -s -X POST -H "Authorization: Bearer $KEY" "$B/messages/$id/archive"
doneWas this article helpful?