inbox_api Sign in for a key

Call this from the server side of your website to read mail that arrives on your OnionMail addresses. Each message tells you which address received it, who sent it, the subject, and the body.

Quick start

  1. Sign in and open the API tab, linked at the top of this page. Copy the key. It reads every address on that account.
  2. Store the key in your website’s server environment, for example ONIONMAIL_API_KEY. Leave it out of pages, apps, and public repositories.
  3. Poll GET https://www.onionmail.su/v1/emails with the key in the Authorization header.
  4. Remember the highest id you have handled and send it as since_id on the next poll.
curl -s \
  -H "Authorization: Bearer YOUR_KEY" \
  "https://www.onionmail.su/v1/emails?limit=20"

Authentication

Every /v1/ request needs the key. Send it as a header.

Authorization: Bearer YOUR_KEY

X-API-Key: YOUR_KEY is the same thing. ?api_key=YOUR_KEY also works, and the key is then stored in access logs. Use the header.

The base URL is https://www.onionmail.su/v1. Responses are JSON, Cache-Control: no-store. A browser on another origin can call the API (Access-Control-Allow-Origin: *). Put the key on your server anyway, because anyone who sees it can read the inbox.

A valid key may make 120 requests a minute. Thirty failed checks from the same IP in a minute, including requests with no key, get 429 until the minute passes.

Polling

Save the largest id you have already processed. The next request is:

GET https://www.onionmail.su/v1/emails?since_id=1042&limit=100

since_id returns messages whose id is greater than that number. The list is newest first. After you handle the page, store the greatest id in it and use that value next time.

Use limit=100. If has_more is true, more than limit messages matched and this page is only the newest of them. Poll about once a second so a single response stays under 100 new messages and has_more stays false. One poll a second is 60 requests a minute, under the limit of 120.

Read text when you want the words (a code, a link). Read body when you want the message as it arrived. to is the address on your account that received it.

List messages

GET https://www.onionmail.su/v1/emails
QueryMeaning
limitHow many messages to return. Default 50. Minimum 1, maximum 100. Newest first.
since_idOnly messages with an id greater than this integer.
sinceOnly messages received after this Unix time. Compared with received_at.
toOnly mail sent to this address. It must be one of yours, or the API returns address_not_owned.
unread=1Only messages still unread in the inbox. true and yes work too.
mark_read=1Mark the messages in this response as read. Leave it off to keep the inbox badges unchanged.
{
  "ok": true,
  "addresses": ["you@your-domain"],
  "count": 1,
  "has_more": false,
  "emails": [
    {
      "id": 1042,
      "to": "you@your-domain",
      "from": "noreply@example.com",
      "subject": "Your verification code",
      "body": "Your code is 482913",
      "text": "Your code is 482913",
      "is_html": false,
      "body_truncated": false,
      "received_at": 1710000000,
      "received_at_iso": "2024-03-09T16:00:00Z",
      "is_read": false,
      "attachments": [
        {"id": 9, "filename": "code.txt", "content_type": "text/plain", "size": 18}
      ]
    }
  ]
}

addresses is the set that was searched. With to set, that array holds just that address. count is how many messages are in this response. has_more means another matched message did not fit in limit.

One message

GET https://www.onionmail.su/v1/emails/<id>

Same key. Optional mark_read=1. The message is under email and uses the same fields as a list item. A missing id, or a message on someone else’s address, is 404 with not_found.

{
  "ok": true,
  "email": {
    "id": 1042,
    "to": "you@your-domain",
    "from": "noreply@example.com",
    "subject": "Your verification code",
    "body": "Your code is 482913",
    "text": "Your code is 482913",
    "is_html": false,
    "body_truncated": false,
    "received_at": 1710000000,
    "received_at_iso": "2024-03-09T16:00:00Z",
    "is_read": false,
    "attachments": []
  }
}

Attachment

GET https://www.onionmail.su/v1/emails/<id>/attachments/<attachment_id>

Same key. The response is the file itself, with that file’s Content-Type and a Content-Disposition filename. It is not JSON. Add ?inline=1 to display an image. A wrong id is 404 not_found.

curl -sL \
  -H "Authorization: Bearer YOUR_KEY" \
  -o code.txt \
  "https://www.onionmail.su/v1/emails/1042/attachments/9"

Addresses

GET https://www.onionmail.su/v1/addresses

The addresses this key can read: the account’s throwaway address, when it has one, and any custom addresses.

{
  "ok": true,
  "addresses": ["you@your-domain", "alias@your-domain"]
}

Message fields

FieldMeaning
idStable integer. Increases as mail arrives. Use it as since_id.
toThe address on your account that received the message.
fromSender. Empty string when the message had none.
subjectSubject. Empty string when there was none.
bodyThe message as it was stored, HTML included when the sender sent HTML.
textPlain text. For HTML mail, tags are removed and <script> and <style> are dropped. For a plain message, this matches body.
is_htmltrue when body is HTML.
body_truncatedtrue when body or text was cut at 200,000 characters.
received_atUnix time, seconds.
received_at_isoThe same instant in UTC, 2024-03-09T16:00:00Z.
is_readWhether the inbox has marked it read. mark_read=1 makes this true for messages in that response.
attachmentsid, filename, content_type, and size in bytes. The bytes themselves are a separate request.

Errors

Failures are JSON: {"ok": false, "error": "invalid_api_key"}. A missing message does not reveal whether the id exists on another account.

StatuserrorWhen
401missing_api_keyNo key was sent.
401invalid_api_keyThe key does not match an account. Replacing the key on the API tab makes the old one fail this way.
400address_not_ownedto is not an address on this account.
400invalid_limitlimit is not an integer from 1 to 100.
400invalid_sincesince is not an integer.
400invalid_since_idsince_id is not an integer.
404not_foundThat message or attachment is not on this account.
429rate_limitedOver 120 requests in a minute for this key, or 30 failed checks from this IP in a minute.

Examples

Both snippets poll once, handle new mail oldest-first, and remember the latest id. Run them on a timer of about one second. Point ONIONMAIL_API_KEY at the key from the API tab.

PHP

<?php
$apiKey = getenv('ONIONMAIL_API_KEY');
$base = 'https://www.onionmail.su';
$state = __DIR__ . '/onionmail-since.txt';
$sinceId = is_file($state) ? (int) file_get_contents($state) : 0;

$ch = curl_init($base . '/v1/emails?since_id=' . $sinceId . '&limit=100');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$data = json_decode($raw, true);
if ($code !== 200 || empty($data['ok'])) {
    error_log('OnionMail ' . $code . ' ' . ($data['error'] ?? ''));
    exit(1);
}

$maxId = $sinceId;
foreach (array_reverse($data['emails']) as $email) {
    $maxId = max($maxId, (int) $email['id']);
    // $email['to'], $email['from'], $email['subject'], $email['text']
    // Download: GET /v1/emails/{id}/attachments/{attachment id}
}
file_put_contents($state, (string) $maxId);

Node.js

import { readFile, writeFile } from "node:fs/promises";

const apiKey = process.env.ONIONMAIL_API_KEY;
const base = "https://www.onionmail.su";
const state = "onionmail-since.txt";
let sinceId = 0;
try { sinceId = Number(await readFile(state, "utf8")) || 0; } catch {}

const res = await fetch(`${base}/v1/emails?since_id=${sinceId}&limit=100`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await res.json();
if (!res.ok || !data.ok) {
  throw new Error(`OnionMail ${res.status} ${data.error || ""}`);
}

let maxId = sinceId;
for (const email of [...data.emails].reverse()) {
  maxId = Math.max(maxId, email.id);
  // email.to, email.from, email.subject, email.text
}
await writeFile(state, String(maxId));

On the API tab, New key replaces the key immediately. Update ONIONMAIL_API_KEY when you do that. Requests still using the previous key receive invalid_api_key.