Mailocity webmail API

version 1.19.0 OpenAPI spec (YAML)

Webmail at /mail signs in here, then speaks JMAP (RFC 8620 and RFC 8621) at /jmap/api, /jmap/upload/{accountId} and /jmap/download/{accountId}/{blobId}/{name} with the session cookie instead of HTTP Basic. Mail apps keep using Basic; a request with an Authorization header is judged by that header alone.

The cookie is __Host-zappocity_mail (zappocity_mail without Secure in dev): HttpOnly, Secure, SameSite=Strict, Path=/. It lasts 7 days and ends after 24 hours without use. Only its SHA-256 is stored.

CSRF. Every JMAP request made with the cookie, and DELETE /api/mail/session, must carry the session's csrfToken in X-CSRF-Token; without it the answer is 403. GET /api/mail/session needs only the cookie and returns the token. No CORS headers are ever sent, so other sites cannot read it.

Who may sign in. Any active account on an active tenant whose plan includes the webmail entitlement. The account and plan are checked again on every request, so suspending an account or tenant, or removing webmail from the plan, ends its sessions at once. A new password signs the account out everywhere. Failed sign-ins share the throttle with IMAP, SMTP and Basic JMAP: 10 failures per address or source in 15 minutes answer 429.

Sending uses JMAP Identity/get, Email/set (create) and EmailSubmission/set under the urn:ietf:params:jmap:submission capability; see docs/kb/webmail.md. Errors use RFC 9457 problem details.

Passwords. A signed-in user changes their password, or sets a recovery address outside the platform, with their current password. A new recovery address counts only once the link mailed to it is opened. A signed-out user can ask for a reset link, which goes only to the confirmed recovery address. Links put a one-time token in the URL fragment (#reset=... or #verify=...); a reset link lasts an hour, a confirmation link 24 hours, and an account gets at most 3 of each per hour. See docs/kb/password-reset.md.

Two-step sign-in. A user can turn on codes from an authenticator app (TOTP, RFC 6238: SHA-1, 6 digits, 30 seconds). Signing in then takes two calls: the password alone answers 401 with twoFactorRequired: true, and the same request again with code signs in. A code is a 6-digit TOTP code, used once, or one of ten single-use recovery codes. A wrong code counts as a failed sign-in for the throttle. With two-step sign-in on, IMAP, SMTP, ManageSieve and JMAP Basic refuse the account password and take app passwords instead. Turning it on, off, or making new recovery codes or app passwords needs the current password; turning it on also needs a code from the new secret. The secret is sealed with the server key. A notice goes to the recovery address for each change. An administrator can turn it off with DELETE /v1/tenants/{tenantId}/accounts/{accountId}/two-factor. See docs/kb/two-step-sign-in.md.

General

GET /api/mail/aliases

The signed-in user's aliases

The user's aliases, how many they may have (max), whether they may add their own (canAdd, the self_aliases entitlement) and the domains they may put them on: their tenant's domains and the shared domains open for signup. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The aliases.
    {
      "aliases": [
        "sales@acme.example"
      ],
      "canAdd": true,
      "domains": [
        "acme.example",
        "mailo.city"
      ],
      "max": 10
    }
POST /api/mail/aliases

Add an alias for the signed-in user

For plans with self_aliases. The name before @ may use letters, digits, '.', '_' and '-' (no '+'). Within the plan's aliases per user (or the user's own limit) and the tenant's total; on a shared domain, reserved names are refused. The alias can be sent from at once.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 201 Added.
  • 403 Not part of the plan.
  • 409 Taken
  • 422 Not a valid name
DELETE /api/mail/aliases/{address}

Remove one of the signed-in user's aliases

ParameterInType
address *pathstring
X-CSRF-Token *headerstring
Responses
  • 204 Removed.
  • 403 Not part of the plan.
  • 404 The user has no such alias.
POST /api/mail/app-passwords

Make an app password

A password for one mail app: 16 letters, shown only now. It signs in to IMAP, SMTP, ManageSieve and JMAP Basic, never to webmail. At most 25 per account. Changing the account password deletes them all.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "currentPassword": "correct horse battery",
  "name": "Phone mail app"
}
Responses
  • 201 Made.
  • 403 Wrong current password
  • 409 The account already has 25.
  • 422 Missing or too long name.
  • 429 Too many wrong passwords.
DELETE /api/mail/app-passwords/{id}

Remove an app password

Mail apps signed in with it are signed out within a minute.

ParameterInType
X-CSRF-Token *headerstring
id *pathstring
Responses
  • 204 Removed.
  • 404 No such app password on this account.
GET /api/mail/client-settings

What to enter in a mail app, with the signed-in user's username

The IMAP, SMTP (submission), ManageSieve and JMAP settings for mail apps, and the CalDAV and CardDAV settings for calendar and contacts apps (calDav, cardDav). appPassword is true when two-step sign-in is on, so mail apps need an app password rather than the account password. Ports and security follow the server's configuration (None only on a server without TLS, such as a development one).

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Mail app settings.
    {
      "appPassword": false,
      "autoconfigUrl": "https://mail.mailo.city/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=bob@acme.test",
      "calDav": {
        "calendarsUrl": "https://mail.mailo.city/dav/6f1c2b9e-0d4a-4c71-9a55-2d7e8f3b1a20/calendars/",
        "server": "mail.mailo.city",
        "serverUrl": "https://mail.mailo.city/.well-known/caldav",
        "username": "bob@acme.test"
      },
      "cardDav": {
        "contactsUrl": "https://mail.mailo.city/dav/6f1c2b9e-0d4a-4c71-9a55-2d7e8f3b1a20/contacts/",
        "server": "mail.mailo.city",
        "serverUrl": "https://mail.mailo.city/.well-known/carddav",
        "username": "bob@acme.test"
      },
      "imap": {
        "authentication": "Normal password",
        "host": "mail.mailo.city",
        "port": 993,
        "security": "SSL/TLS",
        "username": "bob@acme.test"
      },
      "jmap": {
        "sessionUrl": "https://mail.mailo.city/jmap/session",
        "username": "bob@acme.test",
        "webSocketUrl": "wss://mail.mailo.city/jmap/ws",
        "wellKnownUrl": "https://mail.mailo.city/.well-known/jmap"
      },
      "manageSieve": {
        "authentication": "Normal password",
        "host": "mail.mailo.city",
        "port": 4190,
        "security": "STARTTLS",
        "username": "bob@acme.test"
      },
      "smtp": {
        "authentication": "Normal password",
        "host": "mail.mailo.city",
        "port": 587,
        "security": "STARTTLS",
        "username": "bob@acme.test"
      },
      "username": "bob@acme.test"
    }
GET /api/mail/image

Load one remote image through the server

Follows a link from POST /api/mail/images; no cookie is needed because the link is signed. The server fetches the image itself, so the sender sees the server's address, not the reader's. Only public internet addresses are contacted (redirects included, at most 3), only PNG, JPEG, GIF, WebP, AVIF, BMP and ICO are passed on (never SVG), up to 10 MB within 15 seconds.

ParameterInType
u *querystringThe image URL.
a *querystringThe account the link was made for.
e *queryintegerExpiry
s *querystringSignature.
Responses
  • 200 The image.
  • 403 The link is altered or expired
  • 502 The image could not be fetched or is not an allowed image.
POST /api/mail/impersonate

Open a support session from a one-time link

The token from a link made with POST /v1/tenants/{tenantId}/accounts/{accountId}/impersonate. Answers like a sign-in, with impersonatedBy and impersonationReason, and sets the cookie for a one-hour session that cannot change sign-in settings (403 there).

Responses
  • 201 The session.
  • 400 The link was used or is more than five minutes old.
  • 403 The account cannot sign in.
GET /api/mail/imports

The user's imports from other mail servers

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Newest first; fields as MailImport in api/openapi.yaml.
    {
      "items": [
        {
          "foldersDone": 1,
          "foldersTotal": 6,
          "host": "imap.example.com",
          "id": "9c1e...",
          "messagesDone": 120,
          "messagesTotal": 900,
          "status": "running",
          "username": "amy@example.com"
        }
      ]
    }
POST /api/mail/imports

Import mail from another server over IMAP

Copies every remote folder in the background: INBOX, Sent, Drafts, Junk, Trash and Archive (by their special-use flags or usual names) into the matching folders here, other folders into folders of the same name. Read, starred, answered and draft marks and the received date come along; deleted messages and virtual folders (All Mail, Starred) are left out. An import resumes where it stopped and skips messages already here (same Message-ID and size), so running it again copies only what is new. It stops when the mailbox is full. One import per account at a time; the user is emailed when it ends.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "host": "imap.gmail.com",
  "password": "app password",
  "skipFolders": [
    "[Gmail]/Spam"
  ],
  "username": "amy@gmail.com"
}
Responses
  • 201 Queued.
  • 409 An import is already queued or running.
  • 422 A value is not valid.
DELETE /api/mail/imports/{id}

Cancel a queued or running import

ParameterInType
id *pathstring
X-CSRF-Token *headerstring
Responses
  • 204 Cancelled; what was copied stays.
  • 404 No such import.
  • 409 It has already ended.
GET /api/mail/invites

The signed-in user's invitations

How many invitation links the user can still make (after adding what the drip has brought), when more arrive, and the links they made with their status. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The allowance and the links.
    {
      "allowance": {
        "enabled": true,
        "left": 1,
        "max": 5,
        "nextTopUp": "2026-11-06T20:17:28Z",
        "perPeriod": 2
      },
      "invitations": [
        {
          "code": "JsR4bKVKV51X3IFseJvu2g",
          "createdAt": "2026-10-07T21:17:24Z",
          "expiresAt": "2026-10-21T21:17:24Z",
          "link": "https://invite.zappo.city/?invite=JsR4bKVKV51X3IFseJvu2g",
          "note": "for dave",
          "plan": "free",
          "revokedAt": null,
          "status": "open",
          "usedAt": null,
          "usedBy": null
        }
      ]
    }
POST /api/mail/invites

Make one invitation link

Uses up one of the user's invitations. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 201 The new invitation
  • 403 No invitations left
DELETE /api/mail/invites/{code}

Cancel one of the user's unused links

The invitation goes back to the user's allowance. Needs the cookie and X-CSRF-Token.

ParameterInType
code *pathstring
X-CSRF-Token *headerstring
Responses
  • 204 Cancelled.
  • 404 No unused invitation of the user's has that code.
GET /api/mail/later

The signed-in user's snoozed messages and muted conversations

snoozed maps each snoozed message's JMAP id to when it comes back; muted lists the JMAP thread ids of muted conversations. Scheduled sends are JMAP: EmailSubmission/get with undoStatus "pending".

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Snoozes and mutes.
    {
      "muted": [
        "T17"
      ],
      "snoozed": {
        "E42": "2026-10-09T08:00:00Z"
      }
    }
GET /api/mail/login-history

The user's recent sign-ins to every service

Up to 200, newest first, within the days the platform keeps (days).

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The sign-ins.
    {
      "days": 90,
      "items": [
        {
          "at": "2026-10-08T11:59:00Z",
          "id": 7,
          "ip": "192.0.2.10",
          "login": "alice@example.com",
          "reason": "password",
          "service": "imap",
          "success": false,
          "userAgent": ""
        }
      ]
    }
POST /api/mail/mute

Mute or unmute a conversation

New mail in a muted conversation is marked read and filed in Archive instead of the inbox, on whichever MX server receives it. Mail already there is not moved.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "muted": true,
  "threadId": "T17"
}
Responses
  • 204 Changed.
  • 400 Not the JSON above.
  • 404 No such conversation in the user's account.
POST /api/mail/password

Change the signed-in user's password

Needs the cookie and X-CSRF-Token. Every session ends, mail apps included; this browser gets a new session cookie and CSRF token in the answer. A notice goes to the recovery address if there is one.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Changed. Sets a new session cookie.
  • 401 Not signed in.
  • 403 Wrong current password
  • 422 The new password is too short or too long.
  • 429 Too many wrong passwords.
POST /api/mail/password-reset

Ask for a password reset link

Always answers 202 the same way, whether or not the account exists or has a recovery address, so the answer does not reveal accounts. Suspended accounts get no link. At most 10 requests per source address and per account address an hour.

Responses
  • 202 A link was sent if the account has a recovery address.
  • 415 Not JSON.
  • 429 Too many requests.
POST /api/mail/password-reset/confirm

Set a new password from a reset link

The token from #reset= works once, for an hour, and only while the account is active and its recovery address is unchanged. Every session ends; sign in with the new password.

Responses
  • 200 The password is set.
    {
      "address": "alice@example.com"
    }
  • 400 The link expired or was already used.
  • 422 The new password is too short or too long; the link still works.
PUT /api/mail/plus-mode

Choose what happens to the user's name+tag@ mail

folders files it in a folder named after the tag (only with the plus_folders entitlement), inbox keeps it in the inbox, refuse turns it away as an unknown address, and an empty string follows the domain's setting.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "mode": "refuse"
}
Responses
  • 204 Saved.
  • 400 Not the JSON above.
  • 403 `folders` without the plus_folders entitlement.
  • 422 Not one of the modes.
POST /api/mail/recovery

Set or remove the recovery address

Needs the cookie, X-CSRF-Token and the current password. An empty email removes the recovery address at once. Any other address gets a confirmation link and replaces the old one only when the link is opened.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "currentPassword": "correct horse battery",
  "email": "alice.personal@example.net"
}
Responses
  • 200 Removed.
  • 202 Confirmation link sent.
    {
      "pending": "alice.personal@example.net",
      "recoveryEmail": null
    }
  • 403 Wrong current password
  • 422 Not one plain address
  • 429 Too many wrong passwords
  • 502 The confirmation could not be sent.
POST /api/mail/recovery/verify

Confirm a recovery address from its link

No session needed; the token from #verify= proves the mail arrived.

Responses
  • 200 Confirmed.
    {
      "address": "alice@example.com",
      "recoveryEmail": "alice.personal@example.net"
    }
  • 400 The link expired or was already used.
POST /api/mail/report

Report messages as spam or phishing

Each message is copied into a report for the team's administrators (and the platform operator) and moved to Junk, which also teaches the spam filter. Messages not in the user's account are skipped. Up to 100 at a time and 200 a day.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "emailIds": [
    "E42"
  ],
  "kind": "phishing"
}
Responses
  • 200 How many were reported.
    {
      "junkId": "M4",
      "reported": 1
    }
  • 400 Not the JSON above.
  • 429 Too many reports today.
GET /api/mail/security

Two-step sign-in state and app passwords

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The signed-in user's security settings.
  • 401 Not signed in.
  • 403 Missing or wrong CSRF token.
GET /api/mail/security-alerts

Which security events the user is emailed about

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The choices.
    {
      "failed": true,
      "newSignin": true
    }
PUT /api/mail/security-alerts

Choose which security events are emailed

newSignin: a sign-in from an address the account was not used from before. failed: repeated wrong passwords and lockouts. Alerts go to the account and its recovery address.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 204 Saved.
POST /api/mail/send

Send a plain-text message as a mailbox, with its API token

For other services' own mail (Zappocity's confirmation and reset links). Authenticate with Authorization: Bearer cs_mt_..., a mailbox API token with the send scope, made in the admin panel. The message is from the mailbox's address and passes the same checks and limits as mail its user sends (the plan's sending limits apply), and it is archived like it; it is not filed in Sent. Each recipient gets a message of their own. The same tokens with the jmap scope sign in to JMAP as the mailbox, as Authorization: Bearer.

Request
{
  "fromName": "Zappocity",
  "subject": "Confirm your email address",
  "text": "Open this link...",
  "to": [
    "amy@example.com"
  ]
}
Responses
  • 202 Sent (or queued for other domains).
    {
      "sent": 1
    }
  • 400 Not JSON with the documented fields.
  • 401 No mailbox API token with the send scope
  • 422 Too many recipients, a header on two lines, or the mailbox may not send this (its limits, for example).
GET /api/mail/session

The signed-in account and its CSRF token

Needs the cookie only.

Responses
  • 200 Signed in.
  • 401 No session
POST /api/mail/session

Sign in to webmail

Body must be application/json; anything else answers 415.

Request
{
  "address": "alice@example.com",
  "password": "correct horse battery"
}
Responses
  • 201 Signed in. Sets the session cookie.
  • 401 Wrong address or password, or the account cannot sign in. The same answer for unknown addresses. With the right password for an account with two-step sign-in, the problem has `twoFactorRequired: true`: send the request again with `code`.
    {
      "detail": "Enter the code from your authenticator app",
      "status": 401,
      "twoFactorRequired": true,
      "type": "about:blank"
    }
  • 403 The password was right but the plan does not include webmail.
  • 415 Not JSON.
  • 429 Too many failed sign-ins.
DELETE /api/mail/session

Sign out

Ends the session on the server and clears the cookie. Needs X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 204 Signed out.
  • 401 No session.
  • 403 Missing or wrong CSRF token.
GET /api/mail/sessions

Where the user is signed in to webmail

Each session's address, browser, start and last use; current marks the one asking. id is a short hash, never the token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The sessions.
    {
      "items": [
        {
          "createdAt": "2026-10-08T09:00:00Z",
          "current": true,
          "expiresAt": "2026-10-15T09:00:00Z",
          "id": "3f2a9c1b7d4e5f60",
          "ip": "192.0.2.10",
          "lastSeen": "2026-10-08T12:00:00Z",
          "userAgent": "Mozilla/5.0 ..."
        }
      ]
    }
DELETE /api/mail/sessions/{id}

Sign out one session, or every other one

The id others signs out every session but the one asking.

ParameterInType
id *pathstring
X-CSRF-Token *headerstring
Responses
  • 204 Signed out.
  • 404 No such session of the user.
GET /api/mail/shares

Who the user shares their mailbox with, whose is shared with them, and their teammates

A mailbox shared with the user shows in the JMAP session as another account (accountId here), read only (read), to change (write) or also to send from (send). Shares are only between teammates.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Shares and teammates.
    {
      "sharedByMe": [
        {
          "access": "send",
          "address": "assistant@acme.test",
          "name": "Sam",
          "since": "2026-10-08T15:00:00Z"
        }
      ],
      "sharedWithMe": [
        {
          "access": "read",
          "accountId": "2b7f1d6e-3c4a-4e8b-9f10-5a6b7c8d9e0f",
          "address": "sales@acme.test",
          "name": "Sales",
          "since": "2026-10-01T09:00:00Z"
        }
      ],
      "teammates": [
        {
          "address": "assistant@acme.test",
          "name": "Sam"
        },
        {
          "address": "sales@acme.test",
          "name": "Sales"
        }
      ]
    }
PUT /api/mail/shares/{address}

Share the mailbox with a teammate, or change how

Refused (403) in a mailbox opened by staff or a team admin.

ParameterInType
address *pathstringThe teammate's address.
X-CSRF-Token *headerstring
Request
{
  "access": "write"
}
Responses
  • 200 The share.
  • 403 Opened by someone else.
  • 404 Nobody on the team has that address.
  • 422 access is not read
DELETE /api/mail/shares/{address}

Stop sharing the mailbox with a teammate

ParameterInType
address *pathstringThe teammate's address.
X-CSRF-Token *headerstring
Responses
  • 204 No longer shared.
  • 404 Not shared with that teammate.
POST /api/mail/snooze

Snooze messages until a time

Moves each message out of mailboxId to the Snoozed folder (made when first needed) and brings it back to mailboxId, unread, at until, which must be in the next year. Messages not in the user's account are skipped.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "emailIds": [
    "E42"
  ],
  "mailboxId": "M1",
  "until": "2026-10-09T08:00:00Z"
}
Responses
  • 200 Snoozed.
    {
      "mailboxId": "M7",
      "snoozedUntil": "2026-10-09T08:00:00Z"
    }
  • 400 Not the JSON above.
  • 422 `until` is not in the next year, or `mailboxId` is not one of the user's folders.
DELETE /api/mail/snooze/{emailId}

Bring a snoozed message back now

ParameterInType
emailId *pathstring
X-CSRF-Token *headerstring
Responses
  • 204 Back in its folder
  • 404 No such message
GET /api/mail/spam-policy

The user's spam settings and what applies to them

junkScore, allow and block are the user's own; effectiveJunkScore and rejectScore are what applies after the team's and the platform's settings. enabled says whether spam filtering is on at all.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The settings.
    {
      "allow": [
        "news.example"
      ],
      "block": [],
      "effectiveJunkScore": 6,
      "enabled": true,
      "junkScore": null,
      "rejectScore": 15
    }
PUT /api/mail/spam-policy

Replace the user's own junk score and sender lists

junkScore (null for the team's) is the score at which mail goes to Junk. allow and block hold up to 1000 addresses or domains each: allowed senders are never filed in Junk as spam, blocked ones always are.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "allow": [
    "news.example"
  ],
  "block": [
    "spammy.example"
  ],
  "junkScore": 5
}
Responses
  • 204 Saved.
  • 400 Not the JSON above.
  • 422 A list entry is not an address or domain
GET /api/mail/sso

Which ways of signing in to webmail are open

Responses
  • 200 Whether Sign in with Zappocity is offered, and whether the password sign-in still works.
    {
      "passwordSignin": true,
      "sso": true
    }
GET /api/mail/sso/callback

Where Zappocity sends the browser back

ParameterInType
codequerystring
statequerystring
Responses
  • 302 Signed in, to next; to /mail#sso-code when the mailbox has two-step sign-in; or to /mail#sso-error=... (expired, unverified, no-mailbox, suspended, not-in-plan).
POST /api/mail/sso/code

Finish signing in with Zappocity with the mailbox's two-step code

Responses
  • 201 Signed in; `session` is as from POST /api/mail/session, `next` where to go.
  • 401 Wrong code (twoFactorRequired is true), or the sign-in has expired.
  • 429 Too many failed attempts.
GET /api/mail/sso/start

Sign in to webmail (or the team manager) with Zappocity

Sends the browser to Zappocity's sign-in; it comes back to /api/mail/sso/callback. The mailbox linked to the Zappocity account (the one with its email as address first) is signed in, when Zappocity has confirmed the email. next is /mail or /manage.

ParameterInType
nextquerystring
Responses
  • 302 To Zappocity
GET /api/mail/tickets

The user's tickets

teamAdmins says the team has other owners or admins to ask.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Their tickets, as Ticket in api/openapi.yaml.
    {
      "items": [
        {
          "id": 12,
          "priority": "normal",
          "queue": "team",
          "status": "pending",
          "subject": "Phone will not sync"
        }
      ],
      "teamAdmins": true
    }
POST /api/mail/tickets

Ask the team's admins or Zappocity support

A team ticket emails the team's owners and admins (up to 20 open tickets per user). to: platform opens a request on the Zappocity help desk instead, as the user, and answers zappocityRef (ZC-41) and url, where the user follows it on their Zappocity account; the user is emailed. On a plan with priority support it starts at high priority.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 201 The team ticket, or for Zappocity support its number and link.
    {
      "url": "https://zappo.city/account#ticket=41",
      "zappocityRef": "ZC-41"
    }
  • 422 A value is missing.
  • 429 Too many open tickets.
GET /api/mail/tickets/{id}

One of the user's tickets with its replies (never internal notes)

ParameterInType
id *pathinteger
X-CSRF-Token *headerstring
Responses
  • 200 The ticket and its messages.
  • 404 Not one of the user's tickets.
POST /api/mail/tickets/{id}/close

Close one of the user's tickets

Abuse tickets are closed by the platform's staff only (403).

ParameterInType
id *pathinteger
X-CSRF-Token *headerstring
Responses
  • 204 Closed.
  • 403 An abuse ticket.
POST /api/mail/tickets/{id}/messages

Reply on a ticket; it opens again

ParameterInType
id *pathinteger
X-CSRF-Token *headerstring
Responses
  • 204 Sent.
  • 409 The ticket is closed.
POST /api/mail/trackers

Make a read tracker for a message about to be sent

For plans with the read_tracking entitlement (see readTracking in the session). Returns a token and the URL of a 1x1 image to put in the message's HTML; each load of that image counts as an open. At most 1,000 per account per day. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The new tracker.
    {
      "token": "qOgc5Ee4_GC8gG98OT-Sew",
      "url": "https://mail.example.com/t/qOgc5Ee4_GC8gG98OT-Sew.gif"
    }
  • 403 The plan does not include read tracking.
  • 429 Too many trackers made today.
POST /api/mail/trackers/stats

Get the open counts of the signed-in user's trackers

Tokens that are not the account's own are left out. proxiedOpens counts loads through a mail provider's image proxy (Gmail, Yahoo), which can happen without anyone reading. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Counts keyed by token.
    {
      "trackers": {
        "qOgc5Ee4_GC8gG98OT-Sew": {
          "createdAt": "2026-10-07T20:50:30Z",
          "firstOpen": "2026-10-07T20:50:32Z",
          "lastOpen": "2026-10-07T20:50:32Z",
          "opens": 1,
          "proxiedOpens": 0,
          "token": "qOgc5Ee4_GC8gG98OT-Sew"
        }
      }
    }
  • 400 Not the expected JSON
POST /api/mail/two-factor/disable

Turn two-step sign-in off

The password alone signs in again, in webmail and mail apps. Recovery codes are deleted; app passwords stay.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 204 Off.
  • 403 Wrong current password
  • 429 Too many wrong passwords.
POST /api/mail/two-factor/enable

Turn two-step sign-in on

Takes a code from the secret made by setup. Returns ten recovery codes, shown only now. Every other session ends, mail apps included; from now on mail apps need app passwords.

ParameterInType
X-CSRF-Token *headerstring
Request
{
  "code": "492039"
}
Responses
  • 200 On.
  • 409 No setup is waiting
  • 422 The code is wrong.
  • 429 Too many wrong codes.
POST /api/mail/two-factor/recovery-codes

Replace the recovery codes

The old codes stop working. The new ones are shown only now.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 New codes.
  • 403 Wrong current password
  • 409 Two-step sign-in is off.
  • 429 Too many wrong passwords.
POST /api/mail/two-factor/setup

Make a new authenticator secret

Makes a new secret and returns it as text, an otpauth URI and a QR code. Signing in does not change until enable confirms a code from it. Calling it again replaces the secret that is waiting.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 The new secret. Shown only now.
  • 403 Wrong current password
  • 409 Two-step sign-in is already on.
  • 429 Too many wrong passwords.
  • 503 The server has no key to seal secrets with.
POST /api/mail/unsubscribe

Unsubscribe from a mailing list message

For one of the user's messages with a List-Unsubscribe header (Email/get's listUnsubscribe extension says what it offers). When it offers RFC 8058 one-click, the server POSTs List-Unsubscribe=One-Click to the https link itself, through the same public-address-only dialer as remote images, and answers done. Otherwise it returns the mailto address (and subject) or the web page for the user to use. Needs the cookie and X-CSRF-Token.

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Done, or what to do instead.
    {
      "done": false,
      "mailto": "leave@list.example",
      "subject": "unsubscribe",
      "url": "https://list.example/u/123"
    }
  • 404 No such message
GET /api/mail/usage

Storage and sending used by the signed-in user

ParameterInType
X-CSRF-Token *headerstring
Responses
  • 200 Use and limits.
    {
      "quotaBytes": 32212254720,
      "sendPerDay": 1000,
      "sentLastDay": 3,
      "storageBytes": 16179
    }
GET /t/{file}

The read-tracking image

Public: loaded by the recipient's mail app. file is {token}.gif. Counts an open when the token exists and always answers with the same transparent 1x1 GIF, uncached. Only the count and times are kept, never the reader's address or browser.

ParameterInType
file *pathstring
Responses
  • 200 A 1x1 transparent GIF.