Zappocity platform API

version 1.12.0 OpenAPI spec (YAML)

Zappocity is the central site for every product it sells (Mailocity today). This API keeps the customers and what they subscribe to; each product carries subscriptions out through the product contract: making a Mailocity subscription makes (or adopts) a Mailocity team.

Authentication is the admin API's: the platform API key as a bearer token, or an admin panel session with its X-CSRF-Token. Staff need the read permission to read, plans to change products and prices, and tenants for everything else. Teams cannot call it.

Every change is recorded in an append-only audit log with who made it (GET /v1/platform/audit). Amounts are whole US cents.

See docs/kb/zappocity-accounts.md and docs/architecture/zappocity-platform.md.

Audit

GET /v1/platform/audit

Every change, with who made it

ParameterInType
subjectquerystringorg:<id>, person:<id>, product:<key> or subscription:<id>.
beforequeryintegerPage by entry id.
limitqueryinteger
Responses
  • 200 Newest first.
    {
      "items": [
        {
          "action": "provision",
          "at": "2026-10-08T12:00:00Z",
          "by": "the platform API key",
          "detail": "mailocity team year for Acme, resource 4f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
          "id": 12,
          "subject": "subscription:9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d"
        }
      ]
    }
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.

Billing

POST /v1/platform/billing/run

Bill what is due now

Otherwise billing runs every hour. Each organization whose subscriptions have reached their renewal gets one invoice: each plan at its price for the period, less discounts, less credit in its favour. An invoice with nothing left to pay is paid; one that is open is charged to the saved payment method when the organization pays automatically. Free organizations, and plans without a price, are renewed without an invoice.

Responses
  • 200 How many invoices were issued.
    {
      "invoices": 3
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
GET /v1/platform/invoices

Invoices of every organization

ParameterInType
statusquerystring
Responses
  • 200 Newest first
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
GET /v1/platform/invoices/{invoiceId}

An invoice with its lines

ParameterInType
invoiceId *pathstring
Responses
  • 200 The invoice.
    {
      "creditCents": 2500,
      "discountCents": 0,
      "dueAt": "2026-10-15T12:00:00Z",
      "lines": [
        {
          "amountCents": 12000,
          "description": "mailocity team, yearly",
          "kind": "charge"
        },
        {
          "amountCents": -2500,
          "description": "Account credit",
          "kind": "credit"
        }
      ],
      "number": 7,
      "orgName": "Acme",
      "status": "open",
      "subtotalCents": 12000,
      "totalCents": 9500
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/invoices/{invoiceId}/pay

Record a payment made outside the payment provider

ParameterInType
invoiceId *pathstring
Responses
  • 200 The invoice
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 The invoice is not open.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/invoices/{invoiceId}/void

Cancel an open invoice

Its charge goes back on the balance, as a ledger entry.

ParameterInType
invoiceId *pathstring
Responses
  • 200 The invoice
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 The invoice is not open.
GET /v1/platform/orgs/{orgId}/billing

An organization's billing and balance

A positive balance is credit in its favour; a negative one is owed.

ParameterInType
orgId *pathstring
Responses
  • 200 The profile.
    {
      "autoPay": true,
      "balanceCents": 2500,
      "free": true,
      "freeReason": "Launch partner",
      "freeUntil": "2027-01-01T00:00:00Z",
      "mode": "free",
      "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
      "updatedAt": "2026-10-08T12:00:00Z"
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
PATCH /v1/platform/orgs/{orgId}/billing

Make an organization free, or charged again; automatic payment

mode: free (with freeReason) charges nothing for its products, for good or until freeUntil (null: for good); standard charges again. Recorded in the audit log. Needs the billing permission.

ParameterInType
orgId *pathstring
Request
{
  "freeReason": "Launch partner",
  "freeUntil": "2027-01-01",
  "mode": "free"
}
Responses
  • 200 The profile.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/orgs/{orgId}/invoices

An organization's invoices

ParameterInType
orgId *pathstring
statusquerystring
Responses
  • 200 Newest first.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
GET /v1/platform/orgs/{orgId}/ledger

An organization's balance history

Every change, newest first, each with the balance after it. Entries are never changed or deleted.

ParameterInType
orgId *pathstring
beforequeryinteger
limitqueryinteger
Responses
  • 200 The entries.
    {
      "items": [
        {
          "amountCents": 2500,
          "at": "2026-10-08T12:00:00Z",
          "balanceCents": 2500,
          "by": "owner@zappocity.test",
          "description": "Sorry for the outage",
          "id": 3,
          "invoiceId": null,
          "kind": "credit",
          "reference": "ZC-41"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/orgs/{orgId}/ledger

Add credit, take it away, or record a payment or refund

credit and payment add to the balance; debit and refund take from it (the balance may go below zero: the organization owes it). Correct a mistake with an opposite entry.

ParameterInType
orgId *pathstring
Request
{
  "amountCents": 2500,
  "description": "Sorry for the outage",
  "kind": "credit",
  "reference": "ZC-41"
}
Responses
  • 201 The entry.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/payments

The payment provider

Stripe today. Customers save a payment method and pay invoices on Stripe's hosted pages (any method enabled in the Stripe dashboard: cards, wallets, bank debits, crypto where Stripe offers it); card details never reach Zappocity. Point a Stripe webhook at https://zappo.city/api/billing/webhook/stripe for checkout.session.completed and charge.refunded. Keys are kept sealed and never returned.

Responses
  • 200 The settings.
    {
      "hasSecretKey": true,
      "hasWebhookSecret": true,
      "provider": "stripe",
      "updatedAt": "2026-10-08T12:00:00Z"
    }
  • 401 No valid platform API key or admin panel session.
PATCH /v1/platform/payments

Set the payment provider and its keys

Request
{
  "provider": "stripe",
  "secretKey": "sk_live_xxx",
  "webhookSecret": "whsec_xxx"
}
Responses
  • 200 The settings.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/payments/test

Check the key works

Responses
  • 200 Whether Stripe took the key, and why not.
    {
      "error": "stripe: Invalid API Key provided (401 )",
      "ok": false
    }
  • 401 No valid platform API key or admin panel session.

Mail

GET /v1/platform/mail

How Zappocity sends its own mail

jmap sends over JMAP (EmailSubmission) as a Mailocity mailbox: apiUrl is Mailocity's address and apiToken a mailbox API token with the jmap scope; it works the same in one process or once the services run apart. internal sends through Mailocity in the same process (from its notice address, under fromName) until JMAP is set up; smtp sends by SMTP submission as a mailbox with an app password. api (the old send-API mode) is taken as jmap. Secrets are never returned. Staff need the settings permission to change it.

Responses
  • 200 The settings.
    {
      "apiUrl": "https://mailo.city",
      "fromName": "Zappocity",
      "hasApiToken": true,
      "hasSmtpPassword": false,
      "mode": "jmap",
      "smtpHost": "",
      "smtpPort": 587,
      "smtpSecurity": "starttls",
      "smtpUsername": "",
      "updatedAt": "2026-10-08T12:00:00Z"
    }
  • 401 No valid platform API key or admin panel session.
PATCH /v1/platform/mail

Change how Zappocity sends its own mail

Request
{
  "apiToken": "cs_mt_Yk3",
  "apiUrl": "https://mailo.city",
  "mode": "jmap"
}
Responses
  • 200 The settings.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/mail/test

Send a test message the configured way

Responses
  • 200 Whether it went, and the error if not.
    {
      "ok": true
    }
  • 401 No valid platform API key or admin panel session.
  • 422 A value is out of range or names something that does not exist.

Organizations

GET /v1/platform/orgs

Find organizations

ParameterInType
qquerystringPart of the name
limitqueryinteger
Responses
  • 200 By name.
    {
      "items": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "id": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "kind": "company",
          "members": 3,
          "name": "Acme",
          "status": "active",
          "updatedAt": "2026-10-08T12:00:00Z"
        }
      ]
    }
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/orgs

Add an organization with its owner

The one who pays for subscriptions. owner is a person's id or email; they must exist.

Request
{
  "name": "Acme",
  "owner": "amy@example.com"
}
Responses
  • 201 The organization.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/orgs/{orgId}

An organization with its members and subscriptions

ParameterInType
orgId *pathstring
Responses
  • 200 The organization.
    {
      "members": [],
      "org": {
        "id": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
        "kind": "company",
        "members": 1,
        "name": "Acme",
        "status": "active"
      },
      "subscriptions": []
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PATCH /v1/platform/orgs/{orgId}

Rename, or suspend and reactivate

Suspending suspends every active subscription's resource (a Mailocity team cannot sign in or receive mail); making it active again brings them back.

ParameterInType
orgId *pathstring
Request
{
  "status": "suspended"
}
Responses
  • 200 The organization.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
PUT /v1/platform/orgs/{orgId}/members/{personId}

Add a member or change their role

owner (everything), billing (invoices and payment), support (tickets) or member. The last owner cannot be demoted (409).

ParameterInType
orgId *pathstring
personId *pathstring
Request
{
  "role": "billing"
}
Responses
  • 200 The members.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
DELETE /v1/platform/orgs/{orgId}/members/{personId}

Remove a member

The last owner cannot be removed (409).

ParameterInType
orgId *pathstring
personId *pathstring
Responses
  • 204 Removed.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.

People

GET /v1/platform/people

Find people

ParameterInType
qquerystringPart of an email or name.
limitqueryinteger
Responses
  • 200 By email.
    {
      "items": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "email": "amy@example.com",
          "emailVerifiedAt": "2026-10-08T12:00:00Z",
          "hasPassword": true,
          "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
          "name": "Amy",
          "status": "active",
          "updatedAt": "2026-10-08T12:00:00Z"
        }
      ]
    }
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/people

Add a person

A Zappocity account. Without a password, they can set one later.

Request
{
  "email": "amy@example.com",
  "name": "Amy",
  "verified": true
}
Responses
  • 201 The person.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/people/{personId}

A person and the organizations they belong to

ParameterInType
personId *pathstring
Responses
  • 200 The person.
    {
      "memberships": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "email": "amy@example.com",
          "name": "Amy",
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgName": "Acme",
          "personId": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
          "role": "owner"
        }
      ],
      "person": {
        "email": "amy@example.com",
        "hasPassword": true,
        "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
        "name": "Amy",
        "status": "active"
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PATCH /v1/platform/people/{personId}

Change a person's name, status, password or verification

Suspending signs them out everywhere.

ParameterInType
personId *pathstring
Request
{
  "status": "suspended"
}
Responses
  • 200 The person.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.

Products

GET /v1/platform/products

The product catalog

Every product, with its plans (from the product's module) and their prices per period.

Responses
  • 200 The catalog.
    {
      "currency": "USD",
      "items": [
        {
          "description": "Business email on your own domains.",
          "key": "mailocity",
          "module": true,
          "name": "Mailocity",
          "plans": [
            {
              "active": true,
              "code": "team",
              "limits": {
                "aliases": 100,
                "domains": 10,
                "messageBytes": 52428800,
                "sendPerDay": 5000,
                "sendPerHour": 500,
                "storageBytes": 32212254720,
                "users": 25
              },
              "name": "Team",
              "prices": {
                "month": 1200,
                "year": 12000
              }
            }
          ],
          "siteHost": "mailo.city",
          "sortOrder": 10,
          "status": "available",
          "updatedAt": "2026-10-08T12:00:00Z"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
PATCH /v1/platform/products/{product}

Change a product's name, description, status or site

available products are sold; coming ones are shown as on the way; retired ones take no new subscriptions. Only a product with a module here can be available.

ParameterInType
product *pathstring
Request
{
  "siteHost": "mailo.city"
}
Responses
  • 200 The product.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/products/{product}/features

The features a product's plans can include

For products whose plans Zappocity manages (Mailocity). enforced says the product acts on a feature today; the others are kept for what is planned.

ParameterInType
product *pathstring
Responses
  • 200 The features.
    {
      "items": [
        {
          "description": "Read and send mail in the browser at /mail",
          "enforced": true,
          "key": "webmail",
          "name": "Webmail"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
  • 409 This product's plans are not managed here.
PUT /v1/platform/products/{product}/plans/{plan}

Make or change a plan's limits and features

Limits by the names the catalog shows (users, domains, aliases, storageBytes, messageBytes, sendPerHour, sendPerDay, recipients, aliasesPerUser, sharedAliasesPerUser; -1 is unlimited); a limit left out stays. features replaces the plan's features (left out: they stay). A new plan needs a name and every limit. The product keeps and enforces it; prices are set with /prices.

ParameterInType
product *pathstring
plan *pathstring
Request
{
  "active": true,
  "features": [
    "webmail",
    "imap",
    "smtp_submission",
    "team_admin",
    "archiving"
  ],
  "limits": {
    "storageBytes": 53687091200,
    "users": 50
  },
  "name": "Team"
}
Responses
  • 200 The plan
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 Refused (an unknown limit or feature
PUT /v1/platform/products/{product}/plans/{plan}/prices

Set a plan's prices

In cents per period. A field left out stays; null stops selling the plan for that period.

ParameterInType
product *pathstring
plan *pathstring
Request
{
  "month": 1200,
  "year": 12000
}
Responses
  • 200 The whole catalog.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.

Single sign-on

GET /v1/platform/sso-clients

Applications that sign in through Zappocity

Zappocity is an OpenID Connect provider: discovery at /.well-known/openid-configuration, the authorization code flow with PKCE (S256, required), RS256 ID tokens, userinfo. These are the registered clients; secrets are kept hashed.

Responses
  • 200 The clients.
    {
      "items": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "disabled": false,
          "id": "mailocity",
          "name": "Mailocity",
          "redirectUris": [
            "https://mailo.city/api/mail/sso/callback"
          ],
          "updatedAt": "2026-10-08T12:00:00Z"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/sso-clients

Register an application

The secret is in the answer once.

Request
{
  "id": "xiht",
  "name": "xi.ht",
  "redirectUris": [
    "https://xi.ht/auth/callback"
  ]
}
Responses
  • 201 The client and its secret.
    {
      "client": {
        "id": "xiht",
        "name": "xi.ht"
      },
      "secret": "cs_cs_Yk3"
    }
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
PATCH /v1/platform/sso-clients/{clientId}

Rename, change return addresses, or disable

ParameterInType
clientId *pathstring
Responses
  • 200 The client.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
DELETE /v1/platform/sso-clients/{clientId}

Remove an application

ParameterInType
clientId *pathstring
Responses
  • 204 Removed.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/sso-clients/{clientId}/secret

Give an application a new secret

The old secret stops working at once; the new one is in the answer once.

ParameterInType
clientId *pathstring
Responses
  • 200 The client and its new secret.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.

Subscriptions

POST /v1/platform/orgs/{orgId}/subscriptions

Subscribe an organization to a product

The product provisions it (a new Mailocity team named after the organization). With resourceRef, the product instead adopts what already exists (a Mailocity tenant id), keeping its plan unless plan names another; a resource can belong to one live subscription. If the product cannot do it, the subscription is recorded as canceled and the error returned.

ParameterInType
orgId *pathstring
Request
{
  "period": "year",
  "plan": "team",
  "product": "mailocity"
}
Responses
  • 201 The subscription.
    {
      "canceledAt": null,
      "createdAt": "2026-10-08T12:00:00Z",
      "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
      "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
      "orgName": "Acme",
      "period": "year",
      "plan": "team",
      "product": "mailocity",
      "resourceRef": "4f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
      "status": "active",
      "updatedAt": "2026-10-08T12:00:00Z"
    }
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/subscriptions

Find subscriptions

ParameterInType
orgIdquerystring
productquerystring
resourceRefquerystringWhich subscription a resource (a tenant id) belongs to.
statusquerystring
limitqueryinteger
Responses
  • 200 Newest first.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
GET /v1/platform/subscriptions/{subscriptionId}

A subscription and what the product reports on it

ParameterInType
subscriptionId *pathstring
Responses
  • 200 The subscription, and the product's summary of its resource.
    {
      "resource": {
        "limits": {
          "users": 25
        },
        "name": "Acme",
        "plan": "team",
        "ref": "4f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
        "status": "active",
        "usage": {
          "aliases": 2,
          "domains": 1,
          "storageBytes": 1048576,
          "users": 3
        }
      },
      "subscription": {
        "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
        "plan": "team",
        "product": "mailocity",
        "status": "active"
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PATCH /v1/platform/subscriptions/{subscriptionId}

Change the plan or period, suspend, resume or cancel

The product makes the change first, and the subscription changes only if it did (a plan the team does not fit is refused with 409). Canceling suspends the resource and unlinks it, keeping its data; a canceled subscription cannot change. A subscription of a suspended organization cannot be resumed.

ParameterInType
subscriptionId *pathstring
Request
{
  "plan": "team"
}
Responses
  • 200 The subscription.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/subscriptions/{subscriptionId}/features

A subscription's features

Its plan's features, those switched on and off for this customer, and the result the product enforces.

ParameterInType
subscriptionId *pathstring
Responses
  • 200 The features.
    {
      "effective": [
        "webmail",
        "smtp_submission",
        "archiving"
      ],
      "off": [
        "imap"
      ],
      "on": [
        "archiving"
      ],
      "plan": [
        "webmail",
        "imap",
        "smtp_submission"
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
  • 409 Nothing provisioned
PUT /v1/platform/subscriptions/{subscriptionId}/features

Switch features on or off for one subscription

Replaces the subscription's switches; each account's own switches (team admins and staff) still apply on top.

ParameterInType
subscriptionId *pathstring
Request
{
  "off": [
    "imap"
  ],
  "on": [
    "archiving"
  ]
}
Responses
  • 200 The features after the change.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 Refused (an unknown feature

Support

GET /v1/platform/support/follow-ups

Reminders that are due

Responses
  • 200 Oldest first.
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/support/intake

Read the queues' mailboxes now

Otherwise they are read every minute. Replies go on their ticket (by In-Reply-To and References, or the [ZC-n] tag, from someone on the ticket); other mail opens a ticket when the queue takes mail from that sender. Each message is filed out of the inbox, into Tickets, Not accepted or Ignored (auto-replies, bounces, no-reply senders).

Responses
  • 204 Done.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
GET /v1/platform/support/queues

The support queues and their settings

Responses
  • 200 The queues.
    {
      "items": [
        {
          "active": true,
          "address": "abuse@zappo.city",
          "addresses": [
            "abuse@mailo.city"
          ],
          "allowContacts": true,
          "customerClose": false,
          "defaultPriority": "normal",
          "emailIntake": true,
          "hasMailbox": true,
          "id": "3f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
          "intakeFrom": "anyone",
          "name": "Abuse",
          "public": true,
          "readInEmail": true,
          "replyByEmail": true,
          "slug": "abuse",
          "webReply": true,
          "webReplyLogin": false
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/support/queues

Add a queue

When a support team is set up, its mailbox (slug@ the team's domain) is made with it. Settings: emailIntake (mail to the address opens tickets) from anyone or only known people (with a Zappocity account); readInEmail (notifications carry the message, or only say there is one); replyByEmail; webReply (a link to the ticket's page), webReplyLogin (that page needs signing in); allowContacts (customers and staff can add people to the replies); customerClose (customers may close their requests; off for abuse); public (offered when customers open a request); active. Needs the settings permission.

Request
{
  "customerClose": false,
  "emailIntake": true,
  "intakeFrom": "anyone",
  "name": "Abuse",
  "slug": "abuse"
}
Responses
  • 201 The queue.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
PATCH /v1/platform/support/queues/{queueId}

Change a queue's settings

The slug (its address) cannot change. Turn a queue off with active false.

ParameterInType
queueId *pathstring
Request
{
  "webReplyLogin": true
}
Responses
  • 200 The queue.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
DELETE /v1/platform/support/queues/{queueId}

Remove a queue

A queue with tickets needs moveTo, the slug of the queue they move to; without it the answer is 409 with how many there are. The queue's other addresses stop working; its mailbox and the mail in it stay on the support team. The support queue cannot be removed: requests from webmail and imports land in it (turn it off with active false instead). Needs the settings permission.

ParameterInType
queueId *pathstring
moveToquerystringSlug of the queue the tickets move to.
Responses
  • 200 Removed.
    {
      "moved": 12
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
PUT /v1/platform/support/queues/{queueId}/addresses/{address}

Let a queue take mail at another address

The address becomes an alias of the queue's mailbox, so mail to it opens and answers tickets like mail to the queue's own address, and notifications for tickets written to it go out from it. Its domain must be one of the support team's (add one with POST /v1/platform/support/settings/domains). Adding an address the queue has is a no-op. Needs the settings permission.

ParameterInType
queueId *pathstring
address *pathstring
Responses
  • 200 The queue
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
DELETE /v1/platform/support/queues/{queueId}/addresses/{address}

Stop a queue taking mail at one of its other addresses

The alias is removed; tickets written to it are answered from the queue's own address.

ParameterInType
queueId *pathstring
address *pathstring
Responses
  • 200 The queue.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/support/queues/{queueId}/mailbox

Make the queue's mailbox, or give it a new token

ParameterInType
queueId *pathstring
Responses
  • 200 The queue.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
GET /v1/platform/support/settings

The support team that holds the queues' mailboxes

Responses
  • 200 The settings.
    {
      "domain": "zappo.city",
      "jmapUrl": "https://mailo.city",
      "teamRef": "4f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
      "updatedAt": "2026-10-08T12:00:00Z"
    }
  • 401 No valid platform API key or admin panel session.
PUT /v1/platform/support/settings

Set up the support team

Makes a team on Mailocity with the domain (or adopts the team teamRef names, adding the domain), and gives every queue its mailbox (slug@domain) with a token; new queues get one when they are made. Zappocity reads the mailboxes and sends from them over JMAP at jmapUrl (Mailocity's address here when left out). Publish the domain's DNS records (MX, SPF, DKIM, DMARC) from its page under Tenants. Needs the settings permission.

Request
{
  "domain": "zappo.city"
}
Responses
  • 200 The settings.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/support/settings/domains

Add a mail domain to the support team

For queue addresses on it. A domain another team or a shared domain has is refused. Its DNS records are on its page under Tenants. Needs the settings permission.

Request
{
  "domain": "mailo.city"
}
Responses
  • 204 Added
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/support/tickets

Find tickets

ParameterInType
queuequerystring
statusquerystring
assigneequerystring
qquerystringPart of the subject or requester, or a number.
beforequeryintegerPage by ticket number.
limitqueryinteger
Responses
  • 200 Newest first.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/support/tickets

Open a ticket with a customer

The first message is from staff; the customer is told (unless notify is false) and the ticket waits on them.

Responses
  • 201 The ticket.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/support/tickets/{ticketId}

A ticket with every message, note, contact and follow-up

ParameterInType
ticketId *pathstring
Responses
  • 200 The ticket.
    {
      "contacts": [],
      "followUps": [],
      "messages": [
        {
          "authorKind": "customer",
          "body": "...",
          "internal": false
        }
      ],
      "ticket": {
        "number": 41,
        "status": "open",
        "subject": "Spam from your IP"
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PATCH /v1/platform/support/tickets/{ticketId}

Change status, priority, assignee or queue

ParameterInType
ticketId *pathstring
Responses
  • 200 The ticket.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/support/tickets/{ticketId}/contacts

Add someone to the replies

Where the queue allows contacts; at most 20.

ParameterInType
ticketId *pathstring
Responses
  • 200 The contacts.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 409 It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
  • 422 A value is out of range or names something that does not exist.
DELETE /v1/platform/support/tickets/{ticketId}/contacts/{email}

Take someone off the replies

ParameterInType
ticketId *pathstring
email *pathstring
Responses
  • 204 Removed.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/support/tickets/{ticketId}/follow-ups

Schedule a follow-up

A reminder shows in the due list at dueAt; a reply is sent at dueAt as its author, like any reply.

ParameterInType
ticketId *pathstring
Request
{
  "body": "Check the customer's DNS is fixed",
  "dueAt": "2026-10-15T09:00:00Z",
  "kind": "reminder"
}
Responses
  • 201 The follow-up.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.
PATCH /v1/platform/support/tickets/{ticketId}/follow-ups/{followupId}

Mark a follow-up done, or not

ParameterInType
ticketId *pathstring
followupId *pathinteger
Responses
  • 204 Changed.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
POST /v1/platform/support/tickets/{ticketId}/messages

Reply, or add an internal note

A reply tells the requester and contacts the way the queue says, and sets the ticket pending unless status says otherwise. An internal note is seen by staff only; staff named in mentions (their email addresses, as the panel's @ picker sends them) are emailed that they were mentioned. Addresses that are not staff are ignored.

ParameterInType
ticketId *pathstring
Responses
  • 201 The message.
  • 400 The body is not JSON with only the documented fields, or a query parameter is out of range.
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
  • 404 No such object.
  • 422 A value is out of range or names something that does not exist.