Skip to content

Support

An account opens a support ticket from the app: Support in the sidebar on a desktop browser, the More menu on a phone browser, or Settings in the iOS and Android apps once they are in the stores. Email to support@mailcycle.email works too.

Every subject and message is sealed on the device that wrote it, twice: once to the support team’s public key and once to the account’s own support key, which is derived from the recovery phrase. The server stores both boxes and can open neither. Each payload also carries a MAC under a key only the account and the support team can derive, so a message the server wrote or moved between requests fails to open.

A request also carries a binding: a MAC under a secret only the account and the support team can compute, over the account id, the request id and the support key the request vouches for. The server holds neither private key, so it cannot produce one, which is what stops it inventing a request in an account’s name to draw out a reply.

A ticket has a category, a priority (low, normal, high or urgent), a subject and a description, all inside the sealed header and first message. The apps show a short reference taken from the ticket id, such as #4K2M9X7Q, which both sides can quote.

What the server holds in the clear: which account wrote, the public key the account sealed to, when each message was sent, whether the account or the support team wrote it, whether it has been read, and whether the request is open.

All of these need a signed-in session. API keys get 403 session_required. A suspended account can still reach them, at the lowest rate limit, and nothing else.

EndpointWhat it does
GET /supportThe account’s requests, newest first, with available and the staffKey to seal to. available is false while support has no key, and in-app support is off.
POST /support/ticketsOpen a request. Takes a client-generated id (tkt_ and 20 lowercase letters or digits), the account’s accountKey, the staffKey it sealed to, an optional binding (below), a sealed header (the subject) and a first message with its own id (tkm_…). Returns the ticket.
GET /support/tickets/:idOne request, its messages and the current staffKey. Marks the support team’s replies read.
POST /support/tickets/:id/messagesReply. Takes an id (tkm_…), the staffKey and the sealed forStaff and forAccount boxes. Returns ok and the time, at.
POST /support/tickets/:id/closeMark the ticket resolved.
POST /support/tickets/:id/reopenReopen a resolved ticket. Counts against the open limit. Returns the ticket.
POST /support/keyRegister the account’s support key, so the support team can write to the account first. Takes accountKey, binding and the current staffKey. Returns ok and whether anything changed. The apps send it at sign-in when the key the server holds, supportKey on GET /accounts/me, is out of date.

header and each message carry two boxes, forStaff and forAccount. The account only ever gets its own forAccount copies back. A box is { "v", "epk", "n", "c", "t" }, base64url without padding, where v is the seal version: new boxes are version 2, version 1 boxes may still be read, and a reader has to accept both. The Mailcycle apps write them; the format inside is not documented yet.

A request returns:

{
"id": "tkt_4k2m9x7q1b8c3v6n0z5a",
"status": "open",
"lastAuthor": "staff",
"createdAt": "2026-09-19T08:00:00.000Z",
"updatedAt": "2026-09-19T09:30:00.000Z",
"closedAt": null,
"unread": true,
"header": { "v": 2, "epk": "…", "n": "…", "c": "…", "t": "…" }
}

A message returns id, author (account or staff), at and its box.

When the support team replies or resolves a ticket, the account’s phones get a push notification with fixed text, if the support notification is on. See Notifications.

LimitValue
Open requests5 at a time
Stored requests, open or closed20
Messages in one request50, both sides together
Ciphertext per account512 KB across every request, both copies counted
New requests5 a day
Replies30 an hour

A request with no activity for 60 days is closed. A closed request is deleted 90 days after closing. Deleting the account deletes every request with it.

CodeStatusMeaning
support_unavailable503In-app support is unavailable. Email instead.
staff_key_changed409The request was sealed to an old support key. Fetch GET /support and seal again.
too_many_open409Five tickets are open. Also returned when reopening would make six.
too_many_tickets409Twenty requests are stored.
ticket_closed409The ticket is resolved. Reopen it or open a new one.
ticket_full409The request has 50 messages. Open a new one.
support_storage_full409The account’s requests have reached 512 KB.
id_taken409The client-generated id is in use. Make a new one.
too_large413A box is over the size limit.
invalid_box, invalid_key, invalid_id400A box, key or id is not well formed.

© Northlab Studios Ltd, trading as Mailcycle. Registered in England and Wales, company no. 17462734. Registered office: 167-169 Great Portland Street, London W1W 5PF.