Overview
A self-hosted, receive-only mail service. Mail for any address you create is accepted on port 25 and kept until you delete it. You reach it three ways: this API, the webmail at mail.s7ortlynk.com (also at s7ortlynk.com), and the Telegram bot.
Requests and responses are JSON unless noted. Every timestamp is ISO 8601 with an explicit +00:00 offset. An interactive console (Swagger UI) lives at /swagger and the machine-readable schema at /openapi.json.
These are permanent mailboxes. Nothing expires on a timer. An address keeps its newest 1 000 messages; past that, the oldest are dropped as new mail arrives.
Quick start
Three requests take you from nothing to a verification code. Paste your key below and every example on this page fills it in.
BASE="https://mail.s7ortlynk.com/api/v1" KEY="YOUR_KEY" # 1. a random, realistic address (send a local_part to choose one) curl -s -X POST "$BASE/mailboxes" \ -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{}' # 2. use the address to sign up somewhere # 3. block until the mail lands; codes are already extracted curl -s "$BASE/messages/wait?mailbox=priya.nwosu31&timeout=60" \ -H "X-API-Key: $KEY" | jq '{subject, code: .codes[0], links}'
import httpx BASE = "https://mail.s7ortlynk.com/api/v1" H = {"X-API-Key": "YOUR_KEY"} with httpx.Client(base_url=BASE, headers=H, timeout=90) as api: box = api.post("/mailboxes", json={}).json() print(box["address"]) # priya.nwosu31@s7ortlynk.com # … trigger the sign-up email to box["address"] … msg = api.get("/messages/wait", params={"mailbox": box["id"], "timeout": 60}).json() if msg: print(msg["codes"][:1], msg["links"][:1])
const BASE = "https://mail.s7ortlynk.com/api/v1"; const H = { "X-API-Key": "YOUR_KEY", "Content-Type": "application/json" }; const box = await (await fetch(`${BASE}/mailboxes`, { method: "POST", headers: H, body: "{}" })).json(); // … trigger the sign-up email to box.address … const msg = await (await fetch( `${BASE}/messages/wait?mailbox=${box.id}&timeout=60`, { headers: H })).json(); console.log(msg?.codes[0], msg?.links[0]);
Give your HTTP client a timeout longer than the timeout you ask the server for. Otherwise the client gives up before the answer arrives.
Authentication
Every endpoint except /health needs your API key in a header.
X-API-Key: YOUR_KEY
Get a key by messaging @AFK_MAIL_BOT on Telegram and sending /apikey. Sending /newkey issues a new one and the old key stops working at once.
A key sees only its own account. Asking for someone else's mailbox, message or attachment returns 404, exactly as if it did not exist, so ids cannot be probed.
Core ideas
Three ways to name a mailbox
Wherever a path or parameter takes a mailbox, you can pass its numeric id, its full address, or only the part before the @. These three point at the same mailbox:
/api/v1/mailboxes/7 /api/v1/mailboxes/priya.nwosu31@s7ortlynk.com /api/v1/mailboxes/priya.nwosu31
Random addresses look like people
Creating a mailbox without a name gives it a realistic one built from common first and last names in six shapes: steven.gonzalez, priya.nwosu31, h.cook350, jisoo_wright, isabeldiaz13, jordanm6474. Some sign-up forms reject strings that look machine-made; these pass.
Message ids are a cursor
Ids only ever increase. Pass the last id you have seen as since_id to /messages or /messages/wait and you get only what is newer.
Codes and links come pre-extracted
Each full message carries codes and links. Codes are ranked by how close they sit to words like code, OTP or verify; years and order numbers are filtered out. Links that look like confirmation links sort first.
System
Liveness probe for uptime monitors. Also answers at /health.
{ "ok": true, "domain": "s7ortlynk.com", "version": "1.2.0" }The account the key belongs to. The quickest way to check a key works.
{ "id": 1, "telegram_id": 935200729, "username": "admin",
"is_admin": true, "mailbox_count": 3 }Mailboxes
Only addresses that exist here receive mail. Anything else is turned away during the SMTP conversation with 550 No such mailbox here, before the sender transmits the message.
Every address you own, oldest first. Returns an array of Mailbox.
Create an address. Send {} for a random realistic name, or choose one.
| Field | Type | Description |
|---|---|---|
| local_part | string | null | 1–64 characters: lowercase letters, digits and . _ -, starting and ending with a letter or digit. Omit for a generated name. |
curl -s -X POST "$BASE/mailboxes" -H "X-API-Key: $KEY" \
-H "Content-Type: application/json" -d '{"local_part": "orders"}'
api.post("/mailboxes", json={"local_part": "orders"}).json()
await fetch(`${BASE}/mailboxes`, { method: "POST", headers: H, body: JSON.stringify({ local_part: "orders" }) });
{ "id": 7, "address": "orders@s7ortlynk.com", "local_part": "orders",
"notify": true, "created_at": "2026-10-03T18:32:23.084585+00:00" }400 when the name breaks the rules above, is reserved, is already taken, or you already have 10 addresses.
One mailbox, by id, address or local part.
Deletes the address, all of its mail and its attachment files. Returns 204.
This cannot be undone, and the address becomes free for anyone to claim.
Stops Telegram notifications for this address. Mail is still received. Returns the updated Mailbox.
Turns notifications back on.
Messages
Summaries across your addresses, newest first.
| Query | Type | Default | Description |
|---|---|---|---|
| mailbox | string | all | Id, address or local part. |
| limit | integer | 50 | 1 to 200. |
| offset | integer | 0 | For paging. |
| unread_only | boolean | false | Skip messages already read. |
| since_id | integer | none | Only ids above this one. |
The full message: headers, both bodies, attachments, codes and links. Opening it marks it read; add ?mark=false to look without changing that.
{
"id": 42, "mailbox_id": 7,
"msg_from": "GitHub <noreply@github.com>",
"msg_to": "priya.nwosu31@s7ortlynk.com",
"subject": "[GitHub] Please verify your device",
"body_text": "Hi,\n\nYour verification code is 731902\n…",
"body_html": "<html>…</html>",
"size_bytes": 5120, "received_at": "2026-10-03T18:41:07.512+00:00",
"is_read": true, "attachment_count": 0, "attachments": [],
"codes": ["731902"],
"links": ["https://github.com/sessions/verified-device?token=…"],
"auth_verdict": "pass",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "verdict": "pass", "tls": "TLSv1.3" },
"preview_url": "https://mail.s7ortlynk.com/p/2a.1.6aca8c94.YSpR4gEdWluxOwPrOobZqg"
}The original message exactly as received, as message/rfc822: every Received hop, DKIM-Signature, Authentication-Results and so on. Save it as a .eml file to open in any mail client.
Deletes the message and its attachment files. Returns 204.
Wait for mail
The request automation is built around. It holds the connection open until a new message arrives, then returns it in full.
Returns the first message newer than since_id as a full MessageDetail, codes included. If nothing arrives before the timeout it returns null with status 200, so a client simply calls again.
| Query | Type | Default | Description |
|---|---|---|---|
| mailbox | string | all | Id, address or local part. Set it when several addresses are waiting at once. |
| since_id | integer | 0 | Return the first message with a higher id. Pass the last id you saw. |
| timeout | number | 30 | Seconds to hold the request, 1 to 120. |
| mark | boolean | true | Mark the returned message read. |
curl -s "$BASE/messages/wait?mailbox=priya.nwosu31&timeout=60" \ -H "X-API-Key: YOUR_KEY"
last = 0 while True: msg = api.get("/messages/wait", params={ "mailbox": "priya.nwosu31", "since_id": last, "timeout": 60}).json() if msg: last = msg["id"] handle(msg)
let last = 0; for (;;) { const r = await fetch(`${BASE}/messages/wait?mailbox=priya.nwosu31&since_id=${last}&timeout=60`, { headers: H }); const msg = await r.json(); if (msg) { last = msg.id; handle(msg); } }
The server checks for new mail every half second, so a message is usually returned within a second of the sending server finishing its DATA command.
HTML & text views
Most mail arrives with both an HTML version and a plain-text version. This endpoint renders either one, safely.
| Query | Type | Default | Description |
|---|---|---|---|
| view | auto | html | text | auto | auto picks HTML when the message has it. text on HTML-only mail returns the HTML flattened to readable text, links kept. |
| images | boolean | false | Also load remote images. Off by default so opening mail does not trigger tracking pixels. |
{
"id": 42,
"html": "<p>Hi,</p><p>Your verification code is <b>731902</b></p>…",
"format": "html",
"available": ["html", "text"],
"blocked_images": 1,
"inlined_images": 0
}format is the view actually rendered; available lists the versions the message really has. Show an HTML / Text switch only when both are present.
What the sanitizer removes: <script>, <style>, <iframe>, forms, every on… event handler, and javascript: and data: links. Images embedded in the message (cid:) are inlined. Every remaining link gets rel="noopener noreferrer nofollow".
The body_html field on a message is the raw HTML as sent. Never insert it into a page directly; use this endpoint.
Attachments
Downloads one file with its original name and type. Ids come from a message's attachments list.
curl -OJ -H "X-API-Key: YOUR_KEY" "$BASE/attachments/9"410 Gone means the record exists but the file is no longer stored.
Web previews
Every message can be opened as a web page that looks like it does in a mail client: subject, sender, date, the verification code highlighted, and the mail's own design. No login is needed to open one.
Every message the API returns carries its link in preview_url, and the Telegram bot's View email button opens the same page. Links look like https://mail.s7ortlynk.com/p/4.1.6aca8c94.YSpR4g… and are made by the server; you cannot build one yourself.
curl -s "$BASE/messages?limit=1" -H "X-API-Key: YOUR_KEY" | jq -r '.[0].preview_url'| Query | Default | Description |
|---|---|---|
| view | auto | html or text. The page has HTML / Text tabs that set this. |
| images | 0 | 1 shows remote images. The page offers a "Show images" link when some were hidden. |
How a link is protected
| Property | Detail |
|---|---|
| Signed | HMAC-SHA256 over the message id, owner and expiry with a 256-bit server key. Changing any character gives 404. |
| One message | A link opens exactly one message and nothing else. |
| Bound to the owner | If the address is deleted and someone else claims it, old links stop working. |
| Expires | After 7 days. Tap the button again for a fresh one. |
| No scripts | The page ships no JavaScript and sends a policy that forbids scripts, frames, forms and every network request except, on request, images. |
| Not indexed | noindex, no-store, no-referrer, and the token is redacted from server logs. |
Anyone you forward a preview link to can read that one message until it expires. Treat it like the message itself.
Sender checks
Anyone can type any name into the From line of an email. When a message arrives, the server checks whether the sending domain actually vouches for it, the same three checks Gmail and Outlook run.
| Check | Question it answers |
|---|---|
| SPF | Is the server that delivered this allowed to send for that domain? (RFC 7208) |
| DKIM | Does the message carry a valid cryptographic signature from the domain? (RFC 6376) |
| DMARC | Does a passing SPF or DKIM result match the domain shown in From, and what does the domain ask receivers to do if not? (RFC 7489) |
The results are on every message as auth, with a one-word summary in auth_verdict:
| Verdict | Meaning | What clients do |
|---|---|---|
| pass | The From domain is verified. | Show a "Verified sender" mark. |
| fail | The domain says this message is not theirs. Treat it as forged. | Show a warning and do not highlight its code. |
| none | The domain publishes nothing to check against. | Show nothing. |
Mail is never rejected or delayed because of these checks; they only label it. The same results are written into the stored source as an Authentication-Results header, and auth.tls records whether the message arrived over an encrypted connection.
If your automation acts on codes, check auth_verdict != "fail" first. codes is still filled for failed mail so that the decision is yours.
Schemas
Mailbox
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| address | string | Full address. |
| local_part | string | The part before @. |
| notify | boolean | Whether new mail triggers a Telegram message. |
| created_at | string | ISO 8601, UTC. |
| unread | integer | Unread messages in this mailbox. |
MessageSummary
| Field | Type | Notes |
|---|---|---|
| id | integer | Increasing. Use as since_id. |
| mailbox_id | integer | |
| msg_from | string | The From header as sent. Not verified. |
| subject | string | Empty string when there was none. |
| size_bytes | integer | Size of the whole raw message. |
| received_at | string | When this server accepted it. |
| is_read | boolean | |
| attachment_count | integer | |
| auth_verdict | string | pass, fail, none or empty. See Sender checks. |
| preview_url | string | No-login web view of this message. See Web previews. |
MessageDetail
Everything in MessageSummary, plus:
| Field | Type | Notes |
|---|---|---|
| msg_to | string | |
| body_text | string | Empty for HTML-only mail. |
| body_html | string | Raw, unsanitized. Render through /html. |
| attachments | Attachment[] | |
| codes | string[] | Verification codes, most likely first. |
| links | string[] | Links, confirmation-looking ones first. |
| auth | object | spf, dkim, dmarc (result words such as pass, fail, none), verdict, and tls (for example TLSv1.3, empty if unencrypted). |
Attachment
| Field | Type | Notes |
|---|---|---|
| id | integer | For /attachments/{id}. |
| filename | string | As the sender named it. |
| content_type | string | As the sender declared it. |
| size_bytes | integer | |
| is_inline | boolean | True for an image shown inside the HTML body. |
RenderedBody
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| html | string | Sanitized and safe to insert into a page. |
| format | string | html, text or empty. |
| available | string[] | Versions the message has. |
| blocked_images | integer | Remote images held back. |
| inlined_images | integer | Embedded images shown. |
Errors
Every error is {"detail": "…"}, written so you can show it to a person as it is.
| Status | Meaning | Usual cause |
|---|---|---|
| 400 | Bad request | Invalid or reserved name, address taken, or the 10-address limit reached. |
| 401 | Unauthorized | Missing or unknown X-API-Key, or a blocked account. |
| 404 | Not found | No such id, or it belongs to another account. The two look identical on purpose. |
| 410 | Gone | An attachment's file is no longer stored. |
| 422 | Validation error | A field is missing or the wrong type; the response names it. |
SMTP replies
What another mail server hears when it delivers to mail.s7ortlynk.com on port 25. Useful when a sender reports a bounce.
| Reply | When |
|---|---|
| 220 | Greeting. EHLO then advertises STARTTLS, SIZE 26214400 and 8BITMIME. |
| 250 | Recipient accepted, or message stored. |
| 452 | More than 20 recipients in one transaction. The sender retries the rest in another. |
| 451 | Temporary failure (for example, the database is busy). The sender retries later and nothing is lost. Internal errors are never reported as a permanent 5xx. |
| 550 | The address was never created, or the domain is not s7ortlynk.com. This server does not relay. |
| 552 | Message larger than 25 MiB. |
Repeating the same RCPT TO in one transaction is accepted once; the message is stored a single time.
Limits
Reserved names
These can never be created, so abuse and bounce reports always reach the operator:
postmaster abuse admin root hostmaster webmaster noreply no-reply mailer-daemon support
Rate limits
The API itself has none. Outbound mail is limited in practice by the receiving provider, which may slow down a burst from a new sending address.
Security
| Area | What happens |
|---|---|
| Transport | HTTPS only, with Let's Encrypt certificates for both hostnames; plain HTTP redirects. The API process listens on localhost and is reachable only through that proxy. A Content-Security-Policy blocks any script that is not part of the site. Inbound SMTP is encrypted with STARTTLS (TLS 1.2+). |
| Accounts | Every lookup is scoped to the key's account. Other accounts' ids return 404. |
| Mail content | HTML is sanitized before display; remote images are blocked unless asked for. |
| Relaying | Port 25 accepts mail only for addresses created here. It is not an open relay. |
| Sending | Turned off. The service cannot be used to send mail at all, so it cannot be abused as a spam source. |
| Attachments | Always downloaded as files in the webmail, never opened as pages, so a hostile HTML or SVG attachment cannot run. |
| Preview links | Signed, expiring, one message each, no scripts. See Web previews. |
| Flooding | At most 20 recipients per SMTP transaction, duplicates stored once. |
| Files | Sender-chosen attachment names are cleaned and prefixed, so they cannot escape the storage folder. |
Domain & DNS
What it takes for Gmail, Outlook, iCloud and everyone else to deliver here, and to stop anyone faking mail from @s7ortlynk.com.
s7ortlynk.com: v=spf1 -all_dmarc.s7ortlynk.com: v=DMARC1; p=reject; rua=mailto:postmaster@s7ortlynk.comBecause the service only receives, DKIM keys, reverse DNS and IP blocklists do not affect it. They only matter if sending is ever turned on.
Webmail
A full inbox at mail.s7ortlynk.com. Sign in with your API key.
| Feature | How |
|---|---|
| HTML · Text · Source | Every open message has a switch. HTML is the designed version, Text the plain version, Source the original message with all headers. The page remembers whether you prefer HTML or Text. |
| Keyboard | h HTML, t Text, s Source. |
| Codes | A detected code is shown above the message with a copy button. |
| Images | Remote images stay blocked until you choose Load images. |
| Links to a message | https://mail.s7ortlynk.com/#m42 opens message 42 after sign-in. |
| Addresses | Create (typed or random), mute, and delete with a confirmation step. |
Telegram bot
Message @AFK_MAIL_BOT and send /start. New mail arrives as a notification with the code on top and buttons underneath.
| Command | What it does |
|---|---|
| /start | Main menu with buttons for every action. |
| /new [name] | Create an address; without a name you get a realistic random one. |
| /list | Your addresses, each with Inbox, notification and Delete buttons. |
| /inbox | Recent mail, eight per page, tap a number to open. |
| /read id | Open a message. |
| /code | The newest verification code across all addresses. |
| /apikey · /newkey | Show or replace your API key. |
| /web | Link to the webmail. |
When mail arrives
The notification shows who it is from, the subject, the code if there is one, and a short preview. It has two buttons:
| Button | What it does |
|---|---|
| Copy code | Copies the verification code. Shown only when one was found and the sender was not flagged as forged. |
| View email | Opens the message as a web page, the way it looks in a mail client, with no login. See Web previews. |
Opening a message in the bot (/read or from /inbox) adds Attachments when there are any, and Delete.
Changelog
| Version | Changes |
|---|---|
| 1.2.0 | Receive-only: sending removed. HTTPS at mail.s7ortlynk.com and s7ortlynk.com. Web previews (/p/{token}) behind the bot's HTML version button. Mail keeps its own styling, safely. view and available on /html; unread on mailboxes. Realistic random addresses. STARTTLS on receive. Webmail HTML / Text / Source switch. Redesigned bot with buttons. /messages/wait returns messages in arrival order and ids are never reused. Fixes from a full security audit. |
| 1.1.0 | /api/v1 prefix. /messages/wait. Codes and links extraction. Mailbox lookup by address or name. Mute and unmute. Security fixes for recipient handling, header injection and recipient flooding. |
| 1.0.0 | First release: mailboxes, messages, attachments, sending. |