Server API v1
Last updated 2 November 2026
Alias Autofill talks to three kinds of service: addy.io, SimpleLogin, and any mail server that speaks this API. This page is for people who run or build a mail server. If you use addy.io or SimpleLogin you don't need any of this: paste an API key in the extension and you're done.
Overview
A compatible server lets its users make email addresses on demand. The extension needs to:
- get a token for one browser, approved by the user on your site (connect flow);
- list the domains the user can make addresses on (
GET /me); - make an address for a website and remember which site it is for (
POST /addresses,GET /sites); - optionally keep a made-up name and location with an address, and read sign-in codes mailed to it.
All endpoints live under one base path on the server's origin:
https://mail.example.org/api/ext/v1/
The user types only the origin (https://mail.example.org) into the extension. Servers must use HTTPS. http://localhost and http://127.0.0.1 are accepted for development.
Requests and errors
Authentication
Every call carries the browser's token as a bearer token:
Authorization: Bearer jtx_6pQe0yR2…
- Tokens must match
^jtx_[A-Za-z0-9_-]{20,}$. The extension refuses anything else during connect. Use at least 32 random bytes after the prefix. - Store only a hash of the token. Let users see and revoke their connected browsers, and name each one (the connect page tells you the browser, for example "Firefox on Linux").
- A session cookie alone must never be enough for these endpoints. The extension sends requests with
credentials: "omit". - Re-check the account on every call. A token whose user is gone or blocked should get
401.
Request headers
| Header | When | Value |
|---|---|---|
Authorization | always | Bearer <token> |
X-Requested-With | every non-GET request | jtmail. Refuse non-GET requests without it (403); it can't be sent cross-site without a CORS preflight. |
Content-Type | requests with a body | application/json |
Requests come from the extension's background page, which holds a host permission for your origin, so you don't need CORS headers on these endpoints.
Responses
JSON bodies, UTF-8. Send Cache-Control: no-store. Errors use any 4xx/5xx status with this body:
{ "error": "invalid", "message": "That name can't be used." }
error is a short machine word; message is shown to the user as is, so write it for people. Any 401 makes the extension forget the token and ask the user to connect again.
| Status | Meaning |
|---|---|
| 400 | Bad input (invalid, or a more specific word). |
| 401 | Token missing, unknown, revoked, or its user can't use the service any more. |
| 403 | Missing X-Requested-With, or the feature isn't open to this user. |
| 404 | The address isn't the user's (never say whose it is). |
| 409 | Limit reached, for example too many connected browsers or addresses. |
| 429 | Too many requests; add retryAfter (seconds) to the body. |
Connecting a browser
No key is ever copied by hand. When the user presses Connect, the extension asks for permission to reach your origin, then opens this page on your server in a new tab:
https://mail.example.org/extension/connect#n=<nonce>&b=<browser name>
The nonce (48 hex characters) and browser name are in the URL fragment, so they never reach your server's logs. The extension then injects a small script into that one tab and the page and script talk with window.postMessage on your origin:
| From | Message | Meaning |
|---|---|---|
| page | { source: "jtmail-page", type: "ping", nonce } | Is the extension there? Send it once the page loads. |
| extension | { source: "jtmail-ext", type: "hello", nonce } | Yes. Sent on load and in answer to every ping. |
| page | { source: "jtmail-page", type: "token", nonce, token } | Sent after the user presses your Connect button. |
| extension | { source: "jtmail-ext", type: "connected", nonce } | The token is stored. |
Your connect page should:
- require the user to be signed in to your site (send them through your sign-in and back, keeping the fragment);
- read
nandbfromlocation.hash, post aping, and show a "This browser isn't asking to connect" message if nohellowith the same nonce arrives within about 6 seconds; - show what is being connected ("Connect Firefox on Linux to your account?") and wait for a real click;
- make the token with a same-origin request using the user's session, post it with the nonce, and wait for
connected; - delete the token again if
connecteddoesn't arrive within about 8 seconds.
Only accept messages where event.source === window, event.origin === location.origin and the nonce matches. The extension does the same checks on its side, and only accepts a token from the tab it opened for that nonce.
// on /extension/connect, after the user clicks Connect
const p = new URLSearchParams(location.hash.slice(1));
const nonce = p.get('n');
const r = await fetch('/api/ext/tokens', {
method: 'POST', credentials: 'same-origin',
headers: { 'content-type': 'application/json', 'x-requested-with': 'jtmail' },
body: JSON.stringify({ name: p.get('b') || 'Browser' })
});
const { id, token } = await r.json();
const ok = new Promise((res) => {
addEventListener('message', (e) => {
const d = e.data;
if (e.source === window && e.origin === location.origin && d?.source === 'jtmail-ext'
&& d.type === 'connected' && d.nonce === nonce) res(true);
});
setTimeout(() => res(false), 8000);
});
postMessage({ source: 'jtmail-page', type: 'token', nonce, token }, location.origin);
if (!(await ok)) await fetch('/api/ext/tokens/' + id, { method: 'DELETE', headers: { 'x-requested-with': 'jtmail' } });
How you make tokens (/api/ext/tokens above) is up to you; it is only called by your own page.
Name, icon and colour
The extension shows your service's name and icon on its button, menus and toolbar icon, and picks its accent colour from your icon. It reads them from your home page (GET /) when the user connects and every few days after, with no cookies:
- Name: the web app manifest's
short_nameorname, else<meta name="application-name">,og:site_nameor the page title. Keep it under 40 characters. - Icon: manifest icons and
<link rel="icon">/apple-touch-iconon the same origin, PNG/ICO/WebP preferred (SVG is skipped), at least 64×64;/favicon.icoas a last resort. - Colour: the most common bright colour in the icon, else
<meta name="theme-color">.
Endpoints
Paths below are relative to /api/ext/v1.
Who is connected and where addresses can go. The extension caches this for 10 minutes and checks the token with it right after connecting.
{
"name": "Emily",
"defaultDomain": "example.org",
"targets": [
{ "kind": "shared", "domain": "example.org", "label": "example.org" },
{ "kind": "shared", "domain": "example.net", "label": "example.net" },
{ "kind": "catchall", "id": "c_8f2a", "label": "emily.example.org" },
{ "kind": "domain", "id": "d_31b0", "label": "emilycarter.com" }
]
}
| Field | Description |
|---|---|
name | Shown as "Connected as …". |
defaultDomain | Optional. The shared domain new addresses go on when the user picks none. It must also be in targets; otherwise the first shared target is used. |
targets | Every place the user may make addresses, see Targets. Only list domains that receive mail right now. |
Make a new address for a website. The server picks the random part; the client never sends a local part.
{
"format": "site",
"site": "www.netflix.com",
"target": { "kind": "shared", "domain": "example.org" },
"note": "Made for www.netflix.com on 2 Nov 2026",
"withName": true,
"gender": "any"
}
| Field | Description |
|---|---|
format | Required. One of the address formats. |
site | Required. Host name of the page (1-253 characters). Store the registrable domain (netflix.com) and link the new address to it. |
target | Where the address goes (Targets). Missing or null: the default domain. |
length | string format only: 8-16 characters (default 10). |
words | name format only: 2 or 3. |
gender | any, female or male, for person addresses and made-up names. |
note | Optional, up to 200 characters, or null. Shown in the user's address list. |
withName | Optional. Also give the address a made-up first and last name, kept with it (use it as the From name if you have one). |
Answer 201:
{
"address": "netflix.k3x9q@example.org",
"format": "site",
"site": "netflix.com",
"name": "Emily Carter",
"first": "Emily",
"last": "Carter"
}
name, first and last are present when the address has a name (withName, or the person format), else name is null. Retry internally when a generated address is taken; don't ask the client to.
The Undo button right after an address was made. Body { "address": "netflix.k3x9q@example.org" }. Remove the address and its site link, or turn it off if mail has already arrived at it. Answer 200 with any JSON object; 404 if it isn't the user's.
The user's own receiving addresses, for "Another of my addresses…".
{ "addresses": [ { "address": "emily@example.org", "note": null } ] }
Addresses already linked to a website (same registrable domain), newest first, so the extension can offer them again. Leave out addresses that no longer receive mail.
{
"site": "netflix.com",
"addresses": [
{
"address": "netflix.k3x9q@example.org",
"linkedAt": "2026-11-02T18:04:11Z",
"note": "Made for www.netflix.com on 2 Nov 2026",
"name": "Emily Carter", "first": "Emily", "last": "Carter",
"place": { "city": "Dayton", "region": "Ohio", "regionCode": "OH", "postal": "45402", "country": "United States", "countryCode": "US" }
}
]
}
name, first, last and place are null when nothing is kept.
Link an existing address to a site, when the user picks one they already have. Body { "site": "www.netflix.com", "address": "emily@example.org" }. 404 if the address isn't the user's.
A made-up name for name boxes, not tied to any address. Body { "gender": "any" }, answer { "first": "Emily", "last": "Carter", "name": "Emily Carter" }. Draw from real, common first names and surnames so the result passes form checks.
Keep a name with an address the user owns. Body { "address", "first", "last" }; both empty clears it. Answer { "address", "name", "first", "last" }. Refuse names you wouldn't allow as a From name with 400 invalid.
Keep the location the extension filled in, so the same one is used next time. null clears it. Unknown keys: 400.
{
"address": "netflix.k3x9q@example.org",
"place": { "line1": "418 Maple St", "line2": "", "city": "Dayton", "region": "Ohio",
"regionCode": "OH", "postal": "45402", "country": "United States", "countryCode": "US" }
}
Answer { "address", "place" }.
Sign-in codes and confirmation links in mail sent to the site's addresses (or to address, which must be the user's). The extension polls this while a code box is open, and for a few minutes after it made an address on a page that says "check your email".
{
"codes": [
{
"emailId": "M4f1c",
"address": "netflix.k3x9q@example.org",
"from": "info@account.netflix.com",
"subject": "Your sign-in code",
"receivedAt": "2026-11-02T18:05:40Z",
"code": "482913",
"link": "https://www.netflix.com/verify?t=…"
}
]
}
- Newest first; only mail received after
since(default 15 minutes ago, never more than 60 minutes back). code: the most likely 4-8 character code, ornull. Prefer codes near words like "code" or "verification" and in the subject; skip years, prices, dates and order numbers.link: the firsthttpsverify or sign-in link, ornull. The extension only shows it; it never opens it by itself.- Leave out mail with neither. Read-only: don't mark anything read.
Servers that don't offer codes can answer { "codes": [] }.
The extension's own Disconnect button: revoke the token used for this call. Answer 204.
Address formats
| format | Example | Notes |
|---|---|---|
site | netflix.k3x9q@example.org | Site label (registrable domain without the suffix, [a-z0-9-], up to 20 characters, else site) + . + 5 random characters. On a catch-all or the user's own domain, try the bare label first (netflix@emily.example.org). |
person | emily.carter@example.org | A random realistic name; add digits if taken. The name is kept with the address and returned. |
name | quiet-otter-482@example.org | Random words (2 or 3), plus digits. |
string | x7k2m9qa4t@example.org | Random lowercase letters and digits, length long. |
uuid | 3f2c…-…@example.org | A random UUID. |
Unknown formats: 400 invalid. Never reuse an address that was deleted, by this user or anyone else.
Targets
| kind | Shape in requests | Meaning |
|---|---|---|
shared | { "kind": "shared", "domain": "example.org" } | A domain your service offers to everyone. |
catchall | { "kind": "catchall", "id": "c_8f2a" } | A subdomain that belongs to this user, where any name works. |
domain | { "kind": "domain", "id": "d_31b0" } | The user's own verified domain. |
label in GET /me is what the user sees. Send international domain names in their ASCII (punycode) form; the extension shows both forms.
Limits and safety
- Rate-limit per user. As a guide: address creation like your own web app,
POST /nameabout 300 per 10 minutes,GET /codesabout 240 per 10 minutes. - Cap connected browsers per user (10 is plenty) and answer
409past it. - Check ownership on every call that names an address, and answer
404for anything that isn't the user's. - The site name is user data: keep it with the address for the user, and don't use it for anything else.
- Don't put tokens in URLs, logs or error messages.
Checklist
- HTTPS origin;
/extension/connectpage that follows the connect flow. - Hashed
jtx_tokens, revocable by the user, checked on every call. X-Requested-With: jtmailrequired on non-GET calls.GET /me,POST /addresses,POST /addresses/undo,GET /sites,PUT /sites,GET /addresses,DELETE /token: the minimum.POST /name,PUT /addresses/name,PUT /addresses/place,GET /codes: for names, locations and codes.- A manifest or icon on your home page so the extension looks like your service.
Questions about the API: hello@aliasautofill.com.