Airpipe SupportHelp Center FAQ Open Webmail →
Home Developers & API Airpipe Mail API — send, read and organize mail from your systems

Airpipe Mail API — send, read and organize mail from your systems

Category: Developers & API · also in Other apps

Send, read, search, organize and reply to email from your own systems, with one API key per mailbox.

In short: create a key under Settings → API access in Airpipe Mail. A new key can send mail and follow delivery status. To also read or organize mail, switch on Read mail and/or Organize mail on that key. Both are off by default.

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_limited with Retry-After.

Permissions

PermissionAllowsDefault
sendSend mail, reply (together with read)On
statusDelivery status of sent mailOn
webhooksRegister webhooksOn
read — “Read mail”List, search and read messages, list folders and sender addresses, download attachmentsOff
modify — “Organize mail”Move, archive, labels, mark read or unreadOff

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.email can be the mailbox itself or one of its own aliases. GET /v1/mail/identities lists the allowed addresses. Any other address gives 403 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-Key header. Retries with the same key return the first answer.
  • Open tracking: on by default when there is HTML. Set "track": false to turn it off.
  • Size: attachments and inline images together up to about 15 MB.
  • Threading: in_reply_to and references (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
ParameterMeaning
mailboxFolder (id, name or role). Leave out to search the whole mailbox.
qFree text (subject, body, addresses)
from, to, subjectMatch on that field
after, beforeDate or time, e.g. 2026-10-01
unread=true, has_attachment=trueOnly unread mail, or only mail with attachments
limit (1–200, default 50), positionPaging. 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)

ActionRequest
ArchivePOST /v1/mail/messages/{id}/archive (creates an Archive folder if the mailbox has none)
MovePOST /v1/mail/messages/{id}/move {"mailbox":"role:trash"}. The message leaves its current folders.
LabelsPOST /v1/mail/messages/{id}/labels {"add":["Eneas"],"remove":["To do"]}
Read / unreadPOST /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": true everyone 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.
  • from is optional (default: the mailbox) and follows the same alias rules as sending. html, attachments and Idempotency-Key work 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.

Webhooks belong to the mailbox, not to a single key. You receive events for everything sent via the API from that mailbox. Ignore message_ids you do not recognise.

7. Errors

CodeMeaning
401 missing_bearer_token / invalid_api_key / api_key_revokedKey missing, wrong or revoked
403 ip_not_allowedRequest from an IP that is not on the key's list
403 scope_missingSwitch on Read mail / Organize mail for the key
403 from_not_allowedSender is not the mailbox or one of its aliases
404 message_not_found / attachment_not_foundNo such message or attachment in this mailbox
400 unknown_mailbox / unknown_labelNo folder with that id, name or role
429 rate_limitedWait for Retry-After seconds
501 not_supported_for_imap_keysReading and organizing work for Airpipe mailboxes, not for external IMAP accounts
502 send_failedThe 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"
  done

Was this article helpful?

Thanks for your feedback!