Mayfly API

Version 0.1.0 · Proprietary

The contract between the API and everything that uses it, including Mayfly's own web pages.

Conventions

  • Mail endpoints accept an API key, Authorization: Bearer <api key>, or the web app's session cookie.
  • Account, team and API key endpoints accept only the session cookie, so a leaked API key can't manage users. So do adding, removing and checking a domain, so a leaked key can't point a domain's mail anywhere; listing domains takes either.
  • Requests sent with an API key are limited to 600 a minute per key (MAYFLY_MAX_API_REQUESTS_PER_MINUTE), as a token bucket: a key may spend a full bucket at once, and with the refill up to twice the limit can land in any 60 seconds. Over the limit, 429 rate_limited with Retry-After set to the true seconds until the key's next token, on any endpoint a key may call.
  • Requests that change data with a session cookie must send an Origin header matching the site exactly, or get 403 bad_origin. So must sign-in and accepting an invite, which set the cookie.
  • Request bodies are JSON, sent with Content-Type: application/json, and at most 16 KB.
  • Times are RFC 3339 in UTC.
  • Lists are newest first. Pass nextCursor back as cursor for the next page.
  • Addresses are matched against the envelope recipient, case-insensitively.
  • Errors are {"error": {"code": "...", "message": "..."}}.
  • Mail in another workspace returns 404, the same as mail that doesn't exist. So does an invalid ID.
  • Mail that's still pending or failed parsing has null text and html, {} for headers, and [] for the lists. Its tls and authentication come from its row, so they're there whatever its parseStatus.
  • When rate limited you get 429 with a Retry-After header.

Servers

  • https://mayfly.aravaltech.com/api/v1 — Internal use
  • http://localhost:3000/api/v1 — Local development

Authentication

apiKey
An API key from Settings, API keys.
sessionCookie
Used by the web inbox. The cookie is called __Host-mayfly_session where it's Secure, as on the server, and mayfly_session locally; only the current name is ever read. Requests that change data must also send a matching Origin header, or get 403 bad_origin.

Worked examples — a sign-up test and an OTP test — live in docs/examples/ in the repository.

Messages

Mail history. Read, search and delete stored mail

GET /messages

List and search mail history

History covers stored mail only, so it reaches back as far as the workspace's retention period.

ParameterTypeDescription
to querystring (email)Exact recipient address. Control characters are refused with 400.
from querystringPart of the sender's address or name, case-insensitive, matched against the envelope sender and the From header together. %, _ and \ match themselves. Control characters are refused with 400.
subject querystringPart of the subject, case-insensitive. %, _ and \ match themselves. Control characters are refused with 400.
receivedAfter querystring (date-time)Only mail received at or after this time. RFC 3339 with a time zone, within years 2000 to 9999; anything else answers 400.
receivedBefore querystring (date-time)Only mail received before this time. RFC 3339 with a time zone, within years 2000 to 9999; anything else answers 400.
limit queryinteger
cursor querystringThe nextCursor from the previous page.

Responses

200 A page of mail. Bodies are not included.

  • messagesrequired array of MessageSummary

    MessageSummary

    • idrequired string (uuid)
    • torequired string — The envelope recipient, as the receiver stored it — printable ASCII, at most 256 characters, an @ that is neither first nor last, so not always email-shaped.
    • envelopeFromrequired string — The envelope sender. Can be empty for bounces.
    • headerFrom string | null — The From header. Written by the sender, so don't trust it.
    • subject string | null
    • receivedAtrequired string (date-time)
    • expiresAtrequired string (date-time)
    • sizeBytesrequired integer — The size of the raw mail as stored, including the Received header Mayfly adds on top. size_bytes on the row.
    • parseStatusrequired string, one of: pending, parsed, failed
    • otp Otp
      • valuerequired string
      • confidencerequired string, one of: high, low — low means more than one candidate was found, or no keyword was nearby.
  • nextCursorrequired string | null — Null when there are no more pages.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /messages/{id}

Read one mail

ParameterTypeDescription
id pathrequiredstring (uuid)

Responses

200 The mail. A parsed.json larger than MAYFLY_MAX_PARSED_JSON_BYTES (5 MB by default) is not read: the mail comes back with null text and html, empty headers, links and attachments, and a parseWarnings entry saying so and pointing at the raw download — because a 25 MB mail can parse to 150 MB of JSON, and reading that per request would exhaust the app's memory. The raw download is unaffected, and streams. At most a few parsed.json reads run at once (MAYFLY_MAX_PARSED_READS, 4 by default), because each holds a buffer up to the byte cap while it parses; a request beyond that answers 429 with Retry-After: 1, without reading anything.

Message

  • idrequired string (uuid)
  • torequired string — The envelope recipient, as the receiver stored it — printable ASCII, at most 256 characters, an @ that is neither first nor last, so not always email-shaped.
  • envelopeFromrequired string — The envelope sender. Can be empty for bounces.
  • headerFrom string | null — The From header. Written by the sender, so don't trust it.
  • subject string | null
  • receivedAtrequired string (date-time)
  • expiresAtrequired string (date-time)
  • sizeBytesrequired integer — The size of the raw mail as stored, including the Received header Mayfly adds on top. size_bytes on the row.
  • parseStatusrequired string, one of: pending, parsed, failed
  • otp Otp
    • valuerequired string
    • confidencerequired string, one of: high, low — low means more than one candidate was found, or no keyword was nearby.
  • tlsrequired Tls
    • versionrequired string
    • cipherrequired string
  • authenticationrequired Authentication
    • statusrequired string, one of: pending, done — auth_status, with checking reported as pending. While it's pending, spf, dkim and dmarc are null.
    • spfrequired object — For the MAIL FROM domain, or the HELO name when MAIL FROM was empty.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — The domain checked. It comes from the mail, so treat it as untrusted text.
    • dkimrequired object — The best of the mail's signatures, with its d=. none with a null domain when it has no signature.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — The domain checked. It comes from the mail, so treat it as untrusted text.
    • dmarcrequired object — For the domain in the From header.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — Null when the mail has no single From address.
      • policyrequired string | null, one of: none, quarantine, reject, — The policy the domain publishes, or null if it has none. Only shown, never applied.
  • textrequired string | null — Null when the mail isn't parsed, or when its parsed.json was too large to read.
  • htmlrequired string | null — Sanitized, but still unsafe to display outside the sandboxed iframe described in docs/web-app-spec.md. Null when the mail isn't parsed, or when its parsed.json was too large to read.
  • headersrequired object; each key: array of string
  • linksrequired array of string — Found in the mail. Mayfly never fetches them.
  • attachmentsrequired array of Attachment

    Attachment

    • indexrequired integer
    • filenamerequired string — From the mail, so treat it as untrusted text.
    • contentTyperequired string
    • sizerequired integer
  • parseWarningsrequired array of string — What the parser had to leave out, and why a body is missing when it is: a mail whose parsed.json is over MAYFLY_MAX_PARSED_JSON_BYTES says so here, with the size, and points at the raw download.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /messages/{id}

Delete one mail

Deletes the mail's row only. Every read goes through the row, so the mail can't be read at once; the web app has read-only access to mail files on purpose, and the mail service's maintenance job removes them within about an hour. An invalid ID, another workspace's mail, expired mail and a second delete of the same mail all answer the same 404. One mail delivered to several recipients is one copy per recipient, so deleting one leaves the others.

ParameterTypeDescription
id pathrequiredstring (uuid)

Responses

204 Deleted.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /messages/{id}/raw

Download the mail exactly as received

Streams, however large the mail. A caller that hangs up mid-download — or before the download begins — has the file closed at once. No Content-Length is promised or set: Next 16 strips it from route responses, so setting it would be a promise the framework won't keep. Every response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, as every API response does.

ParameterTypeDescription
id pathrequiredstring (uuid)

Responses

200 The raw mail, sent as a download.

Content-Disposition: Always an attachment, named by the mail's generated ID.

Body: message/rfc822

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

HEAD /messages/{id}/raw

The download's headers, without the download

GET's status and headers with no body: the file is opened to check it — with every check GET makes on it — and closed, and no stream is started, so a HEAD request leaves nothing open. Like GET, it sets no Content-Length: Next 16 strips it from route responses.

ParameterTypeDescription
id pathrequiredstring (uuid)

Responses

200 The same headers GET answers with, and no body.

Content-Disposition: Always an attachment, named by the mail's generated ID.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /messages/{id}/attachments/{index}

Download an attachment

Always a download: application/octet-stream, whatever type the mail claimed for the part, with Content-Disposition: attachment and a cleaned filename from parsed.json — ../../evil.csv downloads as evil.csv, runs of Unicode spaces collapse to one so a hidden extension shows, and a name with nothing left falls back to attachment-<index>. A name outside printable ASCII, or one containing % or =? — which a browser would decode inside the quoted filename= — additionally arrives as RFC 5987 filename*=UTF-8''…, percent-encoded as a whole, with filename="attachment-<index>" beside it. The file itself is opened by position out of the mail's own attachments directory, never by the mail's filename.

Mail that isn't parsed — pending or failed, or a parsed.json that's missing or too large to read — answers 404, as it reports attachments: [] itself; reading a parsed.json takes one of the same read slots as the single-mail endpoint, and the 429 is that cap. A caller that hangs up mid-download — or before the download begins — has the file closed at once. No Content-Length is promised or set: Next 16 strips it from route responses. Every response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, as every API response does. See "Attachments" in docs/web-app-spec.md.

ParameterTypeDescription
id pathrequiredstring (uuid)
index pathrequiredintegerThe attachment's index from the mail — its position, which the file is stored under too, spelled the one way such a number is (0, 1, never 01); any other spelling answers the same 404 as an index the mail doesn't have.

Responses

200 The attachment, always as a download.

Content-Disposition: Always attachment, with the cleaned filename the mail gave it, or attachment-<index>.

Body: application/octet-stream

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

HEAD /messages/{id}/attachments/{index}

The download's headers, without the download

GET's status and headers with no body: the file is opened to check it — with every check GET makes on it — and closed, and no stream is started, so a HEAD request leaves nothing open. Like GET, it sets no Content-Length: Next 16 strips it from route responses.

ParameterTypeDescription
id pathrequiredstring (uuid)
index pathrequiredintegerThe attachment's index from the mail — its position, which the file is stored under too, spelled the one way such a number is (0, 1, never 01); any other spelling answers the same 404 as an index the mail doesn't have.

Responses

200 The same headers GET answers with, and no body.

Content-Disposition: Always attachment, with the cleaned filename the mail gave it, or attachment-<index>.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /wait

Wait for a mail to arrive

Returns the newest matching mail received since since, waiting until timeout if there is none yet. Mail is returned once parsing has finished. If parsing failed, you still get the mail, with parseStatus: failed.

Pass `since` whenever you can. Take the time before triggering the mail, then pass it here. Without it, a test that waits twice on one address, such as requesting a second OTP, can get the earlier mail. Use a time from the server, such as the Date header of an earlier API response, because your machine's clock may differ.

Each wait is held open server-side by one poller that serves every wait on the server with a single query per check — however many are waiting, in however many workspaces — checking every 500 ms while any wait is younger than ten seconds and then every 2 s, with a wait that has just started checked at once. The newest 20 seconds of mail — 20 seconds by default, never less than 15 — are re-read from the window's start on every check, so a mail is still returned when its save commits up to 20 seconds after the time it carries; a mail that becomes visible later than that can be missed. One address's mail never holds back another's: each is read separately within the one query, and an address with more mail inside the window than one check may read is caught up over the checks that follow, a bounded part of it each check, so its newest mail is reached within a few checks however much sits behind it. Each wait's own timeout ceiling holds even while a check is stuck on a slow query. How many may run at once is capped three ways: 20 per API key, with a session's waits counting per user; a ceiling over every key and session of one workspace together; and 200 across the whole server, of which a few are reserved for the operator workspace so the monitor's own wait is never refused. A wait beyond any of them is answered 429 with the code waits_full and Retry-After before it starts. Wait on one address at a time, or poll GET /messages instead.

ParameterTypeDescription
to queryrequiredstring (email)Exact recipient address.
from querystringPart of the sender's address or name, case-insensitive, matched against the envelope sender and the From header together. %, _ and \ match themselves. Control characters are refused with 400.
subject querystringPart of the subject, case-insensitive. %, _ and \ match themselves. Control characters are refused with 400.
since querystring (date-time)Only match mail received at or after this time. Defaults to 60 seconds ago.
timeout queryintegerSeconds to wait.

Responses

200 The mail.

Message

  • idrequired string (uuid)
  • torequired string — The envelope recipient, as the receiver stored it — printable ASCII, at most 256 characters, an @ that is neither first nor last, so not always email-shaped.
  • envelopeFromrequired string — The envelope sender. Can be empty for bounces.
  • headerFrom string | null — The From header. Written by the sender, so don't trust it.
  • subject string | null
  • receivedAtrequired string (date-time)
  • expiresAtrequired string (date-time)
  • sizeBytesrequired integer — The size of the raw mail as stored, including the Received header Mayfly adds on top. size_bytes on the row.
  • parseStatusrequired string, one of: pending, parsed, failed
  • otp Otp
    • valuerequired string
    • confidencerequired string, one of: high, low — low means more than one candidate was found, or no keyword was nearby.
  • tlsrequired Tls
    • versionrequired string
    • cipherrequired string
  • authenticationrequired Authentication
    • statusrequired string, one of: pending, done — auth_status, with checking reported as pending. While it's pending, spf, dkim and dmarc are null.
    • spfrequired object — For the MAIL FROM domain, or the HELO name when MAIL FROM was empty.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — The domain checked. It comes from the mail, so treat it as untrusted text.
    • dkimrequired object — The best of the mail's signatures, with its d=. none with a null domain when it has no signature.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — The domain checked. It comes from the mail, so treat it as untrusted text.
    • dmarcrequired object — For the domain in the From header.
      • resultrequired string, one of: pass, fail, softfail, neutral, none, temperror, permerror
      • domainrequired string | null — Null when the mail has no single From address.
      • policyrequired string | null, one of: none, quarantine, reject, — The policy the domain publishes, or null if it has none. Only shown, never applied.
  • textrequired string | null — Null when the mail isn't parsed, or when its parsed.json was too large to read.
  • htmlrequired string | null — Sanitized, but still unsafe to display outside the sandboxed iframe described in docs/web-app-spec.md. Null when the mail isn't parsed, or when its parsed.json was too large to read.
  • headersrequired object; each key: array of string
  • linksrequired array of string — Found in the mail. Mayfly never fetches them.
  • attachmentsrequired array of Attachment

    Attachment

    • indexrequired integer
    • filenamerequired string — From the mail, so treat it as untrusted text.
    • contentTyperequired string
    • sizerequired integer
  • parseWarningsrequired array of string — What the parser had to leave out, and why a body is missing when it is: a mail whose parsed.json is over MAYFLY_MAX_PARSED_JSON_BYTES says so here, with the size, and points at the raw download.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 No matching mail arrived in time. Check for the code wait_timeout.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Two codes, one status. waits_full: one of the wait caps was full, answered before a single poll. rate_limited: the per-key request limit, which applies here as on any endpoint a key may call. The one limit that never answers 429 here is the parsed.json read cap — a busy read slot is waited for within the wait's own timeout, and a slot that never comes answers 404 wait_timeout instead.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

HEAD /wait

Not the way to wait

Always 405, answered before any authentication and before any wait is taken. Next.js would otherwise answer a HEAD by running GET and throwing the body away — a wait held open, a slot taken, a poller's query run, for a caller that wanted no body. Allow names the one method the wait serves.

Responses

405 HEAD is not supported on the wait. The Allow header names the one method it serves.

Allow: The one method the wait serves.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Mailboxes

A view of the addresses that have received mail. Nothing needs creating first

GET /mailboxes

List addresses with stored mail

Newest activity first; ties on the latest time are broken by the address, ascending. This is a view of stored mail, so an address appears when it receives mail and disappears when its mail has all expired or been cleared. Counts cover stored mail only.

ParameterTypeDescription
domain querystringExact domain, matched on the address's own domain part — a subdomain or a parent domain doesn't match. Case doesn't matter.
search querystringPart of the address, case-insensitive. %, _ and \ match only themselves.
limit queryinteger
cursor querystringThe nextCursor from the previous page.

Responses

200 A page of addresses.

  • mailboxesrequired array of Mailbox

    Mailbox

    • addressrequired string
    • mailCountrequired integer
    • lastReceivedAtrequired string (date-time)
  • nextCursorrequired string | null

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /mailboxes/{address}

Delete all mail for an address

Lets a test run clean up after itself, in this workspace only. The count is of stored mail; expired rows go with their partition. The files are removed by the maintenance job. The address keeps working, and new mail still arrives. The address is lowercased first, as the receiver stores them lowercased.

ParameterTypeDescription
address pathrequiredstringExact address, URL-encoded, as the receiver stores it — printable ASCII, at most 256 characters, an @ that is neither first nor last. An address the receiver would refuse answers 400.

Responses

200 Cleared. An address with no mail returns 0.

  • deletedrequired integer

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Overview

Today's figures for the workspace, for the overview page

GET /overview

Today's figures for this workspace

What the overview page shows, counted from this workspace's stored mail: mail received, codes found, mailboxes with mail and mail that failed to parse since midnight, the same figures for the same span of yesterday, and the busiest domains of the last 7 days. Expired mail counts for nothing, like everywhere else.

"Today" starts at midnight in timeZone, the caller's own time zone, and runs until now. The span it's compared with runs from yesterday's midnight in the same zone to this same wall-clock time yesterday, so on a day the clocks change the two spans can differ by an hour. Where the clocks go back across midnight, so that it happens twice, a day starts at the first. sameTimeYesterday is null when some of the mail received in that span has already expired, as it does when the workspace keeps mail for only a day, because a comparison with what's left would undercount.

ParameterTypeDescription
timeZone querystringThe IANA time zone "today" starts at midnight in, such as Europe/London, as a browser reports it (Intl.DateTimeFormat().resolvedOptions().timeZone). Only UTC and Area/Location names are taken, because Postgres reads a bare abbreviation such as CET as a fixed offset; anything else, or a name the server doesn't know, answers 400. Leave it out for UTC.

Responses

200 The figures.

Overview

  • timeZonerequired string — The zone the spans were counted in.
  • todayrequired OverviewSpan — From midnight in timeZone until now.
    • startrequired string (date-time) — Inclusive.
    • endrequired string (date-time) — Exclusive.
    • mailReceivedrequired integer — Mail received in the span. One mail to three recipients is three mails, as everywhere else.
    • codesFoundrequired integer — Of those, the mail an OTP was found in, at either confidence.
    • mailboxesWithMailrequired integer — How many different addresses received that mail.
    • failedToParserequired integer — Of those, the mail whose parsing failed.
  • sameTimeYesterdayrequired OverviewSpan | null — From yesterday's midnight in timeZone until this wall-clock time yesterday. Null when some of the mail received in that span has already expired.
    • startrequired string (date-time) — Inclusive.
    • endrequired string (date-time) — Exclusive.
    • mailReceivedrequired integer — Mail received in the span. One mail to three recipients is three mails, as everywhere else.
    • codesFoundrequired integer — Of those, the mail an OTP was found in, at either confidence.
    • mailboxesWithMailrequired integer — How many different addresses received that mail.
    • failedToParserequired integer — Of those, the mail whose parsing failed.
  • busiestDomainsrequired object — The last 7 days, ending now.
    • startrequired string (date-time)
    • endrequired string (date-time)
    • domainsrequired array of object — At most three, most mail first; ties are broken by the name, ascending byte by byte. A domain is the part of the recipient address after its last @, and a domain with no mail in the window isn't listed.
      • namerequired string
      • mailCountrequired integer

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Domains

Domains this workspace receives mail on, and the test subdomains its admins add, which take mail once their TXT and MX records are found. Adding, removing and checking are for admins only

GET /domains

List this workspace's domains

Every domain the workspace has, in any state, ordered by name: the ones the operator set up and the ones its admins added, with the records each added one needs and what its last check found. The list is bounded by domainLimit, so it isn't paged. Another workspace's domains never appear, not even one claiming the same name.

Responses

200 The domains.

  • domainsrequired array of Domain

    Domain

    • idrequired string (uuid)
    • namerequired string — Lowercase, with no trailing dot.
    • enabledrequired boolean — Whether Mayfly takes mail for it now. True for verified and failing, false otherwise.
    • statusrequired string, one of: pending, verified, failing, lapsed, disabled — - pending: added, and not verified yet, so it takes no mail. - verified: takes mail; its last check passed, or the operator set it up. - failing: still takes mail, but its rechecks haven't passed. Since failingSince a record has been missing or wrong, and if no check passes within 72 hours of that, it stops. Since erroringSince the DNS has given no answer, and if no check passes within 7 days of that, it stops. - lapsed: was verified, and stopped taking mail: its rechecks didn't pass for that long, or the domain moved to the workspace whose value is now in its TXT record. A check that passes turns it back on. - disabled: set up by the operator, and turned off.
    • verificationrequired string, one of: dns, operator — dns: added by this workspace, and checked by its records. operator: set up by Mayfly's operator, and never checked.
    • recordsrequired array of DnsRecord — The records to set at the domain's DNS provider, each with what the last check found. Empty for an operator domain.

      DnsRecord

      • typerequired string, one of: TXT, MX
      • namerequired string — The record's full name. The TXT record is _mayfly. followed by the domain; the MX record is the domain itself.
      • valuerequired string — What the record must hold. The TXT record's is mayfly-verify= and this domain's own random value; the MX record's is Mayfly's MX host.
      • priorityrequired integer | null — The MX record's priority, 10. Null for the TXT record.
      • statusrequired string, one of: pending, verified, failed, error — What the last check found: pending before the first check, verified when the record is right, failed when the DNS answered and the record is missing or wrong, error when the DNS gave no answer, which is tried again and changes nothing about the domain.
      • reasonrequired string | null — Why, for anything but verified and pending, written for people. Null otherwise.
    • verifiedAtrequired string | null (date-time) — When it last started taking mail. Null for a domain that never has.
    • failingSincerequired string | null (date-time) — When its rechecks started finding a record missing or wrong, while it takes mail. Null when they pass.
    • erroringSincerequired string | null (date-time) — When its rechecks started getting no answer from the DNS, while it takes mail. Null when they pass.
    • checkedAtrequired string | null (date-time) — When it was last checked. Null until the first check, and for an operator domain.
    • nextCheckAtrequired string | null (date-time) — When its next check is due. With checkRequested, the asked-for check: when it was asked for, or 30 seconds after checkedAt, whichever is later. Otherwise the scheduled one: every 6 hours while it passes, hourly while it's failing, every 15 minutes in the first day after it's added, and every 6 hours after that, and sooner while its name is contested between workspaces. The check itself starts within a few seconds of this. Null for an operator domain.
    • checkRequestedrequired boolean — A check was asked for, by adding the domain or by checkDomain, and hasn't finished yet.
    • createdAtrequired string (date-time)
  • domainLimitrequired integer — The most domains this workspace may hold, counting every one it has, in any state. 10 unless Mayfly's operator has raised it, which is free on request.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /domains

Add a test subdomain

Adds a subdomain made for testing as this workspace's own. It takes no mail until Mayfly has found both of its records at the domain's DNS provider: a TXT record holding this domain's own value, which proves the workspace's admin controls the domain's DNS, and an MX record pointing at Mayfly's MX host. The first check is asked for at once; set the records, then ask again with checkDomain.

Refused with 400: a name that isn't one (invalid_domain; letters, digits, hyphens and dots only, with an international name in its xn-- form); a registered domain's root or a public suffix, found with the Public Suffix List, because pointing a root's MX record here would send all of its real mail to Mayfly (root_domain); and Mayfly's own domains (reserved_domain). Refused with 409: a name this workspace already has (domain_exists), and a workspace that holds as many domains as its limit (domain_limit_reached), whose message says who to ask for more, at no charge.

Another workspace holding or claiming the same name doesn't stop this, and the answer says nothing about it: the value published in the TXT record is what decides which workspace gets a name. A claim that no check has proven reserves nothing, so whoever claims a name first can't keep it from the workspace that controls its DNS.

Request body

  • namerequired string — The subdomain. Case and a trailing dot don't matter; it's stored lowercase, with no trailing dot.

Responses

201 Added, as a claim no check has proven yet, with the records to set.

Domain

  • idrequired string (uuid)
  • namerequired string — Lowercase, with no trailing dot.
  • enabledrequired boolean — Whether Mayfly takes mail for it now. True for verified and failing, false otherwise.
  • statusrequired string, one of: pending, verified, failing, lapsed, disabled — - pending: added, and not verified yet, so it takes no mail. - verified: takes mail; its last check passed, or the operator set it up. - failing: still takes mail, but its rechecks haven't passed. Since failingSince a record has been missing or wrong, and if no check passes within 72 hours of that, it stops. Since erroringSince the DNS has given no answer, and if no check passes within 7 days of that, it stops. - lapsed: was verified, and stopped taking mail: its rechecks didn't pass for that long, or the domain moved to the workspace whose value is now in its TXT record. A check that passes turns it back on. - disabled: set up by the operator, and turned off.
  • verificationrequired string, one of: dns, operator — dns: added by this workspace, and checked by its records. operator: set up by Mayfly's operator, and never checked.
  • recordsrequired array of DnsRecord — The records to set at the domain's DNS provider, each with what the last check found. Empty for an operator domain.

    DnsRecord

    • typerequired string, one of: TXT, MX
    • namerequired string — The record's full name. The TXT record is _mayfly. followed by the domain; the MX record is the domain itself.
    • valuerequired string — What the record must hold. The TXT record's is mayfly-verify= and this domain's own random value; the MX record's is Mayfly's MX host.
    • priorityrequired integer | null — The MX record's priority, 10. Null for the TXT record.
    • statusrequired string, one of: pending, verified, failed, error — What the last check found: pending before the first check, verified when the record is right, failed when the DNS answered and the record is missing or wrong, error when the DNS gave no answer, which is tried again and changes nothing about the domain.
    • reasonrequired string | null — Why, for anything but verified and pending, written for people. Null otherwise.
  • verifiedAtrequired string | null (date-time) — When it last started taking mail. Null for a domain that never has.
  • failingSincerequired string | null (date-time) — When its rechecks started finding a record missing or wrong, while it takes mail. Null when they pass.
  • erroringSincerequired string | null (date-time) — When its rechecks started getting no answer from the DNS, while it takes mail. Null when they pass.
  • checkedAtrequired string | null (date-time) — When it was last checked. Null until the first check, and for an operator domain.
  • nextCheckAtrequired string | null (date-time) — When its next check is due. With checkRequested, the asked-for check: when it was asked for, or 30 seconds after checkedAt, whichever is later. Otherwise the scheduled one: every 6 hours while it passes, hourly while it's failing, every 15 minutes in the first day after it's added, and every 6 hours after that, and sooner while its name is contested between workspaces. The check itself starts within a few seconds of this. Null for an operator domain.
  • checkRequestedrequired boolean — A check was asked for, by adding the domain or by checkDomain, and hasn't finished yet.
  • createdAtrequired string (date-time)

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 This workspace already has the domain (domain_exists), or holds as many as its limit (domain_limit_reached).

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /domains/{domainId}

Remove a domain

Mail for it stops at once: once this answers, nothing more for the domain is stored, even mail already arriving. Mail already stored stays until it expires. A domain the operator set up can be removed too, and adding it back then needs its records like any other. Another workspace's domain answers 404, the same as one that doesn't exist.

ParameterTypeDescription
domainId pathrequiredstring (uuid)

Responses

204 Removed.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /domains/{domainId}/check

Check a domain's records now

Asks for the domain's TXT and MX records to be checked soon, rather than on its schedule. The check runs in Mayfly's mail service; this answers at once, with checkRequested true and nextCheckAt saying when the check will be taken, and the result appears on each record, and checkRequested turns false, when it's done. An asked-for check runs at most once every 30 seconds for a domain: one asked for sooner after the last check (checkedAt) waits until then, and nextCheckAt says so. A request is never dropped, and asking again while one is waiting asks for nothing more. A domain just added has its first check asked for already, and taken at once. A domain the operator set up is never checked.

ParameterTypeDescription
domainId pathrequiredstring (uuid)

Responses

202 Asked for.

Domain

  • idrequired string (uuid)
  • namerequired string — Lowercase, with no trailing dot.
  • enabledrequired boolean — Whether Mayfly takes mail for it now. True for verified and failing, false otherwise.
  • statusrequired string, one of: pending, verified, failing, lapsed, disabled — - pending: added, and not verified yet, so it takes no mail. - verified: takes mail; its last check passed, or the operator set it up. - failing: still takes mail, but its rechecks haven't passed. Since failingSince a record has been missing or wrong, and if no check passes within 72 hours of that, it stops. Since erroringSince the DNS has given no answer, and if no check passes within 7 days of that, it stops. - lapsed: was verified, and stopped taking mail: its rechecks didn't pass for that long, or the domain moved to the workspace whose value is now in its TXT record. A check that passes turns it back on. - disabled: set up by the operator, and turned off.
  • verificationrequired string, one of: dns, operator — dns: added by this workspace, and checked by its records. operator: set up by Mayfly's operator, and never checked.
  • recordsrequired array of DnsRecord — The records to set at the domain's DNS provider, each with what the last check found. Empty for an operator domain.

    DnsRecord

    • typerequired string, one of: TXT, MX
    • namerequired string — The record's full name. The TXT record is _mayfly. followed by the domain; the MX record is the domain itself.
    • valuerequired string — What the record must hold. The TXT record's is mayfly-verify= and this domain's own random value; the MX record's is Mayfly's MX host.
    • priorityrequired integer | null — The MX record's priority, 10. Null for the TXT record.
    • statusrequired string, one of: pending, verified, failed, error — What the last check found: pending before the first check, verified when the record is right, failed when the DNS answered and the record is missing or wrong, error when the DNS gave no answer, which is tried again and changes nothing about the domain.
    • reasonrequired string | null — Why, for anything but verified and pending, written for people. Null otherwise.
  • verifiedAtrequired string | null (date-time) — When it last started taking mail. Null for a domain that never has.
  • failingSincerequired string | null (date-time) — When its rechecks started finding a record missing or wrong, while it takes mail. Null when they pass.
  • erroringSincerequired string | null (date-time) — When its rechecks started getting no answer from the DNS, while it takes mail. Null when they pass.
  • checkedAtrequired string | null (date-time) — When it was last checked. Null until the first check, and for an operator domain.
  • nextCheckAtrequired string | null (date-time) — When its next check is due. With checkRequested, the asked-for check: when it was asked for, or 30 seconds after checkedAt, whichever is later. Otherwise the scheduled one: every 6 hours while it passes, hourly while it's failing, every 15 minutes in the first day after it's added, and every 6 hours after that, and sooner while its name is contested between workspaces. The check itself starts within a few seconds of this. Null for an operator domain.
  • checkRequestedrequired boolean — A check was asked for, by adding the domain or by checkDomain, and hasn't finished yet.
  • createdAtrequired string (date-time)

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 Mayfly's operator set this domain up, so it isn't checked. The code is operator_domain.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Auth

Sign in and out, invites and password resets

POST /auth/sign-in

Sign in

Sets the session cookie. A wrong email, a wrong password and a locked account all return the same 401, with the code sign_in_failed. Five wrong passwords in 15 minutes lock an email for 15 minutes. At most a few password checks run at once (2 by default), and a request beyond that is answered 429 with the same body whether or not the email has an account, without counting toward the lockout.

Request body

  • emailrequired string (email)
  • passwordrequired string

Responses

204 Signed in.

Set-Cookie: The session cookie, called __Host-mayfly_session where it's Secure, and mayfly_session locally.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 Wrong email, wrong password, or that email is locked out. The code is sign_in_failed, and the body is the same for each, so none can be told apart.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /auth/sign-out

Sign out

Deletes the session, and clears the cookie under both of its names, so a stale one from before Secure was switched on or off can't stick around. An expired or unknown session still answers 401, and clears the cookies with it, so a dead one doesn't linger in the browser.

Responses

204 Signed out.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /auth/invites/{token}

Check an invite link

ParameterTypeDescription
token pathrequiredstringThe token from an invite or reset link.

Responses

200 The invite is valid.

  • emailrequired string (email)
  • workspaceNamerequired string

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /auth/invites/{token}

Accept an invite

Creates a new user, joins the workspace and signs in, setting the session cookie. It never changes an existing account. The new password is hashed under the same cap as sign-in's password checks, so a request can answer 429 while the cap is full.

ParameterTypeDescription
token pathrequiredstringThe token from an invite or reset link.

Request body

  • namerequired string
  • passwordrequired string

Responses

204 Account created and signed in.

Set-Cookie: The session cookie, called __Host-mayfly_session where it's Secure, and mayfly_session locally.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 This email already has an account. The code is account_exists.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /auth/password-resets/{token}

Set a new password with a reset link

Signs the user out everywhere, and clears the sign-in count on the user's email, so an account that was locked when the link was made is unlocked. An expired, used or unknown token returns the same 404. The new password is hashed under the same cap as sign-in's password checks, so a request can answer 429 while the cap is full.

ParameterTypeDescription
token pathrequiredstringThe token from an invite or reset link.

Request body

  • passwordrequired string

Responses

204 Password changed.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Account

The signed-in user's own account

GET /me

The signed-in user

Responses

200 The user and their workspace.

Me

  • idrequired string (uuid)
  • emailrequired string (email)
  • namerequired string
  • rolerequired string, one of: admin, member
  • workspacerequired object
    • idrequired string (uuid)
    • namerequired string
    • retentionDaysrequired integer

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

PATCH /me

Change my name

Request body

  • namerequired string

Responses

200 Updated.

Me

  • idrequired string (uuid)
  • emailrequired string (email)
  • namerequired string
  • rolerequired string, one of: admin, member
  • workspacerequired object
    • idrequired string (uuid)
    • namerequired string
    • retentionDaysrequired integer

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /me/password

Change my password

Signs out my other browsers, and keeps this one signed in. A wrong current password returns 403 and counts toward the lockout. The new password is hashed under the same cap as sign-in's password checks, so a request can answer 429 while the cap is full.

Request body

  • currentPasswordrequired string
  • newPasswordrequired string

Responses

204 Password changed.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 The current password is wrong, or the account is locked. A change whose membership condition matched no row — the caller left the workspace while the request ran — is refused the same way, having changed nothing. Or Origin doesn't match, with the code bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

429 Too many requests. The code is rate_limited; the wait caps answer waits_full instead, so a client can tell the two apart.

Retry-After: Seconds to wait.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Team

People in the workspace. Admins only

GET /team/members

List people in the workspace

Responses

200 The members.

  • membersrequired array of Member

    Member

    • idrequired string (uuid)
    • emailrequired string (email)
    • namerequired string
    • rolerequired string, one of: admin, member
    • joinedAtrequired string (date-time)

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /team/members/{userId}

Remove someone from the workspace

Deletes their user, which ends their sessions. They can be invited again later. Their open invites and password-reset links in this workspace are cancelled with them.

ParameterTypeDescription
userId pathrequiredstring (uuid)

Responses

204 Removed.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 This would leave the workspace with no admin. The code is last_admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

PATCH /team/members/{userId}

Change someone's role

ParameterTypeDescription
userId pathrequiredstring (uuid)

Request body

  • rolerequired string, one of: admin, member

Responses

200 Updated.

Member

  • idrequired string (uuid)
  • emailrequired string (email)
  • namerequired string
  • rolerequired string, one of: admin, member
  • joinedAtrequired string (date-time)

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 This would leave the workspace with no admin. The code is last_admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

GET /team/invites

List pending invites

Responses

200 Invites not yet accepted or expired. Links aren't included, because only their hashes are stored.

  • invitesrequired array of Invite

    Invite

    • idrequired string (uuid)
    • emailrequired string (email)
    • rolerequired string, one of: admin, member
    • expiresAtrequired string (date-time)

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /team/invites

Invite someone

The link is shown only in this response, and expires after 7 days. Creating one cancels the email's earlier open invite in this workspace.

Request body

  • emailrequired string (email)
  • rolerequired string, one of: admin, member

Responses

201 The link.

OneTimeLink

  • urlrequired string (uri) — Shown once. Built from MAYFLY_BASE_URL.
  • expiresAtrequired string (date-time)

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

409 This email already has an account. The code is account_exists.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /team/invites/{inviteId}

Cancel an invite

ParameterTypeDescription
inviteId pathrequiredstring (uuid)

Responses

204 Cancelled.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

API keys

Keys for test scripts. Admins only

GET /api-keys

List API keys

Every key the workspace owns, including revoked ones, which stay listed with their revokedAt. No full key is ever in the list: it exists only in the response that made it.

Responses

200 The keys, without their secret values.

  • apiKeysrequired array of ApiKey

    ApiKey

    • idrequired string (uuid)
    • namerequired string
    • prefixrequired string — The first 8 characters, to recognize a key.
    • createdAtrequired string (date-time)
    • lastUsedAtrequired string | null (date-time)
    • revokedAtrequired string | null (date-time)

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

POST /api-keys

Create an API key

Makes a key named name and returns it. The key is mf_ plus 32 random letters and digits from a secure generator; the response is the only place the full key ever appears, and only its SHA-256 hash and first 8 characters are stored. name is checked like a person's name: some text, at most 200 characters, and no control or format characters.

Request body

  • namerequired string

Responses

201 The new key.

  • idrequired string (uuid)
  • namerequired string
  • prefixrequired string — The first 8 characters, to recognize a key.
  • createdAtrequired string (date-time)
  • lastUsedAtrequired string | null (date-time)
  • revokedAtrequired string | null (date-time)
  • keyrequired string — The full key, shown once and never again.

400 A parameter is missing or invalid.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

DELETE /api-keys/{keyId}

Revoke an API key

The key's next request gets 401, because the auth lookup checks revoked_at IS NULL on every request. Revoking a key that's already revoked still answers 204, and keeps the timestamp of the first revocation. Another workspace's key id answers 404, the same as one that doesn't exist.

ParameterTypeDescription
keyId pathrequiredstring (uuid)

Responses

204 Revoked.

401 The API key is missing, wrong or revoked, or there's no live session. The code is unauthorized.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

403 A request that changes data with the session cookie, or sets it, didn't send an Origin header matching the site. The code is bad_origin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

404 It doesn't exist, it's in another workspace, or you need to be an admin.

Error

  • errorrequired object
    • coderequired string — Stable and machine-readable, such as not_found, wait_timeout, rate_limited or waits_full.
    • messagerequired string — For people. May change.

Health

The health endpoint the monitor and the deploy healthcheck call

GET /healthz

Whether the web app can serve

Served under its own base URL, not the servers above: https://mayfly.aravaltech.com/apihttp://localhost:3000/api

Whether Postgres answers, because every page and route needs it. Public, and it says nothing about any workspace. The body is plain text, as the mail service's /healthz is.

Responses

200 Postgres answers.

Body: text/plain

503 Postgres doesn't answer.

Body: text/plain