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
/v1/platform/audit
Every change, with who made it
| Parameter | In | Type | |
|---|---|---|---|
subject | query | string | org:<id>, person:<id>, product:<key> or subscription:<id>. |
before | query | integer | Page by entry id. |
limit | query | integer |
200Newest 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" } ] }400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
Billing
/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.
200How many invoices were issued.{ "invoices": 3 }401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.
/v1/platform/invoices
Invoices of every organization
| Parameter | In | Type | |
|---|---|---|---|
status | query | string |
200Newest first400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
/v1/platform/invoices/{invoiceId}
An invoice with its lines
| Parameter | In | Type | |
|---|---|---|---|
invoiceId * | path | string |
200The 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 }401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/v1/platform/invoices/{invoiceId}/pay
Record a payment made outside the payment provider
| Parameter | In | Type | |
|---|---|---|---|
invoiceId * | path | string |
200The invoice401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409The invoice is not open.422A value is out of range or names something that does not exist.
/v1/platform/invoices/{invoiceId}/void
Cancel an open invoice
Its charge goes back on the balance, as a ledger entry.
| Parameter | In | Type | |
|---|---|---|---|
invoiceId * | path | string |
200The invoice401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409The invoice is not open.
/v1/platform/orgs/{orgId}/billing
An organization's billing and balance
A positive balance is credit in its favour; a negative one is owed.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
200The 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" }401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
{
"freeReason": "Launch partner",
"freeUntil": "2027-01-01",
"mode": "free"
}
200The profile.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/v1/platform/orgs/{orgId}/invoices
An organization's invoices
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string | |
status | query | string |
200Newest first.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string | |
before | query | integer | |
limit | query | integer |
200The 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" } ] }401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
{
"amountCents": 2500,
"description": "Sorry for the outage",
"kind": "credit",
"reference": "ZC-41"
}
201The entry.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/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.
200The settings.{ "hasSecretKey": true, "hasWebhookSecret": true, "provider": "stripe", "updatedAt": "2026-10-08T12:00:00Z" }401No valid platform API key or admin panel session.
/v1/platform/payments
Set the payment provider and its keys
{
"provider": "stripe",
"secretKey": "sk_live_xxx",
"webhookSecret": "whsec_xxx"
}
200The settings.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.422A value is out of range or names something that does not exist.
/v1/platform/payments/test
Check the key works
200Whether Stripe took the key, and why not.{ "error": "stripe: Invalid API Key provided (401 )", "ok": false }401No valid platform API key or admin panel session.
/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.
200The 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" }401No valid platform API key or admin panel session.
/v1/platform/mail
Change how Zappocity sends its own mail
{
"apiToken": "cs_mt_Yk3",
"apiUrl": "https://mailo.city",
"mode": "jmap"
}
200The settings.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.422A value is out of range or names something that does not exist.
/v1/platform/mail/test
Send a test message the configured way
200Whether it went, and the error if not.{ "ok": true }401No valid platform API key or admin panel session.422A value is out of range or names something that does not exist.
Organizations
/v1/platform/orgs
Find organizations
| Parameter | In | Type | |
|---|---|---|---|
q | query | string | Part of the name |
limit | query | integer |
200By 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" } ] }400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
/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.
{
"name": "Acme",
"owner": "amy@example.com"
}
201The organization.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.422A value is out of range or names something that does not exist.
/v1/platform/orgs/{orgId}
An organization with its members and subscriptions
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
200The organization.{ "members": [], "org": { "id": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f", "kind": "company", "members": 1, "name": "Acme", "status": "active" }, "subscriptions": [] }401No valid platform API key or admin panel session.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
{
"status": "suspended"
}
200The organization.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/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).
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string | |
personId * | path | string |
{
"role": "billing"
}
200The members.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/orgs/{orgId}/members/{personId}
Remove a member
The last owner cannot be removed (409).
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string | |
personId * | path | string |
204Removed.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
People
/v1/platform/people
Find people
| Parameter | In | Type | |
|---|---|---|---|
q | query | string | Part of an email or name. |
limit | query | integer |
200By 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" } ] }400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
/v1/platform/people
Add a person
A Zappocity account. Without a password, they can set one later.
{
"email": "amy@example.com",
"name": "Amy",
"verified": true
}
201The person.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/people/{personId}
A person and the organizations they belong to
| Parameter | In | Type | |
|---|---|---|---|
personId * | path | string |
200The 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" } }401No valid platform API key or admin panel session.404No such object.
/v1/platform/people/{personId}
Change a person's name, status, password or verification
Suspending signs them out everywhere.
| Parameter | In | Type | |
|---|---|---|---|
personId * | path | string |
{
"status": "suspended"
}
200The person.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
Products
/v1/platform/products
The product catalog
Every product, with its plans (from the product's module) and their prices per period.
200The 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" } ] }401No valid platform API key or admin panel session.
/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.
| Parameter | In | Type | |
|---|---|---|---|
product * | path | string |
{
"siteHost": "mailo.city"
}
200The product.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/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.
| Parameter | In | Type | |
|---|---|---|---|
product * | path | string |
200The features.{ "items": [ { "description": "Read and send mail in the browser at /mail", "enforced": true, "key": "webmail", "name": "Webmail" } ] }401No valid platform API key or admin panel session.404No such object.409This product's plans are not managed here.
/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); 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.
| Parameter | In | Type | |
|---|---|---|---|
product * | path | string | |
plan * | path | string |
{
"active": true,
"features": [
"webmail",
"imap",
"smtp_submission",
"team_admin",
"archiving"
],
"limits": {
"storageBytes": 53687091200,
"users": 50
},
"name": "Team"
}
200The plan401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409Refused (an unknown limit or feature
/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.
| Parameter | In | Type | |
|---|---|---|---|
product * | path | string | |
plan * | path | string |
{
"month": 1200,
"year": 12000
}
200The whole catalog.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
Single sign-on
/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.
200The 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" } ] }401No valid platform API key or admin panel session.
/v1/platform/sso-clients
Register an application
The secret is in the answer once.
{
"id": "xiht",
"name": "xi.ht",
"redirectUris": [
"https://xi.ht/auth/callback"
]
}
201The client and its secret.{ "client": { "id": "xiht", "name": "xi.ht" }, "secret": "cs_cs_Yk3" }400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/sso-clients/{clientId}
Rename, change return addresses, or disable
| Parameter | In | Type | |
|---|---|---|---|
clientId * | path | string |
200The client.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/v1/platform/sso-clients/{clientId}
Remove an application
| Parameter | In | Type | |
|---|---|---|---|
clientId * | path | string |
204Removed.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
clientId * | path | string |
200The client and its new secret.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
Subscriptions
/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.
| Parameter | In | Type | |
|---|---|---|---|
orgId * | path | string |
{
"period": "year",
"plan": "team",
"product": "mailocity"
}
201The 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" }400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/subscriptions
Find subscriptions
| Parameter | In | Type | |
|---|---|---|---|
orgId | query | string | |
product | query | string | |
resourceRef | query | string | Which subscription a resource (a tenant id) belongs to. |
status | query | string | |
limit | query | integer |
200Newest first.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
/v1/platform/subscriptions/{subscriptionId}
A subscription and what the product reports on it
| Parameter | In | Type | |
|---|---|---|---|
subscriptionId * | path | string |
200The 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" } }401No valid platform API key or admin panel session.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
subscriptionId * | path | string |
{
"plan": "team"
}
200The subscription.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/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.
| Parameter | In | Type | |
|---|---|---|---|
subscriptionId * | path | string |
200The features.{ "effective": [ "webmail", "smtp_submission", "archiving" ], "off": [ "imap" ], "on": [ "archiving" ], "plan": [ "webmail", "imap", "smtp_submission" ] }401No valid platform API key or admin panel session.404No such object.409Nothing provisioned
/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.
| Parameter | In | Type | |
|---|---|---|---|
subscriptionId * | path | string |
{
"off": [
"imap"
],
"on": [
"archiving"
]
}
200The features after the change.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409Refused (an unknown feature
Support
/v1/platform/support/follow-ups
Reminders that are due
200Oldest first.401No valid platform API key or admin panel session.
/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).
204Done.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.
/v1/platform/support/queues
The support queues and their settings
200The 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 } ] }401No valid platform API key or admin panel session.
/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.
{
"customerClose": false,
"emailIntake": true,
"intakeFrom": "anyone",
"name": "Abuse",
"slug": "abuse"
}
201The queue.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/support/queues/{queueId}
Change a queue's settings
The slug (its address) cannot change. Turn a queue off with active false.
| Parameter | In | Type | |
|---|---|---|---|
queueId * | path | string |
{
"webReplyLogin": true
}
200The queue.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/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.
| Parameter | In | Type | |
|---|---|---|---|
queueId * | path | string | |
moveTo | query | string | Slug of the queue the tickets move to. |
200Removed.{ "moved": 12 }401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/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.
| Parameter | In | Type | |
|---|---|---|---|
queueId * | path | string | |
address * | path | string |
200The queue401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/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.
| Parameter | In | Type | |
|---|---|---|---|
queueId * | path | string | |
address * | path | string |
200The queue.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/v1/platform/support/queues/{queueId}/mailbox
Make the queue's mailbox, or give it a new token
| Parameter | In | Type | |
|---|---|---|---|
queueId * | path | string |
200The queue.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.
/v1/platform/support/settings
The support team that holds the queues' mailboxes
200The settings.{ "domain": "zappo.city", "jmapUrl": "https://mailo.city", "teamRef": "4f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d", "updatedAt": "2026-10-08T12:00:00Z" }401No valid platform API key or admin panel session.
/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.
{
"domain": "zappo.city"
}
200The settings.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/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.
{
"domain": "mailo.city"
}
204Added400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/support/tickets
Find tickets
| Parameter | In | Type | |
|---|---|---|---|
queue | query | string | |
status | query | string | |
assignee | query | string | |
q | query | string | Part of the subject or requester, or a number. |
before | query | integer | Page by ticket number. |
limit | query | integer |
200Newest first.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.
/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.
201The ticket.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.422A value is out of range or names something that does not exist.
/v1/platform/support/tickets/{ticketId}
A ticket with every message, note, contact and follow-up
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string |
200The ticket.{ "contacts": [], "followUps": [], "messages": [ { "authorKind": "customer", "body": "...", "internal": false } ], "ticket": { "number": 41, "status": "open", "subject": "Spam from your IP" } }401No valid platform API key or admin panel session.404No such object.
/v1/platform/support/tickets/{ticketId}
Change status, priority, assignee or queue
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string |
200The ticket.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/v1/platform/support/tickets/{ticketId}/contacts
Add someone to the replies
Where the queue allows contacts; at most 20.
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string |
200The contacts.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.409It already exists, the product refused the change, the resource belongs to another subscription, or an organization would lose its last owner.422A value is out of range or names something that does not exist.
/v1/platform/support/tickets/{ticketId}/contacts/{email}
Take someone off the replies
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string | |
email * | path | string |
204Removed.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string |
{
"body": "Check the customer's DNS is fixed",
"dueAt": "2026-10-15T09:00:00Z",
"kind": "reminder"
}
201The follow-up.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.
/v1/platform/support/tickets/{ticketId}/follow-ups/{followupId}
Mark a follow-up done, or not
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string | |
followupId * | path | integer |
204Changed.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.
/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.
| Parameter | In | Type | |
|---|---|---|---|
ticketId * | path | string |
201The message.400The body is not JSON with only the documented fields, or a query parameter is out of range.401No valid platform API key or admin panel session.403The staff account lacks the permission, or the caller is a team.404No such object.422A value is out of range or names something that does not exist.