Alias Autofill

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:

  1. get a token for one browser, approved by the user on your site (connect flow);
  2. list the domains the user can make addresses on (GET /me);
  3. make an address for a website and remember which site it is for (POST /addresses, GET /sites);
  4. 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…

Request headers

HeaderWhenValue
AuthorizationalwaysBearer <token>
X-Requested-Withevery non-GET requestjtmail. Refuse non-GET requests without it (403); it can't be sent cross-site without a CORS preflight.
Content-Typerequests with a bodyapplication/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.

StatusMeaning
400Bad input (invalid, or a more specific word).
401Token missing, unknown, revoked, or its user can't use the service any more.
403Missing X-Requested-With, or the feature isn't open to this user.
404The address isn't the user's (never say whose it is).
409Limit reached, for example too many connected browsers or addresses.
429Too 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:

FromMessageMeaning
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:

  1. require the user to be signed in to your site (send them through your sign-in and back, keeping the fragment);
  2. read n and b from location.hash, post a ping, and show a "This browser isn't asking to connect" message if no hello with the same nonce arrives within about 6 seconds;
  3. show what is being connected ("Connect Firefox on Linux to your account?") and wait for a real click;
  4. make the token with a same-origin request using the user's session, post it with the nonce, and wait for connected;
  5. delete the token again if connected doesn'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:

Endpoints

Paths below are relative to /api/ext/v1.

GET/me

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" }
  ]
}
FieldDescription
nameShown as "Connected as …".
defaultDomainOptional. 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.
targetsEvery place the user may make addresses, see Targets. Only list domains that receive mail right now.
POST/addresses

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"
}
FieldDescription
formatRequired. One of the address formats.
siteRequired. Host name of the page (1-253 characters). Store the registrable domain (netflix.com) and link the new address to it.
targetWhere the address goes (Targets). Missing or null: the default domain.
lengthstring format only: 8-16 characters (default 10).
wordsname format only: 2 or 3.
genderany, female or male, for person addresses and made-up names.
noteOptional, up to 200 characters, or null. Shown in the user's address list.
withNameOptional. 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.

POST/addresses/undo

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.

GET/addresses

The user's own receiving addresses, for "Another of my addresses…".

{ "addresses": [ { "address": "emily@example.org", "note": null } ] }
GET/sites?site=<host>

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.

POST/name

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.

PUT/addresses/name

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.

PUT/addresses/place

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" }.

GET/codes?site=<host>&address=<address>&since=<ISO time>

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=…"
    }
  ]
}

Servers that don't offer codes can answer { "codes": [] }.

DELETE/token

The extension's own Disconnect button: revoke the token used for this call. Answer 204.

Address formats

formatExampleNotes
sitenetflix.k3x9q@example.orgSite 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).
personemily.carter@example.orgA random realistic name; add digits if taken. The name is kept with the address and returned.
namequiet-otter-482@example.orgRandom words (2 or 3), plus digits.
stringx7k2m9qa4t@example.orgRandom lowercase letters and digits, length long.
uuid3f2c…-…@example.orgA random UUID.

Unknown formats: 400 invalid. Never reuse an address that was deleted, by this user or anyone else.

Targets

kindShape in requestsMeaning
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

Checklist

  1. HTTPS origin; /extension/connect page that follows the connect flow.
  2. Hashed jtx_ tokens, revocable by the user, checked on every call.
  3. X-Requested-With: jtmail required on non-GET calls.
  4. GET /me, POST /addresses, POST /addresses/undo, GET /sites, PUT /sites, GET /addresses, DELETE /token: the minimum.
  5. POST /name, PUT /addresses/name, PUT /addresses/place, GET /codes: for names, locations and codes.
  6. A manifest or icon on your home page so the extension looks like your service.

Questions about the API: hello@aliasautofill.com.