Polpo Mail API
Read, organise and send email from your own code with a simple JSON API, or connect any IMAP/SMTP library. Everything authenticates with personal access tokens.
Access tokens
Create tokens in Settings › Access tokens. Each token has a name, an optional expiry date and one or more permissions:
| Scope | Allows |
|---|---|
read | IMAP login; GET mailboxes, messages, raw messages and attachments |
send | SMTP submission; POST /api/v1/messages/send |
modify | PATCH and DELETE messages (flags, move, trash) |
Tokens start with pm_, are shown only once, and can be revoked at any time. Your account password is never accepted by the API, SMTP or IMAP; it only works on the website, where two-step verification protects it.
SMTP & IMAP
Use your email address as the username and a token as the password:
- SMTP
mx.polpomail.com, port 587 (STARTTLS), AUTH PLAIN, token withsend- IMAP
mx.polpomail.com, 143 (STARTTLS), token withread
# Python: send with smtplib
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"], msg["To"], msg["Subject"] = "you@yourcompany.com", "client@example.com", "Hello"
msg.set_content("Sent with an access token.")
with smtplib.SMTP("mx.polpomail.com", 587) as s:
s.starttls()
s.login("you@yourcompany.com", "pm_…")
s.send_message(msg)
API basics
- Base URL:
https://polpomail.com/api/v1 - Authentication:
Authorization: Bearer pm_… - Request and response bodies are JSON (UTF-8). Times are RFC 3339 in UTC.
- Message
ids are stable until a message is moved; moving creates a new id in the destination mailbox.
Account
GET /api/v1/me: who the token belongs to, storage usage and the token's scopes. Works with any scope.
curl https://polpomail.com/api/v1/me -H "Authorization: Bearer $TOKEN"
{"email":"you@yourcompany.com","name":"You","domain":"yourcompany.com","role":"admin",
"quota_bytes":1073741824,"used_bytes":5242880,
"token":{"name":"CRM","scopes":["read","send"],"expires_at":null}}
Mailboxes
GET /api/v1/mailboxes (scope read)
{"mailboxes":[{"name":"INBOX","role":"","messages":42,"unread":3},
{"name":"Sent","role":"sent","messages":17,"unread":0}, …]}
List messages
GET /api/v1/messages (scope read). Newest first.
| Parameter | Default | Meaning |
|---|---|---|
mailbox | INBOX | Mailbox name |
q | Search subject, sender and recipients | |
unread | false | true returns only unread messages from the page |
limit / offset | 50 / 0 | Paging (limit up to 100); use next_offset from the response |
curl "https://polpomail.com/api/v1/messages?mailbox=INBOX&limit=2" -H "Authorization: Bearer $TOKEN"
{"mailbox":"INBOX","total":42,"offset":0,"limit":2,"next_offset":2,
"messages":[{"id":1187,"mailbox":"INBOX","uid":42,"date":"2026-11-07T09:42:00Z",
"from":"Ana Ruiz <ana@example.com>","to":"you@yourcompany.com","subject":"Q4 roadmap",
"size":18342,"flags":[],"seen":false,"flagged":false,"answered":false}, …]}
Read a message
GET /api/v1/messages/{id} returns headers, the decoded text and html bodies and a list of attachments. Add ?mark_seen=true to mark it read (needs modify too).
GET /api/v1/messages/{id}/raw returns the original RFC 5322 message (message/rfc822).
GET /api/v1/messages/{id}/attachments/{index} downloads one attachment; the url of each is included in the message response.
Organise messages
PATCH /api/v1/messages/{id} (scope modify). Any of seen, flagged and answered (booleans), and mailbox to move it.
curl -X PATCH https://polpomail.com/api/v1/messages/1187 -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"seen":true,"mailbox":"Archive"}'
DELETE /api/v1/messages/{id} moves the message to Trash; when it is already in Trash, or with ?permanent=true, it is deleted for good.
Send a message
POST /api/v1/messages/send (scope send). The message is DKIM-signed, delivered and saved to Sent (unless "save_sent": false).
curl https://polpomail.com/api/v1/messages/send -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{
"to": ["Client <client@example.com>"],
"cc": [], "bcc": [],
"subject": "Your invoice",
"text": "Hi, please find the invoice attached.",
"html": "<p>Hi, please find the invoice attached.</p>",
"attachments": [{"filename": "invoice.pdf", "content_type": "application/pdf", "content": "JVBERi0xLjQK…"}]
}'
HTTP/1.1 202 Accepted
{"message_id":"<3f9c…@yourcompany.com>","status":"sent","recipients":1}
Optional fields: from (your address or an alias you belong to), reply_to, in_reply_to and references for threading. Attachment content is standard base64. At most 100 recipients, and the whole message up to 25 MB. If some recipients fail, status is partial and errors lists them.
Errors & limits
HTTP/1.1 403 Forbidden
{"error":{"code":"insufficient_scope","message":"This token needs the \"send\" scope."}}
| Status | Codes |
|---|---|
| 400 / 422 | bad_json, invalid_recipient, no_recipients, empty_body, bad_attachment |
| 401 | unauthorized, invalid_token (missing, wrong, expired or revoked) |
| 403 | insufficient_scope, from_not_allowed, account_unavailable |
| 404 | message_not_found, mailbox_not_found, attachment_not_found, not_found |
| 413 | too_large |
| 429 | rate_limited (600 requests per minute per token), send_limit (500 external recipients per user per hour), throttled; see Retry-After |
Need higher limits, webhooks or a dedicated deployment? Talk to sales.