Zappocity platform API

version 1.33.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/coupons

Coupons, newest first

ParameterInType
qquerystringCodes containing this.
Responses
  • 200 The coupons.
    {
      "items": [
        {
          "active": true,
          "amountOffCents": null,
          "boosts": {},
          "buyCount": 1,
          "code": "WELCOME20",
          "createdAt": "2026-10-09T12:00:00Z",
          "createdBy": "owner@zappocity.test",
          "description": "20% off your first three months",
          "duration": "months",
          "durationMonths": 3,
          "expiresAt": "2027-01-01T00:00:00Z",
          "firstOrder": true,
          "freeCount": 1,
          "id": "6f1c2a9e-1b0e-4f3a-9a52-2f4d1e0c7b11",
          "kind": "percent",
          "maxRedemptions": null,
          "percentOff": 20,
          "periods": [
            "month"
          ],
          "plans": [],
          "product": "mailocity",
          "redemptions": 12,
          "signedUpAfter": null,
          "signedUpBefore": null,
          "startsAt": null,
          "updatedAt": "2026-10-09T12: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.
POST /v1/platform/coupons

Make a coupon

kind is percent (percentOff off each charge it covers), fixed (amountOffCents off an invoice's charges it covers), bogo (of every buyCount + freeCount subscriptions billed together, the cheapest freeCount are free, or percentOff off) or boost (raises the product's limits by name, by the amounts in boosts, such as {"users": 5, "storageBytes": 10737418240}; the names are the plan limits in GET /v1/platform/products). product, plans and periods limit which subscriptions it covers (none: all). duration is once (the next invoice it takes something off), months (durationMonths from redeeming) or forever; a boost lasts months or forever. startsAt and expiresAt are when it can be redeemed; maxRedemptions caps how many organizations can redeem it (1 for single use; leave it out for unlimited); firstOrder is for organizations never invoiced before, and applies only to their first invoice; signedUpAfter and signedUpBefore limit it to organizations made in that window. A code left out is made up (12 characters, for single-use codes). Codes are upper case.

Responses
  • 201 The coupon.
  • 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 A coupon with this code already exists.
  • 422 A value is out of range or names something that does not exist.
GET /v1/platform/coupons/{couponId}

A coupon

ParameterInType
couponId *pathstring
Responses
  • 200 The coupon.
  • 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/coupons/{couponId}

Switch a coupon off or on, or change its end or cap

What a coupon takes off cannot change once it is out; make a new one for other terms. Switching it off stops new redemptions; organizations that have it keep it (remove it from one with DELETE /v1/platform/orgs/{orgId}/coupons/{redemptionId}). null removes the end date or the cap.

ParameterInType
couponId *pathstring
Request
{
  "active": false
}
Responses
  • 200 The coupon.
  • 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/coupons/{couponId}

Delete a coupon nobody has redeemed

ParameterInType
couponId *pathstring
Responses
  • 204 Deleted.
  • 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 Someone has redeemed it; switch it off instead.
GET /v1/platform/coupons/{couponId}/redemptions

Who redeemed a coupon

ParameterInType
couponId *pathstring
Responses
  • 200 The redemptions, newest first.
    {
      "items": [
        {
          "active": true,
          "by": "pat@acme.test",
          "coupon": {
            "code": "WELCOME20"
          },
          "discountCents": 200,
          "endReason": null,
          "endedAt": null,
          "endsAt": "2027-01-09T12:00:00Z",
          "id": "0b8e5f43-2d6c-4c55-8f1e-7c9a3e2d1b00",
          "invoices": 1,
          "invoicesLeft": null,
          "orgId": "3c1d9a7e-55b2-4b8e-9e1f-0a2b3c4d5e6f",
          "orgName": "Acme",
          "redeemedAt": "2026-10-09T12: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.
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/loyalty

The loyalty and referral program's rules

Responses
  • 200 The rules.
    {
      "enabled": true,
      "inviteCostPoints": 100,
      "inviteUsedPoints": 20,
      "minRedeemPoints": 500,
      "pointValue": 100,
      "pointsPerDollar": 1,
      "referralPoints": 500,
      "signupPoints": 0,
      "updatedAt": "2026-10-09T12:00:00Z",
      "welcomePoints": 200
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
PATCH /v1/platform/loyalty

Change the rules

Points belong to people. They are earned for money paid on invoices (pointsPerDollar per whole dollar, to the organization's first owner); when someone a person invited (with their invitation link) has an organization that pays for the first time (referralPoints to the inviter, welcomePoints to the person invited, once each); on confirming a new account (signupPoints); and when someone signs up to Mailocity with an invitation the person's mailbox gave (inviteUsedPoints). They are spent on credit for an organization the person pays for (pointValue is a point's worth in hundredths of a cent: 100 is 1 cent; at least minRedeemPoints at a time) or on more Mailocity invitations (inviteCostPoints each; 0: not offered). inviteUsedPoints must be less than inviteCostPoints when both are set. Nothing is earned or spent while enabled is false.

Request
{
  "enabled": true,
  "inviteCostPoints": 100,
  "referralPoints": 1000
}
Responses
  • 200 The rules.
  • 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}/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}/coupons

An organization's coupons

Every coupon it redeemed, newest first. active says it is still in force; endReason is used (a coupon for once, on an invoice), expired or removed. invoices and discountCents are what it has taken off so far.

ParameterInType
orgId *pathstring
Responses
  • 200 The redemptions.
  • 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}/coupons

Redeem a code for an organization

On the same terms as when its people redeem it on the account page. A boost applies at once.

ParameterInType
orgId *pathstring
Request
{
  "code": "WELCOME20"
}
Responses
  • 201 The redemption.
  • 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 organization or code.
  • 409 Refused, with the reason: switched off, not started, expired, used up, already used by the organization, first order only, not for organizations made then, or the organization has 10 coupons in force.
DELETE /v1/platform/orgs/{orgId}/coupons/{redemptionId}

Remove a coupon from an organization

It takes nothing more off, and its boosts go at once. What it took off already stays on those invoices.

ParameterInType
orgId *pathstring
redemptionId *pathstring
Responses
  • 200 The ended redemption.
  • 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 has already ended.
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.
GET /v1/platform/people/{personId}/points

A person's points and their history

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

ParameterInType
personId *pathstring
beforequeryinteger
limitqueryinteger
Responses
  • 200 The balance and entries.
    {
      "items": [
        {
          "at": "2026-10-09T12:00:00Z",
          "balance": 520,
          "by": "Zappocity loyalty",
          "description": "Referral: bob@example.com became a customer",
          "id": 7,
          "kind": "referral",
          "points": 500,
          "reference": "3c1d9a7e-55b2-4b8e-9e1f-0a2b3c4d5e6f"
        }
      ],
      "points": 520
    }
  • 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/people/{personId}/points

Give or take points by hand

ParameterInType
personId *pathstring
Request
{
  "description": "Thanks for the bug report",
  "points": 250
}
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.
  • 409 It would take more points than the person has.
  • 422 A value is out of range or names something that does not exist.

DNS

GET /v1/platform/dns/accounts/{accountId}/zones

A DNS account's zones

The zones dnso.city serves for the account, with their serials and record counts, and the account itself.

ParameterInType
accountId *pathstring
Responses
  • 200 The zones.
    {
      "account": {
        "id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6a7b",
        "name": "Acme",
        "plan": "personal",
        "status": "active"
      },
      "items": [
        {
          "id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
          "name": "example.com",
          "records": 2,
          "serial": 2026100901,
          "status": "active"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such DNS account
POST /v1/platform/dns/accounts/{accountId}/zones

Serve a zone

Adds a domain for dnso.city to serve, within the plan's zones. A domain is served for one account only (409 when it is already). Point the domain's name servers at dnso.city's at its registrar for the answers to be used.

ParameterInType
accountId *pathstring
Responses
  • 201 The zone.
  • 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 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account
  • 409 The zone is served already, or the plan's limit is reached.
  • 422 The record or zone is not valid
GET /v1/platform/dns/accounts/{accountId}/zones/{zoneId}

A zone and its records

With its DNSSEC: every zone is signed with a key of its own; dnssec.ds is the DS record to give the domain's registrar to turn DNSSEC on. With its secondary DNS: transfers lists the addresses it may be transferred to and its TSIG key (never the secret). A secondary zone has kind secondary, its records as last transferred (read only), and secondary: its primaries, key name, serial and the state of its checks.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 The zone and its records.
    {
      "dnssec": {
        "algorithm": 13,
        "ds": "24680 13 2 9A1B...",
        "dsRecord": "example.com.\t3600\tIN\tDS\t24680 13 2 9A1B...",
        "keyTag": 24680,
        "ready": true
      },
      "records": [
        {
          "data": "10 mail.example.com.",
          "id": "3c4d5e6f-7081-4293-a4b5-c6d7e8f90123",
          "name": "@",
          "ttl": 3600,
          "type": "MX"
        }
      ],
      "transfers": {
        "addresses": [],
        "key": null
      },
      "zone": {
        "accountId": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6a7b",
        "id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
        "name": "example.com",
        "records": 2,
        "serial": 2026100901,
        "status": "active"
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such DNS account
DELETE /v1/platform/dns/accounts/{accountId}/zones/{zoneId}

Stop serving a zone

Its records go with it.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 204 Removed.
  • 401 No valid platform API key or admin panel session.
  • 403 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account
  • 409 The zone is served already, or the plan's limit is reached.
  • 422 The record or zone is not valid
GET /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/delegation

Check the zone's delegation

Asks the zone's parent (the registry, for most domains) directly, not a resolver's cache: status is delegated (dnso.city's name servers and no others), partly (some of them, or others too: usual for a secondary zone), elsewhere (none of them), unregistered (the parent has no such name) or unknown (the parent could not be asked; error says why). ds is on (the parent's DS record is the zone's: DNSSEC works), mismatch (DS records, none the zone's: resolvers that check DNSSEC fail; replace it at the registrar), off or unknown. Kept a minute.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 The parent's answer.
    {
      "delegation": {
        "checkedAt": "2026-10-09T12:00:00Z",
        "ds": "off",
        "missing": [
          "ns2.dnso.city"
        ],
        "nameServers": [
          "ns1.dnso.city",
          "ns1.oldhost.net"
        ],
        "parent": "com",
        "status": "partly"
      },
      "expected": [
        "ns1.dnso.city",
        "ns2.dnso.city"
      ],
      "zone": "example.com"
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such DNS account or zone here.
  • 503 Delegation is not checked by this server.
PUT /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/primaries

Set a secondary zone's primaries

The name servers a secondary zone is transferred from (IP addresses on the internet, 1 to 10, asked on port 53), and, when key is given, the TSIG key they want (null: none; left out: unchanged). The zone is checked with them within a minute. Answers the zone as GET does.

ParameterInType
accountId *pathstring
zoneId *pathstring
Request
{
  "addresses": [
    "192.0.2.53"
  ],
  "key": {
    "algorithm": "hmac-sha256",
    "name": "transfer.example.org",
    "secret": "c2VjcmV0LXNlY3JldC1zZWNyZXQtc2VjcmV0LTAxMjM="
  }
}
Responses
  • 200 The zone
  • 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 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account or zone here.
  • 409 The zone is not a secondary.
  • 422 An address or the key is not valid.
POST /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/records

Add a record

Checked as DNS reads it, and with the zone's other records: a name with a CNAME has nothing else, the zone itself is never a CNAME (use ALIAS), its own NS records are dnso.city's. The serial goes up. Answers the zone and its records.

ParameterInType
accountId *pathstring
zoneId *pathstring
Request
{
  "data": "10 mail.example.com.",
  "name": "@",
  "ttl": 3600,
  "type": "MX"
}
Responses
  • 201 The zone and its records.
    {
      "dnssec": {
        "algorithm": 13,
        "ds": "24680 13 2 9A1B...",
        "dsRecord": "example.com.\t3600\tIN\tDS\t24680 13 2 9A1B...",
        "keyTag": 24680,
        "ready": true
      },
      "records": [
        {
          "data": "10 mail.example.com.",
          "id": "3c4d5e6f-7081-4293-a4b5-c6d7e8f90123",
          "name": "@",
          "ttl": 3600,
          "type": "MX"
        }
      ],
      "zone": {
        "accountId": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6a7b",
        "id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
        "name": "example.com",
        "records": 2,
        "serial": 2026100901,
        "status": "active"
      }
    }
  • 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 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account
  • 409 The zone is served already, or the plan's limit is reached.
  • 422 The record or zone is not valid
PUT /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/records/{recordId}

Replace a record

ParameterInType
accountId *pathstring
zoneId *pathstring
recordId *pathstring
Request
{
  "data": "10 mail.example.com.",
  "name": "@",
  "ttl": 3600,
  "type": "MX"
}
Responses
  • 200 The zone and its records.
  • 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 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account
  • 409 The zone is served already, or the plan's limit is reached.
  • 422 The record or zone is not valid
DELETE /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/records/{recordId}

Remove a record

ParameterInType
accountId *pathstring
zoneId *pathstring
recordId *pathstring
Responses
  • 200 The zone and its records.
  • 401 No valid platform API key or admin panel session.
  • 403 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account
  • 409 The zone is served already, or the plan's limit is reached.
  • 422 The record or zone is not valid
POST /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/refresh

Check a secondary zone now

Asks the primaries for the zone's SOA and transfers the zone if its serial moved. What happened is in secondary (lastError empty when it went well). Otherwise this happens on the schedule the zone's SOA sets (refresh, and retry after a failure) and when a primary sends NOTIFY to dnso.city's name servers.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 The zone, its records and its secondary state.
    {
      "dnssec": {
        "ready": false,
        "secondary": true
      },
      "records": [
        {
          "data": "192.0.2.7",
          "name": "www",
          "ttl": 300,
          "type": "A"
        }
      ],
      "secondary": {
        "expire": 1209600,
        "keyName": "transfer.example.org.",
        "lastCheck": "2026-10-09T12:00:00Z",
        "lastError": "",
        "lastOk": "2026-10-09T12:00:00Z",
        "lastTransfer": "2026-10-09T12:00:00Z",
        "nextCheck": "2026-10-09T13:00:00Z",
        "primaries": [
          "192.0.2.53"
        ],
        "primarySerial": 7,
        "records": 3,
        "refresh": 3600,
        "retry": 600
      },
      "transfers": {
        "addresses": [],
        "key": null
      },
      "zone": {
        "id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
        "kind": "secondary",
        "name": "example.org",
        "records": 3,
        "serial": 7,
        "status": "active"
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 403 Not allowed (staff need the tenants permission to change zones).
  • 404 No such DNS account or zone here.
  • 409 The zone is not a secondary.
PUT /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/transfers

Set the addresses the zone is transferred to

Secondary DNS, on plans that include it: the zone may be transferred (AXFR, or IXFR, answered with the whole zone) to these addresses, over TCP and only with a request signed with the zone's TSIG key (HMAC-SHA256); they are sent a NOTIFY when the zone changes. Addresses are on the internet (no private or loopback ones), at most 10; an empty list stops transfers. The first time, the zone gets its key, and the answer carries its secret: shown this once, so keep it. The zone goes out unsigned (dnso.city signs as it answers), so while the registrar has the zone's DS record, every name server for it must be dnso.city's. ALIAS records are not transferred. The serial goes up.

ParameterInType
accountId *pathstring
zoneId *pathstring
Request
{
  "addresses": [
    "192.0.2.53",
    "2001:db8::53"
  ]
}
Responses
  • 200 The zone's transfers; `key.secret` only when the key was just made.
    {
      "addresses": [
        "192.0.2.53",
        "2001:db8::53"
      ],
      "key": {
        "algorithm": "hmac-sha256",
        "createdAt": "2026-10-09T12:00:00Z",
        "name": "xfr.example.com.",
        "secret": "q2Lw...base64...="
      }
    }
  • 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 Not allowed (staff need the tenants permission to change zones), or the plan does not include secondary DNS.
  • 404 No such DNS account or zone here.
  • 422 An address is not valid, not on the internet, or there are more than 10.
POST /v1/platform/dns/accounts/{accountId}/zones/{zoneId}/transfers/key

Replace the zone's TSIG key

A new key for transfers; the old one stops working once the name servers next read the zone (within a minute). The new secret is in this answer only.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 The zone's transfers with the new key's secret.
    {
      "addresses": [
        "192.0.2.53"
      ],
      "key": {
        "algorithm": "hmac-sha256",
        "createdAt": "2026-10-09T12:00:00Z",
        "name": "xfr.example.com.",
        "secret": "b7Rk...base64...="
      }
    }
  • 401 No valid platform API key or admin panel session.
  • 403 Not allowed (staff need the tenants permission to change zones), or the plan does not include secondary DNS.
  • 404 No such DNS account or zone here.
GET /v1/platform/dns/settings

dnso.city's name servers

The name servers every served zone names in its NS records (the first also in its SOA), and the SOA's contact. dnsod reads them with the zones.

Responses
  • 200 The settings.
    {
      "hostmaster": "hostmaster@dnso.city",
      "nameServers": [
        "ns1.dnso.city",
        "ns2.dnso.city"
      ],
      "updatedAt": "2026-10-09T12:00:00Z"
    }
  • 401 No valid platform API key or admin panel session.
PATCH /v1/platform/dns/settings

Set dnso.city's name servers

Needs the settings permission. Up to 13 host names; the hostmaster is an email address (empty for hostmaster@ the first name server's domain).

Request
{
  "hostmaster": "hostmaster@dnso.city",
  "nameServers": [
    "ns1.dnso.city",
    "ns2.dnso.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.
  • 422 A value is out of range or names something that does not exist.

Jobs

GET /v1/platform/jobs

The platform's scheduled jobs

When the platform runs apart from the mail server (zappocityd): its own recurring work (help desk mail, billing, Zappocity account deletions, the sign-in clean-up), in the same form as the admin API's /v1/jobs. In one process the list is empty, as /v1/jobs has every job. See docs/kb/scheduled-jobs.md.

Responses
  • 200 The jobs.
    {
      "items": [
        {
          "defaultEverySeconds": 3600,
          "description": "Invoices subscriptions that are due, charges saved payment methods and sends the invoices.",
          "enabled": true,
          "everySeconds": 3600,
          "failures": 0,
          "lastDurationMs": 2012,
          "lastError": "",
          "lastFinishedAt": "2026-10-09T17:00:02Z",
          "lastOk": true,
          "lastResult": "3 invoices made",
          "lastStartedAt": "2026-10-09T17:00:00Z",
          "lastTrigger": "schedule",
          "name": "billing",
          "nextAt": "2026-10-09T18:00:00Z",
          "running": false,
          "runs": 40,
          "title": "Billing"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
PATCH /v1/platform/jobs/{name}

Pause, resume or reschedule a platform job

A paused job still runs when asked to run now. Needs the settings permission.

ParameterInType
name *pathstring
Request
{
  "enabled": false
}
Responses
  • 200 The job.
  • 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/jobs/{name}/run

Run a platform job now

It starts within moments on zappocityd (202); its outcome shows in the list. Its next scheduled run counts from this one. Needs the settings permission.

ParameterInType
name *pathstring
Responses
  • 200 Run here (no scheduler running); its outcome.
    {
      "durationMs": 41,
      "error": "",
      "finished": true,
      "ok": true,
      "result": "0 invoices made",
      "started": true
    }
  • 202 Started.
  • 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 is running now.

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. internalAvailable is false when Mailocity runs as its own service (CORPSUITE_MAILOCITY_API_URL), so mode internal is refused.
    {
      "apiUrl": "https://mailo.city",
      "fromName": "Zappocity",
      "hasApiToken": true,
      "hasSmtpPassword": false,
      "internalAvailable": true,
      "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), admin (runs the Mailocity team), billing (invoices and payment), support (tickets), member or guest: the same six roles as Mailocity mailboxes. The last owner cannot be demoted (409). A member with a mailbox in one of the organization's Mailocity teams has that mailbox's role: a role set here reaches all their mailboxes in its teams within a minute (the account sync), and one changed on a mailbox later comes back here.

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), nor a member with a mailbox in one of the organization's Mailocity teams (409, naming the mailbox): delete or unlink the mailbox in Mailocity first. A member the account sync added for their mailbox is removed by it once the mailbox is gone.

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.
POST /v1/platform/orgs/{orgId}/merge

Merge an organization recorded twice into this one

For one company with two organizations. The other's members (the stronger role kept where a person is in both), subscriptions, invoices and ledger (so its credit), coupons (one of each), help desks, DNS accounts, tickets and Mailocity teams move to this one. Its balance moves by an adjustment entry on each ledger (ledger entries are never changed); it is kept as status merged with mergedInto, out of lists. This one's billing profile and saved payment method are kept. Cannot be undone; recorded in the audit log with what moved. Merging again into the same organization only repeats the teams' move (to finish one that answered 502). Needs the billing permission.

ParameterInType
orgId *pathstringThe organization to keep.
Request
{
  "from": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"
}
Responses
  • 200 The organization kept, and how many of each thing moved.
    {
      "moved": {
        "invoices": 4,
        "members": 2,
        "subscriptions": 1
      },
      "org": {
        "id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6a7b",
        "name": "Acme"
      }
    }
  • 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 One of them is merged into another organization already.
  • 422 from is missing or the same organization.
  • 502 Merged, but the teams could not be moved; merge again to finish.

People

GET /v1/platform/badges

Badges, with the icons and colours they can have

Responses
  • 200 The badges.
    {
      "colors": [
        "primary",
        "success"
      ],
      "icons": [
        "star",
        "user",
        "rocket"
      ],
      "items": [
        {
          "active": true,
          "color": "primary",
          "description": "Works at Zappocity",
          "holders": 0,
          "icon": "user",
          "id": "6f1c2a9e-1b0e-4f3a-9a52-2f4d1e0c7b11",
          "joinedBefore": null,
          "key": "staff",
          "name": "Staff",
          "plan": null,
          "product": null,
          "rule": "staff",
          "sortOrder": 20
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 403 The staff account lacks the permission, or the caller is a team.
POST /v1/platform/badges

Make a badge

rule says who has it: manual (given by hand), early_adopter (joined before joinedBefore, or ever subscribed to product's plan; either is enough), staff (active panel staff) or administrator (staff with every permission). Staff and administrator badges show on proven mail even when the person's profile is hidden.

Request
{
  "color": "warning",
  "description": "Reported a security bug",
  "icon": "bolt",
  "key": "bug-hunter",
  "name": "Bug hunter"
}
Responses
  • 201 The badge.
  • 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 A badge with this key exists.
  • 422 A value is out of range or names something that does not exist.
PATCH /v1/platform/badges/{badgeId}

Change a badge

Everything but key and rule. joinedBefore null removes the date; plan "" removes the plan.

ParameterInType
badgeId *pathstring
Request
{
  "active": true,
  "joinedBefore": "2027-06-01"
}
Responses
  • 200 The badge.
  • 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/badges/{badgeId}

Delete a badge (and take it from everyone)

ParameterInType
badgeId *pathstring
Responses
  • 204 Deleted.
  • 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/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.
DELETE /v1/platform/people/{personId}

Delete a Zappocity account

Schedules the person's erasure after the platform's deletionGraceDays (now=true: at once). Erasing removes their email, name, password, profile, two-step sign-in, security keys, sessions, sign-in history, memberships, badges and invitations; the row stays as status deleted with a placeholder address, because billing, points and audit records name it. mailboxes=true also deletes the Mailocity mailboxes signed in to with it (otherwise they are unlinked and stay). Refused while the person is the only owner of an organization with other members or products.

ParameterInType
personId *pathstring
nowqueryboolean
mailboxesqueryboolean
Responses
  • 202 Scheduled; the person
  • 204 Erased.
  • 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, with the reason: the only owner of an organization, or a mailbox under a legal hold or its team's last owner.
PUT /v1/platform/people/{personId}/badges/{badgeId}

Give a badge by hand

ParameterInType
personId *pathstring
badgeId *pathstring
Responses
  • 204 Given (or already had).
  • 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 badge is given by its rule
DELETE /v1/platform/people/{personId}/badges/{badgeId}

Take a badge given by hand

ParameterInType
personId *pathstring
badgeId *pathstring
Responses
  • 204 Taken.
  • 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/people/{personId}/merge

Merge a person recorded twice into this one

For one human with two Zappocity accounts. The other's memberships (the stronger role kept where both belong), help desk seats, tickets, points (by an adjustment on each side), badges, invitations, the people it invited, its sign-in history and the mailboxes linked to it move to this one. The other is signed out, stripped as an erased account is (its email address is free again) and kept as status merged with mergedInto, so its points history stays; it is found by its customer number only. This one keeps its email, password and two-step sign-in. Neither may be merged, deleted or scheduled for deletion. Cannot be undone; recorded in the audit log with what moved. Merging again into the same person only repeats the mailboxes' move (to finish one that answered 502). Needs the tenants permission.

ParameterInType
personId *pathstringThe person to keep.
Request
{
  "from": "3f1c2b4a-5d6e-4f70-8a91-b2c3d4e5f607"
}
Responses
  • 200 The person kept, and how many of each thing moved.
    {
      "moved": {
        "memberships": 1,
        "points": 2,
        "tickets": 3
      },
      "person": {
        "email": "amy@example.com",
        "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f"
      }
    }
  • 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 One of them is merged, deleted or to be deleted; or the other is linked to a mailbox of a team this person is not on (no mailbox there, not in the team's organization): unlink it first.
  • 422 from is missing or the same person.
  • 502 Merged, but the mailboxes could not be moved; merge again to finish.
GET /v1/platform/people/{personId}/profile

A person's profile and badges

ParameterInType
personId *pathstring
Responses
  • 200 The profile and the badges they have.
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PATCH /v1/platform/people/{personId}/profile

Correct or hide a profile

The same fields as the person's own; visible false hides it.

ParameterInType
personId *pathstring
Request
{
  "bio": "",
  "visible": false
}
Responses
  • 200 The profile and badges.
  • 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 That handle is taken.
  • 422 A value is out of range or names something that does not exist.
POST /v1/platform/people/{personId}/restore

Cancel a Zappocity account's deletion

Also cancels the deletion of the mailboxes scheduled with it.

ParameterInType
personId *pathstring
Responses
  • 200 The person.
  • 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.

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",
              "perSeat": false,
              "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. retired: true retires the plan: no new subscription can take it, and customers already on it keep it (and its price) for their renewals; retiredNote says why. Left out, a retired plan stays retired. The catalog shows retired, retiredNote and customers (how many are on each plan).

ParameterInType
product *pathstring
plan *pathstring
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. With perSeat true the prices are per seat: invoices charge them times a subscription's seats.

ParameterInType
product *pathstring
plan *pathstring
Request
{
  "month": 1900,
  "perSeat": true,
  "year": 19000
}
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",
      "seats": 1,
      "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, period or seats, 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. On a plan priced per seat, more seats are invoiced at once for the rest of the period already billed (in proportion to the time left); fewer seats take effect without a refund.

ParameterInType
subscriptionId *pathstring
Request
{
  "seats": 5
}
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.
GET /v1/platform/support/help/articles

The help center's articles

Drafts and staff-only articles too. q searches titles and text (the best matches first, with a snippet).

ParameterInType
qquerystring
categoryquerystringA category's slug.
kindquerystring
Responses
  • 200 The articles.
    {
      "items": [
        {
          "category": "billing",
          "helpful": 31,
          "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
          "kind": "faq",
          "notHelpful": 2,
          "slug": "refund",
          "status": "published",
          "title": "How do I get a refund?",
          "views": 120,
          "visibility": "public"
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
POST /v1/platform/support/help/articles

Write an article or FAQ

Markdown; links may go to http(s), mail, this site's pages and anchors. visibility: public (the help center), customers (signed-in customers only) or staff. Needs the support permission.

Request
{
  "body": "Open a request under **Billing** within 30 days.",
  "category": "billing",
  "kind": "faq",
  "slug": "refund",
  "status": "published",
  "title": "How do I get a refund?",
  "visibility": "public"
}
Responses
  • 201 The article.
  • 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/help/articles/{articleId}

An article, with its body rendered

ParameterInType
articleId *pathstring
Responses
  • 200 The article and its HTML: { article, html }.
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
PUT /v1/platform/support/help/articles/{articleId}

Change an article

Replaces every field.

ParameterInType
articleId *pathstring
Request
{
  "slug": "refund",
  "status": "draft",
  "title": "How do I get a refund?"
}
Responses
  • 200 The article.
  • 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/support/help/articles/{articleId}

Delete an article

ParameterInType
articleId *pathstring
Responses
  • 204 Deleted.
  • 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/support/help/categories

The help center's categories

The knowledge base staff write for the help center (/help) and the account page: articles and FAQs, for everyone, for signed-in customers, or for staff. articles counts each category's published ones. See docs/kb/help-center.md.

Responses
  • 200 The categories.
    {
      "items": [
        {
          "articles": 6,
          "description": "Invoices, cards and refunds",
          "id": "2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f",
          "name": "Billing",
          "slug": "billing",
          "sortOrder": 0
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
POST /v1/platform/support/help/categories

Add a category

Request
{
  "description": "Invoices, cards and refunds",
  "name": "Billing",
  "slug": "billing"
}
Responses
  • 201 The category.
  • 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.
PUT /v1/platform/support/help/categories/{categoryId}

Change a category

ParameterInType
categoryId *pathstring
Request
{
  "name": "Billing and invoices",
  "slug": "billing",
  "sortOrder": 1
}
Responses
  • 200 The category.
  • 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/support/help/categories/{categoryId}

Delete a category

Its articles stay, without a category.

ParameterInType
categoryId *pathstring
Responses
  • 204 Deleted.
  • 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/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/overview

The help desk's front page

Each queue the caller is an agent of (every queue for staff with every permission, the platform API key and the command line), with counts of its active tickets (open, pending, and by priority) and up to 50 of them, the most urgent first, then the longest waiting.

Responses
  • 200 The queues.
    {
      "items": [
        {
          "counts": {
            "normal": 3,
            "open": 3,
            "pending": 1,
            "urgent": 1
          },
          "queue": {
            "active": true,
            "id": "3f1c2d3e-1111-4a2b-9c3d-5e6f7a8b9c0d",
            "name": "Billing",
            "slug": "billing",
            "ticketPrefix": "BIL"
          },
          "tickets": [
            {
              "customerNumber": 17,
              "number": 1042,
              "priority": "urgent",
              "requesterEmail": "amy@example.com",
              "status": "open",
              "subject": "Charged twice",
              "tag": "BIL-1042"
            }
          ]
        }
      ]
    }
  • 401 No valid platform API key or admin panel session.
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",
          "ticketPrefix": "ABU",
          "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/replies

List canned replies and macros

Most used first. With queue, only that queue's own replies and those for every queue. placeholders lists the names a body may use.

ParameterInType
queuequerystringA queue's slug.
Responses
  • 200 The replies.
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
POST /v1/platform/support/replies

Save a canned reply or macro

A canned reply is text with placeholders, {{customer.name}} or {{customer.name|there}} with a fallback for an empty value: customer.name, customer.firstName, customer.email, ticket.number, ticket.tag (ZC-123), ticket.subject, queue.name, agent.name (from the staff address: jane.doe@ is Jane Doe) and agent.email. An unknown placeholder is refused. Actions make it a macro: using it also sets the ticket's status, priority, queue or assignee ("me" is whoever uses it, "" unassigns). A macro may have no body, and then only takes its actions. Needs the support permission.

Request
{
  "body": "Hi {{customer.firstName|there}},\n\nThat is fixed now. Reply to {{ticket.tag}} if it comes back.\n\n{{agent.name}}",
  "name": "Fixed, closing",
  "queue": "billing",
  "setAssignee": "me",
  "setStatus": "resolved"
}
Responses
  • 201 Saved.
  • 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.
PUT /v1/platform/support/replies/{replyId}

Change a canned reply or macro

Replaces every field; actions left out are removed. Needs the support permission.

ParameterInType
replyId *pathstring
Request
{
  "body": "Hi {{customer.firstName|there}}, could you send us a screenshot?",
  "name": "Waiting on you",
  "setStatus": "pending"
}
Responses
  • 200 Changed.
  • 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/support/replies/{replyId}

Delete a canned reply or macro

ParameterInType
replyId *pathstring
Responses
  • 204 Deleted.
  • 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/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

Only tickets of queues the caller is an agent of.

ParameterInType
queuequerystring
statusquerystring
assigneequerystring
qquerystringA ticket number or tag (1042, #1042, BIL-1042: that ticket only), a customer number (C-17: that customer's tickets), or part of the subject, requester address or name.
orderquerystringpriority: the most urgent first, then the longest waiting.
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,
        "prefix": "ABU",
        "status": "open",
        "subject": "Spam from your IP",
        "tag": "ABU-41"
      }
    }
  • 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.
GET /v1/platform/support/tickets/{ticketId}/replies/{replyId}

Fill in a canned reply for a ticket

The body with its placeholders filled in, to edit before sending. Nothing changes.

ParameterInType
ticketId *pathstring
replyId *pathstring
Responses
  • 200 The reply and its text for this ticket.
    {
      "body": "Hi Ana,\n\nThat is fixed now. Reply to ZC-1042 if it comes back.\n\nJane Doe"
    }
  • 401 No valid platform API key or admin panel session.
  • 404 No such object.
  • 409 The reply is for another queue.
POST /v1/platform/support/tickets/{ticketId}/replies/{replyId}

Use a canned reply or macro on a ticket

Takes the macro's actions (priority, queue, assignee), then sends the body filled in, or the edited text given, as a reply to the customer or an internal note, setting the status (a reply without a status sets the ticket pending, as any reply does). A macro without a body only takes its actions. Needs the support permission.

ParameterInType
ticketId *pathstring
replyId *pathstring
Request
{}
Responses
  • 200 The ticket as it is now, and the message sent (null when there was no body).
  • 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 The reply is for another queue.
  • 422 A value is out of range or names something that does not exist.