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.
What is encrypted
Section titled “What is encrypted”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.
Endpoints
Section titled “Endpoints”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.
| Endpoint | What it does |
|---|---|
GET /support | The 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/tickets | Open 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/:id | One request, its messages and the current staffKey. Marks the support team’s replies read. |
POST /support/tickets/:id/messages | Reply. Takes an id (tkm_…), the staffKey and the sealed forStaff and forAccount boxes. Returns ok and the time, at. |
POST /support/tickets/:id/close | Mark the ticket resolved. |
POST /support/tickets/:id/reopen | Reopen a resolved ticket. Counts against the open limit. Returns the ticket. |
POST /support/key | Register 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.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Open requests | 5 at a time |
| Stored requests, open or closed | 20 |
| Messages in one request | 50, both sides together |
| Ciphertext per account | 512 KB across every request, both copies counted |
| New requests | 5 a day |
| Replies | 30 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.
Errors
Section titled “Errors”| Code | Status | Meaning |
|---|---|---|
support_unavailable | 503 | In-app support is unavailable. Email instead. |
staff_key_changed | 409 | The request was sealed to an old support key. Fetch GET /support and seal again. |
too_many_open | 409 | Five tickets are open. Also returned when reopening would make six. |
too_many_tickets | 409 | Twenty requests are stored. |
ticket_closed | 409 | The ticket is resolved. Reopen it or open a new one. |
ticket_full | 409 | The request has 50 messages. Open a new one. |
support_storage_full | 409 | The account’s requests have reached 512 KB. |
id_taken | 409 | The client-generated id is in use. Make a new one. |
too_large | 413 | A box is over the size limit. |
invalid_box, invalid_key, invalid_id | 400 | A 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.