Zappocity account API

version 1.63.0 OpenAPI spec (YAML)

The account page at /account (on the Zappocity host, such as zappo.city) calls these. A Zappocity account is one sign-in for every Zappocity product.

Sessions. POST /api/account/session (or a verification link) sets an HttpOnly, Secure, SameSite=Lax cookie (__Host-zappocity_account) and returns a CSRF token, which every request other than GET must send in X-CSRF-Token. Sessions last 30 days and end after 7 days unused. Bodies must be application/json.

Protection. Ten wrong passwords in a row lock an account for 15 minutes, and sign-in, sign-up and reset requests are throttled by address. The captcha (when the operator turns it on) is answered in captcha. Answers never say whether an email has an account: sign-up and reset answer the same either way, and a locked account answers like a throttled one.

See docs/kb/admin/zappocity-accounts.md.

Account

GET /api/account/delete

Whether the account is to be deleted

confirmWith is password, or email for an account without one (passkeys only).

Responses
  • 200 The deletion state.
    {
      "confirmWith": "password",
      "deleteAfter": null
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/delete

Delete my Zappocity account

After the grace period the account is erased: email, name, password, profile, two-step sign-in, security keys, sessions, sign-in history, memberships, badges and invitations go; billing, points and audit records keep a "deleted" placeholder. Until then it works as before and DELETE cancels. mailboxes true also deletes the Mailocity mailboxes signed in to with this account, on the same day; otherwise they stay and are signed in to with their own passwords. Refused while the person is the only owner of an organization that has other members or products.

Responses
  • 202 Scheduled.
    {
      "deleteAfter": "2026-10-23T12:00:00Z",
      "status": "active"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Wrong password or email
  • 409 Refused, with the reason in detail: the only owner of an organization, or a mailbox under a legal hold or its team's last owner.
DELETE /api/account/delete

Keep my Zappocity account (and the mailboxes scheduled with it)

Responses
  • 204 No longer to be deleted.
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/desks

Make a supporto.city help desk

Makes an organization the signed-in person owns, named name, subscribed to supporto.city's Free plan: its desk is ready at once with the person as its administrator, at url (its customer page; the agent app is /desk/{slug} on the same host). Needs a confirmed email. A person can make up to 3 (409 after).

Request
{
  "name": "Acme Support"
}
Responses
  • 201 The desk.
    {
      "name": "Acme Support",
      "slug": "acme-support",
      "url": "https://acme-support.supporto.city/support"
    }
  • 400 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The email is not confirmed.
  • 409 Too many desks.
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/invites

The person's invitation link and the invitations they sent

link is the person's own sign-up link (#signup&invite=CODE); people who sign up with it are recorded as invited by them (for referral rewards). joined counts them.

Responses
  • 200 The link and invitations.
    {
      "items": [
        {
          "email": "bob@example.com",
          "joined": true,
          "sentAt": "2026-10-08T12:00:00Z"
        }
      ],
      "joined": 1,
      "link": "https://zappo.city/account#signup\u0026invite=k3j9m2xp4q"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/invites

Email an invitation

Needs a confirmed address; at most 20 a day; not to someone who already has an account.

Request
{
  "email": "bob@example.com"
}
Responses
  • 204 Sent.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The person's own address is not confirmed.
  • 409 That address already has an account.
  • 422 Not an email address
  • 429 Enough invitations for today.
GET /api/account/mail

Your mailboxes and where Mailocity is

The Mailocity mailboxes linked to the signed-in person's account (a mailbox is linked when they sign in to webmail with it), never anyone else's, with the team each is in, and the addresses of webmail, team management and Mailocity sign-up. offered is false (and the rest empty) where Mailocity is not served with this account page.

Responses
  • 200 The mailboxes.
    {
      "links": {
        "manage": "https://manage.example.com/",
        "signup": "https://join.example.com/",
        "webmail": "https://mail.example.com/"
      },
      "mailboxes": [
        {
          "address": "amy@acme.example",
          "orgId": "2f1c7d2e-58a4-4f0e-9d3c-2f1f8f3f0a11",
          "team": "Acme Inc"
        }
      ],
      "offered": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/me

The account and its organizations

Owners and billing members also see each organization's subscriptions. person.number is the customer number (shown as C-17), to give when contacting support. Each organization's orgKind (since 1.27.0) is personal (one person's, made for a free or solo mailbox, with no members to invite) or company.

Responses
  • 200 The account.
    {
      "orgs": [
        {
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgKind": "company",
          "orgName": "Acme",
          "role": "owner",
          "subscriptions": [
            {
              "period": "year",
              "plan": "team",
              "product": "mailocity",
              "status": "active"
            }
          ]
        }
      ],
      "person": {
        "email": "amy@example.com",
        "emailVerifiedAt": "2026-10-08T12:00:00Z",
        "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
        "name": "Amy",
        "number": 17,
        "timeZone": "Europe/London"
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
PATCH /api/account/me

Change your name or time zone

timeZone (since 1.28.0) is an IANA zone such as Europe/London: the account page shows times in it, and Zappocity's messages to you (sign-in alerts, invitations) use it. Empty uses your device's on screen and the platform's in messages. Mailboxes have their own, set in webmail. 422 for a zone the server does not know.

Request
{
  "timeZone": "America/Toronto"
}
Responses
  • 200 The account.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 422 A value is not acceptable, such as a password under 12 characters.
POST /api/account/orgs/{orgId}/apply-credit

Pay open invoices from credit

For owners and billing members: pays the open invoices listed (or all), oldest first, from the organization's credit, as far as it goes. The balance already counts open invoices as owed, so it does not change. Returns the invoices paid.

ParameterInType
orgId *pathstring
Responses
  • 200 The invoices paid.
    {
      "items": [
        {
          "number": 7,
          "paymentMethod": "credit",
          "status": "paid"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle
GET /api/account/orgs/{orgId}/billing

An organization's balance, history and invoices

For its owners and billing members. creditNotes are the credit notes that took back all or part of its invoices' charges (kind void, refund or credit), newest first. paymentsTest is true while payments go through Stripe's test mode (test cards, no real money).

ParameterInType
orgId *pathstring
Responses
  • 200 The billing.
    {
      "billing": {
        "address": {
          "city": "Berlin",
          "country": "DE",
          "line1": "Hauptstr. 1",
          "postalCode": "10115"
        },
        "autoPay": true,
        "balanceCents": 2500,
        "free": false,
        "taxId": {
          "type": "eu_vat",
          "value": "DE123456789"
        }
      },
      "creditNotes": [
        {
          "amountCents": 2500,
          "createdAt": "2026-11-05T13:20:49Z",
          "invoiceNumber": 7,
          "kind": "credit",
          "number": 12,
          "reason": "Outage on 3 November",
          "tag": "CN-12"
        }
      ],
      "disputes": [],
      "invoices": [],
      "ledger": [],
      "org": {
        "name": "Acme"
      },
      "payments": true,
      "paymentsTest": false,
      "taxEnabled": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle.
PATCH /api/account/orgs/{orgId}/billing

Pay invoices automatically or not; the billing address and tax number

When tax is on (taxEnabled), invoices are taxed from the billing address (country required) and tax number. A tax number needs a country; Stripe's types such as eu_vat, gb_vat, au_abn.

ParameterInType
orgId *pathstring
Request
{
  "address": {
    "city": "Berlin",
    "country": "DE",
    "line1": "Hauptstr. 1",
    "postalCode": "10115"
  },
  "taxId": {
    "type": "eu_vat",
    "value": "DE123456789"
  }
}
Responses
  • 200 The billing profile.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle.
  • 422 The address or tax number is not valid.
GET /api/account/orgs/{orgId}/coupons

The organization's coupons

For its owners and billing members. Every coupon it redeemed, newest first; active says it is still in force, and invoices and discountCents are what it has taken off so far.

ParameterInType
orgId *pathstring
Responses
  • 200 The redemptions.
    {
      "items": [
        {
          "active": true,
          "by": "pat@acme.test",
          "coupon": {
            "code": "WELCOME20",
            "description": "20% off your first three months",
            "kind": "percent",
            "percentOff": 20
          },
          "discountCents": 200,
          "endReason": null,
          "endedAt": null,
          "endsAt": "2027-01-09T12:00:00Z",
          "id": "0b8e5f43-2d6c-4c55-8f1e-7c9a3e2d1b00",
          "invoices": 1,
          "invoicesLeft": null,
          "redeemedAt": "2026-10-09T12:00:00Z"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such organization
POST /api/account/orgs/{orgId}/coupons

Redeem a code

Discounts come off the next invoices it covers; a boost raises the product's limits at once. Wrong codes are limited to 10 an hour.

ParameterInType
orgId *pathstring
Request
{
  "code": "WELCOME20"
}
Responses
  • 201 The redemption.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Missing or wrong X-CSRF-Token header.
  • 404 No such code (or organization).
  • 409 Refused, with the reason in detail (expired, used up, first order only...).
  • 429 Too many wrong codes.
GET /api/account/orgs/{orgId}/credit-notes/{creditNoteId}/pdf

A credit note as a PDF file

The amount taken back from one of the organization's invoices, and why.

ParameterInType
orgId *pathstring
creditNoteId *pathstring
Responses
  • 200 The PDF file, shown in the browser (Content-Disposition inline, named after its number, such as ZI-12.pdf).
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such credit note of this organization
POST /api/account/orgs/{orgId}/funds

Add funds (buy credit ahead)

For the organization's owners and billing members, where staff offer it (the billing answer has funds with minCents, maxCents and maxBalanceCents, or null). Answers the payment provider's page to pay the amount; once it is paid the balance is credited, and it pays the next invoices.

ParameterInType
orgId *pathstring
Request
{
  "amountCents": 5000
}
Responses
  • 200 Where to go to pay.
    {
      "url": "https://checkout.stripe.com/c/pay/cs_live_xxx"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Adding funds is not offered.
  • 404 No such organization
  • 422 The amount is outside the limits
  • 503 Card payments are not set up.
GET /api/account/orgs/{orgId}/invoices/{invoiceId}

One invoice with its lines

With its credit notes (creditNotes) and how much they took back (creditedCents).

ParameterInType
orgId *pathstring
invoiceId *pathstring
Responses
  • 200 The invoice.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such invoice of this organization.
POST /api/account/orgs/{orgId}/invoices/{invoiceId}/pay

Pay an open invoice now

Answers the provider's page to pay it (any enabled method); the method used is saved for next time.

ParameterInType
orgId *pathstring
invoiceId *pathstring
Responses
  • 200 Where to go.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such invoice of this organization.
  • 409 The invoice has nothing to pay.
  • 503 Card payments are not set up.
GET /api/account/orgs/{orgId}/invoices/{invoiceId}/pdf

An invoice as a PDF file

For the organization's owners and billing members. Laid out the way Zappocity's staff set (sender, logo, colour, paper); stamped PAID, VOID or OVERDUE.

ParameterInType
orgId *pathstring
invoiceId *pathstring
Responses
  • 200 The PDF file, shown in the browser (Content-Disposition inline, named after its number, such as ZI-12.pdf).
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such invoice of this organization
POST /api/account/orgs/{orgId}/pay

Pay several invoices at once

For owners and billing members: opens the payment provider's page (url) for the open invoices listed, a line each; the browser comes back to /account#billing. They are paid when the provider confirms the total was paid; one paid meanwhile (from credit, say) leaves what was paid for it on the balance as credit.

ParameterInType
orgId *pathstring
Request
{
  "invoices": [
    "0d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6",
    "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"
  ]
}
Responses
  • 200 The payment page.
    {
      "totalCents": 14500,
      "url": "https://checkout.stripe.com/c/pay/cs_test_123"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle
  • 422 No invoices
  • 503 Payments are not set up.
POST /api/account/orgs/{orgId}/payment-method

Save a payment method

Answers the payment provider's page (url). Finished, the browser comes back to /account?org={orgId}&setup={sessionId}#billing, and the page confirms the session (payment-method/confirm); cancelled, to /account#billing. The method then pays renewals automatically.

ParameterInType
orgId *pathstring
Responses
  • 200 Where to go.
    {
      "url": "https://checkout.stripe.com/c/pay/cs_..."
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle.
  • 503 Card payments are not set up.
DELETE /api/account/orgs/{orgId}/payment-method

Remove the saved payment method

ParameterInType
orgId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle.
  • 503 Card payments are not set up.
POST /api/account/orgs/{orgId}/payment-method/confirm

Confirm a payment method saved on the provider's page

Saves the method of the setup session the browser came back with, so it shows at once even when the provider's webhook is late or never arrives. Stripe is asked about the session with the key in use. It is accepted only as a completed setup whose metadata names this organization, for the organization's customer in the mode in use, with a succeeded setup intent. After the webhook it changes nothing.

ParameterInType
orgId *pathstring
Request
{
  "sessionId": "cs_test_a1B2c3D4"
}
Responses
  • 200 Saved.
    {
      "paymentMethod": "Visa ending 4242 (test)"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose billing you handle.
  • 422 Not a finished setup of this organization (another organization's or customer's
  • 503 Card payments are not set up.
GET /api/account/orgs/{orgId}/quotes/{quoteId}/pdf

A quote sent to the organization as a PDF file (an estimate)

Only quotes sent to the organization; drafts are not found.

ParameterInType
orgId *pathstring
quoteId *pathstring
Responses
  • 200 The PDF file, shown in the browser (Content-Disposition inline, named after its number, such as ZI-12.pdf).
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such quote sent to this organization
POST /api/account/orgs/{orgId}/subscriptions

Subscribe an organization to a product's plan

For the organization's owners and billing members (403 for others; 404 for an organization the person is not in), for products bought here (Tether; 404 for others), while the product is available. One subscription per organization: a second is refused with 409 (change its plan with PATCH instead). A paid plan needs a payment method saved for the organization (or billing waived by staff), else 409 with the reason. The subscription starts now and is invoiced on the next billing run (within the hour); the product is told at once. Products send people here with /account#buy=tether&plan=tether_pro&period=year&org={orgId} (org and period optional), which opens this as a form. Since 1.26.0.

ParameterInType
orgId *pathstring
Request
{
  "period": "year",
  "plan": "tether_pro",
  "product": "tether"
}
Responses
  • 201 The subscription.
    {
      "canceledAt": null,
      "createdAt": "2026-10-26T12:00:00Z",
      "id": "6f1c0d2e-5b7a-4c1e-9a3d-2b8e4f6a1c90",
      "orgId": "0b9a7c4e-2d1f-4e8a-b6c3-5a7d9e1f2b40",
      "orgName": "Acme",
      "period": "year",
      "plan": "tether_pro",
      "product": "tether",
      "renewsAt": "2026-10-26T12:00:00Z",
      "resourceRef": "org-0b9a7c4e",
      "seats": 1,
      "status": "active",
      "unpaidSuspendedAt": null,
      "updatedAt": "2026-10-26T12:00:00Z"
    }
  • 400 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Not an owner or billing member
  • 404 No such organization
  • 409 Refused
PATCH /api/account/orgs/{orgId}/subscriptions/{subscriptionId}

Change a self-serve subscription's plan, seats or billing period

For the organization's owners and billing members (403 for others), and only for products customers change themselves (supporto.city, dnso.city, Tether; 404 for others). A dearer plan and more seats are invoiced at once for the rest of the period already paid (the difference in price, in proportion to the time left); a cheaper plan or fewer seats take effect without a refund. period (month or year) applies from the next renewal: yearly is cheaper (dnso.city's monthly prices are 20% more). A plan, seats or period the resource does not fit (more agents, queues, zones or records than allowed) or that is not offered is refused with 409 and the reason.

ParameterInType
orgId *pathstring
subscriptionId *pathstring
Responses
  • 200 The subscription.
  • 400 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Not an owner or billing member.
  • 404 No such subscription here.
  • 409 Refused
POST /api/account/password

Change your password

Needs the current one. Every other session ends, and the address is told by email.

Responses
  • 204 Changed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The current password is not right (it counts as a failed sign-in).
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/plans/{product}

A product's plans and prices

For products customers change themselves (supporto.city, dnso.city, Tether); 404 for others.

ParameterInType
product *pathstring
Responses
  • 200 Its active plans.
    {
      "currency": "USD",
      "plans": [
        {
          "active": true,
          "code": "team",
          "features": [
            "help_center",
            "sla"
          ],
          "limits": {
            "agents": -1,
            "agentsBySeats": 1,
            "queues": -1
          },
          "name": "Team",
          "perSeat": true,
          "prices": {
            "month": 2280,
            "year": 22800
          }
        }
      ]
    }
  • 404 Not a self-serve product.
GET /api/account/points

The person's points, their history and what the program gives

program says what earns points (per dollar paid, per referral that becomes a customer, and so on) and what they buy: credit at pointValue hundredths of a cent each (at least minRedeemPoints at a time), and Mailocity invitations at inviteCostPoints each when invitations is true.

Responses
  • 200 The points.
    {
      "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": ""
        }
      ],
      "points": 520,
      "program": {
        "enabled": true,
        "invitations": true,
        "inviteCostPoints": 100,
        "inviteUsedPoints": 20,
        "minRedeemPoints": 500,
        "pointValue": 100,
        "pointsPerDollar": 1,
        "referralPoints": 500,
        "welcomePoints": 200
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/points/credit

Turn points into credit for an organization

For an organization the person is an owner or billing member of. The credit is on its balance and pays its next invoices.

Request
{
  "orgId": "3c1d9a7e-55b2-4b8e-9e1f-0a2b3c4d5e6f",
  "points": 500
}
Responses
  • 201 The points entry and the credit entry.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Missing or wrong X-CSRF-Token header.
  • 404 No such organization
  • 409 Refused, with the reason in detail: the program is off, fewer than minRedeemPoints, or not enough points.
POST /api/account/points/invitations

Get more Mailocity invitations for points

Adds them to the person's Mailocity mailbox (the one they sign in to with this account); 1 to 50 at a time.

Request
{
  "count": 2
}
Responses
  • 201 The points entry and how many invitations the mailbox now has.
    {
      "entry": {
        "balance": 320,
        "kind": "invitations",
        "points": -200
      },
      "invitationsLeft": 3
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Missing or wrong X-CSRF-Token header.
  • 409 Refused, with the reason in detail: not offered, not enough points, or no Mailocity mailbox (the points are given back).
GET /api/account/profile

The person's profile and badges

Responses
  • 200 The profile and badges.
    {
      "badges": [
        {
          "color": "success",
          "icon": "star",
          "key": "early-adopter",
          "name": "Early adopter",
          "rule": "early_adopter"
        }
      ],
      "profile": {
        "bio": "Support lead",
        "createdAt": "2026-10-09T12:00:00Z",
        "handle": "pat",
        "name": "Pat Lee",
        "personId": "19adb6dd-1eac-4d74-8134-2668d30e9561",
        "pronouns": "they/them",
        "visible": true,
        "website": "https://pat.example"
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
PATCH /api/account/profile

Change the profile

handle is 3 to 30 lower-case letters, digits, '.', '_' and '-' (null removes it; some names are kept for the platform); website is an https:// address. visible lets others see it: in webmail next to mail from you, and by handle. Badges are given by staff or their rules.

Request
{
  "bio": "Support lead",
  "handle": "pat",
  "name": "Pat Lee",
  "pronouns": "they/them",
  "visible": true,
  "website": "https://pat.example"
}
Responses
  • 200 The profile and badges.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Missing or wrong X-CSRF-Token header.
  • 409 That handle is taken.
  • 422 A value is not valid.
GET /api/account/profiles/{handle}

Someone's visible profile

For people signed in; only profiles made visible (and the person's own).

ParameterInType
handle *pathstring
Responses
  • 200 The profile and the badges that show.
    {
      "badges": [
        {
          "color": "primary",
          "icon": "user",
          "key": "staff",
          "name": "Staff",
          "rule": "staff"
        }
      ],
      "bio": "Support lead",
      "handle": "pat",
      "name": "Pat Lee",
      "pronouns": "they/them",
      "visible": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such visible profile.
GET /api/account/security-alerts

Whether sign-ins from new devices are emailed

A sign-in from an address and browser not seen in the last 90 days is emailed when newSignIn is on (the default). Changes to the password, two-step sign-in and security keys are always emailed.

Responses
  • 200 The setting.
    {
      "newSignIn": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
PUT /api/account/security-alerts

Turn sign-in alerts on or off

Request
{
  "newSignIn": false
}
Responses
  • 200 The setting.
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/security-keys

The account's security keys and passkeys

Hardware keys (YubiKey and others) and passkeys (1Password, iCloud Keychain, Google Password Manager, Windows Hello) registered with WebAuthn. Any of them makes signing in take a second step; a passkey (passkey: true) can also sign in on its own.

Responses
  • 200 The keys.
    {
      "items": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "id": "q8V2...",
          "lastUsed": "2026-10-09T08:30:00Z",
          "name": "YubiKey 5C",
          "passkey": false
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/security-keys

Finish adding a key or passkey

Send the challengeId from POST /api/account/security-keys/register and the browser's navigator.credentials.create() result (as JSON). The first key turns two-step sign-in on: every other device is signed out and recovery codes are returned once. The address is told by email.

Request
{
  "challengeId": "5b0b4c1e-7d1a-4f5e-9a3b-1c2d3e4f5a6b",
  "credential": {
    "id": "q8V2...",
    "rawId": "q8V2...",
    "response": {
      "attestationObject": "o2Nm...",
      "clientDataJSON": "eyJ0..."
    },
    "type": "public-key"
  },
  "name": "YubiKey 5C"
}
Responses
  • 201 Added.
    {
      "id": "q8V2...",
      "name": "YubiKey 5C",
      "passkey": false,
      "recoveryCodes": [
        "k3j9-2mxp",
        "..."
      ]
    }
  • 400 The challenge expired or was used.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 409 The key is already registered
  • 422 The key's answer did not check out.
POST /api/account/security-keys/register

Start adding a key or passkey

Needs the current password. Returns options for navigator.credentials.create() (publicKey) and a challengeId, good for ten minutes and once. With passkey: true the authenticator must keep a discoverable credential and verify the user (PIN or biometrics), so it can sign in without a password.

Request
{
  "currentPassword": "correct horse battery",
  "passkey": true
}
Responses
  • 200 The options.
    {
      "challengeId": "5b0b4c1e-7d1a-4f5e-9a3b-1c2d3e4f5a6b",
      "options": {
        "publicKey": {
          "challenge": "Wm9...",
          "pubKeyCredParams": [
            {
              "alg": -7,
              "type": "public-key"
            }
          ],
          "rp": {
            "id": "zappo.city",
            "name": "Zappocity"
          }
        }
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Wrong password.
PATCH /api/account/security-keys/{keyId}

Rename a key

ParameterInType
keyId *pathstringThe key's id (base64url).
Request
{
  "name": "Backup YubiKey"
}
Responses
  • 204 Renamed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such key.
DELETE /api/account/security-keys/{keyId}

Remove a key or passkey

Needs the current password. With no key and no authenticator app left, two-step sign-in is off and the recovery codes go. The address is told by email.

ParameterInType
keyId *pathstringThe key's id (base64url).
Request
{
  "currentPassword": "correct horse battery"
}
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Wrong password.
  • 404 No such key.
GET /api/account/sessions

Where you are signed in

Responses
  • 200 Most recently used first.
    {
      "items": [
        {
          "createdAt": "2026-10-08T12:00:00Z",
          "current": true,
          "expiresAt": "2026-11-07T12:00:00Z",
          "id": 4,
          "ip": "203.0.113.9",
          "lastSeen": "2026-10-08T12:30:00Z",
          "userAgent": "Firefox"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
DELETE /api/account/sessions

Sign out everywhere else

Responses
  • 204 Done.
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/signins

Your recent sign-in attempts

Responses
  • 200 Newest first, at most 100.
    {
      "items": [
        {
          "at": "2026-10-08T12:00:00Z",
          "email": "amy@example.com",
          "ip": "203.0.113.9",
          "reason": "password",
          "success": false,
          "userAgent": "Firefox"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/ssh-keys

Your SSH keys, and how to reach Hatch

The public keys that sign you in to Hatch, the emergency shell for the servers you manage, and (when it is on) where to connect: ssh -p PORT USER@HOST, with the host key's fingerprint to check the first time.

Responses
  • 200 The keys.
    {
      "hatch": {
        "enabled": true,
        "fingerprint": "SHA256:k3yF1ngerprintExampleExampleExampleExample0",
        "host": "hatch.example.com",
        "port": 2222,
        "user": "amy@example.com"
      },
      "items": [
        {
          "createdAt": "2026-11-08T12:00:00Z",
          "fingerprint": "SHA256:Hq3Y0exampleexampleexampleexampleexample0",
          "id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
          "lastUsedAt": null,
          "name": "amy@laptop",
          "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyOnly"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/ssh-keys

Add an SSH key

One public key (the .pub file's line): Ed25519, ECDSA, a security key, or RSA of 2048 bits or more. It takes your current password, since a key reaches every server you manage, and you are emailed. A key can be on one account only, since the key says who signs in to Hatch (the user name is ignored). The name defaults to the key's comment. At most 20 keys.

Request
{
  "currentPassword": "your current password",
  "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyOnly amy@laptop"
}
Responses
  • 201 Added.
    {
      "createdAt": "2026-11-08T12:00:00Z",
      "fingerprint": "SHA256:Hq3Y0exampleexampleexampleexampleexample0",
      "id": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
      "lastUsedAt": null,
      "name": "amy@laptop",
      "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyOnly"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Your current password is not right.
  • 409 That key is on an account already (this one or another; whose is not said).
  • 422 Not a public key Hatch takes, no password on the account, or 20 keys already.
DELETE /api/account/ssh-keys/{keyId}

Remove an SSH key

It no longer signs in to Hatch; sessions it opened end within a minute.

ParameterInType
keyId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such key on your account.
GET /api/account/storage

Your organizations' object storage

The storage accounts of your organizations' object storage subscriptions, with what each keeps and may keep, whether you manage it (owners do; admins too on plans with team access), and where S3 clients connect (path-style; off until staff give the service its host).

Responses
  • 200 The accounts and the endpoint.
    {
      "endpoint": {
        "enabled": true,
        "host": "s3.example.com",
        "pathStyle": true,
        "region": "us-east-1",
        "url": "https://s3.example.com"
      },
      "items": [
        {
          "buckets": 2,
          "bytesUsed": 1048576,
          "canManage": true,
          "id": "6f1d2c3b-4a5e-4f60-8172-839a4b5c6d7e",
          "limits": {
            "buckets": 10,
            "storageBytes": 250000000000
          },
          "name": "Acme Files",
          "orgId": "0a1b2c3d-4e5f-4061-8273-94a5b6c7d8e9",
          "orgName": "Acme Files",
          "plan": "starter",
          "status": "active"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/storage/{accountId}/buckets

A storage account's buckets

For the account's managers; anyone else is told it isn't there (or, for its organization's other members, 403).

ParameterInType
accountId *pathstring
Responses
  • 200 The buckets, by name.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You are in the organization but don't manage its storage.
  • 404 No such storage account of yours.
POST /api/account/storage/{accountId}/buckets

Make a bucket

Bucket names are one namespace for everyone, as on S3: 3 to 63 lower-case letters, digits, dots and hyphens, starting and ending with a letter or digit, not shaped like an IP address. Up to the plan's number of buckets.

ParameterInType
accountId *pathstring
Request
{
  "name": "acme-backups"
}
Responses
  • 201 Made.
    {
      "accountId": "6f1d2c3b-4a5e-4f60-8172-839a4b5c6d7e",
      "bytes": 0,
      "createdAt": "2026-11-08T12:00:00Z",
      "id": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
      "name": "acme-backups",
      "objects": 0
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You don't manage this account's storage.
  • 404 No such storage account of yours.
  • 409 The name is taken (by you or anyone), or the account is suspended.
  • 422 Not a bucket name we take, or the plan's buckets are used.
DELETE /api/account/storage/{accountId}/buckets/{bucketId}

Remove an empty bucket

Access keys held to the bucket are removed with it.

ParameterInType
accountId *pathstring
bucketId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You don't manage this account's storage.
  • 404 No such bucket in this account.
  • 409 The bucket holds objects or unfinished uploads, or the account is suspended.
GET /api/account/storage/{accountId}/keys

A storage account's access keys

Never their secrets, which are shown once, when made.

ParameterInType
accountId *pathstring
Responses
  • 200 The keys, oldest first.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You don't manage this account's storage.
  • 404 No such storage account of yours.
POST /api/account/storage/{accountId}/keys

Make an access key

An access key id and secret for S3 clients. The secret is in this answer only: we keep it sealed and never show it again. A key can be held to one bucket of the account, or to reading. At most 50.

ParameterInType
accountId *pathstring
Request
{
  "bucketId": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
  "name": "backup server",
  "readOnly": false
}
Responses
  • 201 Made; keep the secret now.
    {
      "key": {
        "accessKeyId": "ZKEXAMPLEEXAMPLEEXA",
        "accountId": "6f1d2c3b-4a5e-4f60-8172-839a4b5c6d7e",
        "bucketId": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
        "createdAt": "2026-11-08T12:00:00Z",
        "id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f9a0",
        "lastUsedAt": null,
        "name": "backup server",
        "readOnly": false
      },
      "secretAccessKey": "your-secret-is-shown-here-once"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You don't manage this account's storage.
  • 404 No such storage account of yours.
  • 409 The account is suspended.
  • 422 No name, a bucket that isn't this account's, or 50 keys already.
DELETE /api/account/storage/{accountId}/keys/{keyId}

Remove an access key

Requests signed with it are refused at once.

ParameterInType
accountId *pathstring
keyId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 You don't manage this account's storage.
  • 404 No such key in this account.
  • 409 The account is suspended.
GET /api/account/two-factor

Whether two-step sign-in is on

Responses
  • 200 The state.
    {
      "enabled": true,
      "recoveryCodesLeft": 9
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
DELETE /api/account/two-factor

Turn two-step sign-in off

Needs the password and a current code (or a recovery code). The address is told by email.

Responses
  • 204 Off.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The password or code is not right (it counts as a failed sign-in).
POST /api/account/two-factor/enable

Turn it on with a first code

Returns ten recovery codes, shown once. Every other session ends and the address is told by email.

Responses
  • 200 The recovery codes.
    {
      "recoveryCodes": [
        "abcd-efgh-jkmn"
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 409 Setup was not started.
  • 422 The code is not right.
POST /api/account/two-factor/recovery-codes

Replace the recovery codes

Responses
  • 200 The new codes
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The password is not right.
  • 409 Two-step sign-in is off.
POST /api/account/two-factor/setup

Start setting up an authenticator app

Returns a new secret as text, an otpauth URI and a QR code. Nothing changes until it is enabled with a code.

Responses
  • 200 The secret.
    {
      "qrCode": "data:image/png;base64,...",
      "secret": "JBSWY3DPEHPK3PXP",
      "uri": "otpauth://totp/Zappocity:amy@example.com?secret=..."
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The password is not right.
  • 409 It is already on.

Billing

GET /api/account/orgs/{orgId}/quotes

The quotes we sent an organization

For its owners and billing members: every quote sent to it (never drafts), newest first, with its lines, what the first invoice and each month or year after come to, and whether it can still be answered (status sent and expired false).

ParameterInType
orgId *pathstring
Responses
  • 200 The quotes.
    {
      "items": [
        {
          "answeredAt": null,
          "answeredBy": "",
          "declineReason": "",
          "expired": false,
          "firstCents": 27500,
          "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
          "invoiceId": null,
          "lines": [
            {
              "description": "Mailbox migration, hours",
              "period": "once",
              "quantity": 3,
              "unitCents": 7500
            },
            {
              "description": "Priority support",
              "period": "month",
              "quantity": 1,
              "unitCents": 5000
            }
          ],
          "monthlyCents": 5000,
          "notes": "",
          "number": 1001,
          "orgId": "9f8e7d6c-5b4a-4321-8fed-cba987654321",
          "orgName": "Acme Co",
          "sentAt": "2026-11-05T12:00:00Z",
          "status": "sent",
          "subject": "Migration and priority support",
          "tag": "ZQ-1001",
          "validUntil": "2026-12-05T23:59:59Z",
          "yearlyCents": 0
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such organization
POST /api/account/orgs/{orgId}/quotes/{quoteId}/accept

Accept a quote

An owner or billing member accepts a sent quote before it expires: its lines become charges on the organization and are invoiced at once (the answer has the invoice, to pay as any other); monthly or yearly lines are billed each period after.

ParameterInType
orgId *pathstring
quoteId *pathstring
Responses
  • 200 The quote, accepted, and its invoice.
    {
      "invoice": {
        "number": 1043,
        "status": "open",
        "totalCents": 27500
      },
      "quote": {
        "status": "accepted",
        "tag": "ZQ-1001"
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such quote or organization
  • 409 Already answered
  • 410 Expired.
POST /api/account/orgs/{orgId}/quotes/{quoteId}/decline

Decline a quote

ParameterInType
orgId *pathstring
quoteId *pathstring
Request
{
  "reason": "Over budget this quarter."
}
Responses
  • 200 The quote
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such quote or organization
  • 409 Already answered
  • 410 Expired.
  • 422 The reason is too long.

DNS

GET /api/account/dns

Your DNS accounts

The dnso.city DNS accounts of the organizations you are a member of; owners change their zones, other members read them.

Responses
  • 200 The accounts.
    {
      "items": [
        {
          "canEdit": true,
          "id": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6a7b",
          "name": "Acme",
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgName": "Acme",
          "plan": "free",
          "status": "active",
          "zones": 1
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/dns

Start a dnso.city DNS account

Makes an organization you own, named name, subscribed to dnso.city's Free plan (3 zones). Needs a confirmed email and dnso.city available; up to 3 per person (409 after).

Request
{
  "name": "Acme"
}
Responses
  • 201 The DNS account.
  • 400 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 The email is not confirmed.
  • 409 Not open yet
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/dns/{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 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such DNS account
POST /api/account/dns/{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 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{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 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such DNS account
DELETE /api/account/dns/{accountId}/zones/{zoneId}

Stop serving a zone

Its records go with it.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{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. When the zone is linked to a provider, linked names it: its name servers answer for the domain with its own DNSSEC key, so elsewhere and a mismatched DS record are expected then.

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 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such DNS account or zone here.
  • 503 Delegation is not checked by this server.
PUT /api/account/dns/{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 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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.
GET /api/account/dns/{accountId}/zones/{zoneId}/push

The provider the zone is pushed to

dnso.city keeps every zone; a zone whose name servers are another provider's (Route53, Cloudflare, DigitalOcean, Gandi, GoDaddy, Hetzner, Linode, Namecheap, OVHcloud, Porkbun, Spaceship, Vultr) can be linked to it, and dnso.city then pushes every change there through the provider's API, a moment after it is made, and again every hour. providers lists them with the fields each needs. Secrets are never shown; secretsSet names those kept. unmanaged are the provider's records dnso.city leaves alone, as last read.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 Whether and where the zone is pushed, and how the last push went.
    {
      "changes": 3,
      "error": "",
      "inStep": true,
      "linked": true,
      "mode": "authoritative",
      "offered": true,
      "provider": "cloudflare",
      "pushedAt": "2026-11-07T12:00:00Z",
      "pushedSerial": 12,
      "secretsSet": [
        "apiToken"
      ],
      "settings": {},
      "unmanaged": [
        {
          "data": "1 . alpn=h2",
          "name": "@",
          "ttl": 300,
          "type": "HTTPS"
        }
      ],
      "updatedAt": "2026-11-07T11:59:00Z"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such DNS account or zone here.
PUT /api/account/dns/{accountId}/zones/{zoneId}/push

Link the zone to the provider whose name servers answer for it

The credentials are checked by reading the zone at the provider; nothing is changed when that fails. mode authoritative (the default) makes the provider's copy exactly dnso.city's, removing and overwriting what dnso.city does not have, after first importing the provider's records dnso.city lacks (import, default true); managed touches only the record sets dnso.city has (and removes there only those it pushed before), listing the provider's others as unmanaged. Either way the provider's SOA, its name servers at the zone's top, and record types dnso.city cannot hold are left alone. Credentials not given again are kept when the provider is the same. Once linked, the zone's DNSSEC is the provider's: dnso.city's DS is no longer offered. The zone is pushed at once; a failure comes back as warning.

ParameterInType
accountId *pathstring
zoneId *pathstring
Request
{
  "credentials": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  },
  "mode": "authoritative",
  "provider": "route53"
}
Responses
  • 200 The link, with `imported` (records added to dnso.city), `leftAtProvider` and any `warning`.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones.
  • 404 No such DNS account or zone here
  • 409 The provider's records would pass the plan's limit.
  • 422 Not a provider, missing or malformed credentials, a secondary zone, or the provider could not be read with them.
DELETE /api/account/dns/{accountId}/zones/{zoneId}/push

Stop pushing the zone

The records at the provider stay as they are. dnso.city's DNSSEC is offered again.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 204 Unlinked.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones.
  • 404 No such DNS account or zone here
POST /api/account/dns/{accountId}/zones/{zoneId}/push/adopt

Bring the provider's unmanaged records into dnso.city

Reads the zone at the provider and adds its record sets dnso.city lacks (all, or those named in sets as "name TYPE"), so dnso.city manages them from then on. Types dnso.city cannot hold stay at the provider (leftAtProvider).

ParameterInType
accountId *pathstring
zoneId *pathstring
Request
{
  "sets": [
    "www A",
    "@ MX"
  ]
}
Responses
  • 200 The link, with `adopted` (records added).
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones.
  • 404 No such DNS account or zone here
  • 409 The records would pass the plan's limit.
  • 422 The provider could not be read.
POST /api/account/dns/{accountId}/zones/{zoneId}/push/run

Push the zone now

Pushes it now (unless a push of it is running), putting back anything changed at the provider. A failure comes back as warning.

ParameterInType
accountId *pathstring
zoneId *pathstring
Responses
  • 200 The link and how the push went.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones.
  • 404 No such DNS account or zone here
POST /api/account/dns/{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 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{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 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{accountId}/zones/{zoneId}/records/{recordId}

Remove a record

ParameterInType
accountId *pathstring
zoneId *pathstring
recordId *pathstring
Responses
  • 200 The zone and its records.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{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 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones.
  • 404 No such DNS account or zone here.
  • 409 The zone is not a secondary.
PUT /api/account/dns/{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 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its 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 /api/account/dns/{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 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change its zones, or the plan does not include secondary DNS.
  • 404 No such DNS account or zone here.

Organizations

GET /api/account/invitations

Invitations to join organizations

The invitations to your email address that have not expired. None are listed until the address is confirmed (emailConfirmed).

Responses
  • 200 The invitations.
    {
      "emailConfirmed": true,
      "items": [
        {
          "createdAt": "2026-10-18T09:00:00Z",
          "email": "sam@example.com",
          "expiresAt": "2026-11-01T09:00:00Z",
          "id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
          "invitedBy": "Alice",
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgName": "Acme",
          "role": "member"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
DELETE /api/account/invitations/{invitationId}

Decline the invitation

ParameterInType
invitationId *pathstring
Responses
  • 204 Declined.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Your email address is not confirmed yet.
  • 404 No such invitation to your address
POST /api/account/invitations/{invitationId}/accept

Join the organization

You become a member with the invitation's role (keeping your role if you are one already).

ParameterInType
invitationId *pathstring
Responses
  • 200 Your membership.
    {
      "createdAt": "2026-01-02T10:00:00Z",
      "email": "alice@example.com",
      "fromMailbox": false,
      "name": "Alice",
      "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
      "orgKind": "company",
      "orgName": "Acme",
      "personId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
      "role": "owner"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Your email address is not confirmed yet.
  • 404 No such invitation to your address
POST /api/account/orgs

Start an organization

Makes a company organization you own, alongside any you already belong to. Needs a confirmed email address (403); a person owns at most 20 (409).

Request
{
  "name": "Acme"
}
Responses
  • 201 The organization.
    {
      "id": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
      "kind": "company",
      "members": 1,
      "name": "Acme",
      "status": "active"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Your email address is not confirmed yet.
  • 409 You own the most organizations one person can.
  • 422 The name is missing or too long.
POST /api/account/orgs/{orgId}/invitations

Invite someone to the organization

For owners and admins with a confirmed email address; only owners invite owners (403). Emails the address a link to the account page, where the person with that confirmed address accepts or declines; it lasts 14 days. Inviting the same address again changes the role and starts the 14 days again, but is emailed again only after an hour. Each person sends at most 50 invitations a day (429).

ParameterInType
orgId *pathstring
Request
{
  "email": "sam@example.com",
  "role": "member"
}
Responses
  • 201 The members and invitations
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Your email address is not confirmed
  • 404 Not an organization whose members you manage.
  • 409 The address is a member's already.
  • 422 Not an email address
  • 429 You sent the day's invitations.
DELETE /api/account/orgs/{orgId}/invitations/{invitationId}

Withdraw an invitation

For owners and admins.

ParameterInType
orgId *pathstring
invitationId *pathstring
Responses
  • 204 Withdrawn.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such invitation
GET /api/account/orgs/{orgId}/members

An organization's members and open invitations

For its owners and admins.

ParameterInType
orgId *pathstring
Responses
  • 200 The members, and invitations not yet answered or expired.
    {
      "invitations": [
        {
          "createdAt": "2026-10-18T09:00:00Z",
          "email": "sam@example.com",
          "expiresAt": "2026-11-01T09:00:00Z",
          "id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
          "invitedBy": "Alice",
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgName": "Acme",
          "role": "member"
        }
      ],
      "members": [
        {
          "createdAt": "2026-01-02T10:00:00Z",
          "email": "alice@example.com",
          "fromMailbox": false,
          "name": "Alice",
          "orgId": "0f9e8d7c-6b5a-4c3d-2e1f-0a9b8c7d6e5f",
          "orgKind": "company",
          "orgName": "Acme",
          "personId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
          "role": "owner"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 Not an organization whose members you manage.
PUT /api/account/orgs/{orgId}/members/{personId}

Change a member's role

For owners and admins; only owners make owners or change an owner's role (403). The last owner cannot be demoted (409). A member with a mailbox in one of the organization's Mailocity teams gets the role on those mailboxes too.

ParameterInType
orgId *pathstring
personId *pathstring
Request
{
  "role": "billing"
}
Responses
  • 200 The members and invitations
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only owners make or change owners.
  • 404 No such member
  • 409 The organization would have no owner.
  • 422 Not one of the roles.
DELETE /api/account/orgs/{orgId}/members/{personId}

Remove a member, or leave

Your own id leaves the organization, whatever your role. Owners and admins remove others (only owners remove owners). The last owner cannot go (409), nor a member with a mailbox in one of its Mailocity teams while the mailbox is there (409, naming it).

ParameterInType
orgId *pathstring
personId *pathstring
Responses
  • 204 Removed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only owners remove owners.
  • 404 No such member or organization.
  • 409 The last owner
GET /api/account/orgs/{orgId}/whois

How the organization appears in ARIN's whois

Where we register its servers' address blocks with ARIN (SWIP), whois shows private (the default: ARIN's Private Customer, with only the city, state, postal code and country of its billing address) or organization (its name and billing address). With the blocks registered. For its owners and admins.

ParameterInType
orgId *pathstring
Responses
  • 200 The choice and the blocks registered.
    {
      "reassignments": [
        {
          "net": "2001:db8:1000:1::/64",
          "netHandle": "NET6-2001-DB8-1000-1-1",
          "status": "registered"
        }
      ],
      "whois": "private"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such organization
PUT /api/account/orgs/{orgId}/whois

Change how the organization appears in ARIN's whois

For its owners. The blocks registered are updated at ARIN within minutes. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
Request
{
  "whois": "organization"
}
Responses
  • 200 The choice and the blocks registered.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only the organization's owners change it.
  • 404 No such organization
  • 422 Not private or organization.

Servers

GET /api/account/orgs/{orgId}/servers

The organization's servers

For the organization's owners and admins. Each VPS subscription is one server; setUp is false until we have set its VM up. state is the server's state now (with uptime and CPU, memory, disk and network use when the plan includes status and usage), or stateError says it could not be read. network has its IPv4 addresses (bind them, or take them by DHCP) and its IPv6: a management address in a shared /64 with its gateway, and a /64 of its own routed to the management address. With ipv6.dhcp, both can also be taken by DHCPv6 (the /64 as a delegated prefix). unpaid says how far a suspension for an unpaid invoice has gone: offline (its network is off, still running), shutdown, or removed (its VM is gone); empty otherwise.

ParameterInType
orgId *pathstring
Responses
  • 200 The servers.
    {
      "items": [
        {
          "createdAt": "2026-11-06T12:00:00Z",
          "features": [
            "power",
            "usage",
            "ipv6"
          ],
          "id": "1f0c2b9e-3a5d-4a8e-9c1b-7d2e4f6a8b0c",
          "kind": "qemu",
          "name": "Server 1",
          "network": {
            "ipv4": [
              {
                "address": "203.0.113.10",
                "bridge": "",
                "gateway": "",
                "prefix": 32,
                "routed": true
              }
            ],
            "ipv4Management": {
              "address": "10.255.0.10",
              "bridge": "vmbr0",
              "dhcp": true,
              "gateway": "10.255.0.1",
              "network": "10.255.0.0/24"
            },
            "ipv6": {
              "bridge": "vmbr1",
              "dhcp": false,
              "gateway": "2001:db8:ffff::1",
              "management": "2001:db8:ffff::100",
              "network": "2001:db8:ffff::/64",
              "nic": 1,
              "prefix": "2001:db8:1000:1::/64"
            },
            "mac": "bc:24:11:aa:bb:cc"
          },
          "plan": {
            "code": "vps-small",
            "diskGb": 40,
            "ipv4": 1,
            "memoryMb": 2048,
            "name": "Small",
            "snapshots": 3,
            "vcpus": 1
          },
          "setUp": true,
          "state": {
            "cpu": 0.04,
            "cpus": 1,
            "maxmem": 2147483648,
            "mem": 734003200,
            "netin": 1048576,
            "netout": 524288,
            "status": "running",
            "uptime": 86400
          },
          "status": "active",
          "unpaid": ""
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such organization
GET /api/account/orgs/{orgId}/servers/{serverId}

A server

As in the list, for the organization's owners and admins.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The server
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
PATCH /api/account/orgs/{orgId}/servers/{serverId}

Rename a server

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "name": "web1"
}
Responses
  • 200 The server
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 422 The name is empty
GET /api/account/orgs/{orgId}/servers/{serverId}/access

A server's access settings

What the VM's cloud-init sets at its next start: its hostname, the user it makes, the SSH keys that user accepts, whether a password is set (never the password), and whether the VM has a cloud-init drive (without one, ask support). VMs only: a container's access is set inside it. With the access feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The access settings.
    {
      "cloudInit": true,
      "hostname": "web1.example.com",
      "passwordSet": true,
      "sshKeys": [
        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample amy@laptop"
      ],
      "user": "debian"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, no cloud-init drive, or a field not as described.
  • 502 The server could not be reached just now.
PATCH /api/account/orgs/{orgId}/servers/{serverId}/access

Set a server's hostname, user, SSH keys or password

Changes what the VM's cloud-init sets; fields left out stay as they are. The VM takes the change on at its next start through us (a reboot from inside the VM does not): send reboot true to reboot a running VM now. An empty password makes a random one of 20 characters, answered once as password and never kept by us; a password given must be 12 to 128 characters. An empty sshKeys removes them all. The password is never recorded; the change is, by what it set. With the access feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "password": "",
  "reboot": true
}
Responses
  • 200 The access settings now, the password made (only when one was), and whether it was rebooted.
    {
      "cloudInit": true,
      "hostname": "web1",
      "password": "x7Kp2mQr9TzVw4NcHb3E",
      "passwordSet": true,
      "rebooted": true,
      "sshKeys": [],
      "user": "root"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, no cloud-init drive, or a field not as described.
  • 429 Another action was taken on the server a moment ago (reboot only).
  • 502 The server could not be reached just now.
GET /api/account/orgs/{orgId}/servers/{serverId}/backups

A server's backups

The server's backups on its cluster's backup storage, newest first, with how many are kept and whether a backup or restore is running now. Only the backups Proxmox lists for the server's own VM are given. Where the plan includes backups, for the organization's owners and admins, while the server is active.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The backups.
    {
      "items": [
        {
          "ctime": 1793500000,
          "notes": "Zappocity: web1",
          "protected": false,
          "size": 1073741824,
          "vmid": 101,
          "volid": "backups:backup/vzdump-qemu-101-2026_11_01-02_00_00.vma.zst"
        }
      ],
      "keep": 7,
      "running": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no backups, the server is suspended, or backups are not set up for it yet.
  • 429 A backup was taken or restored less than ten minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/backups

Back a server up now

A snapshot backup, so the server keeps running. Proxmox keeps the newest few of the server (the cluster's backupKeep) and removes the oldest beyond that. Refused while Proxmox is busy with the server. Customers may take a backup, or restore one, every ten minutes. Where the plan includes backups, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 202 Started; the backup appears in the list once Proxmox has finished.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no backups, the server is suspended, or backups are not set up for it yet.
  • 429 A backup was taken or restored less than ten minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/backups/delete

Delete one of a server's backups

Removes one of the server's own backups from the storage; a protected one cannot be removed. Where the plan includes backups, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "volid": "backups:backup/vzdump-qemu-101-2026_11_01-02_00_00.vma.zst"
}
Responses
  • 200 Deleted.
    {
      "deleted": "backups:backup/vzdump-qemu-101-2026_11_01-02_00_00.vma.zst"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server (or you do not manage the organization's servers), or no such backup of it.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no backups, the server is suspended, or backups are not set up for it yet.
  • 429 A backup was taken or restored less than ten minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/backups/restore

Restore a backup over a server

Puts one of the server's own backups back over its disks: everything on the server now is replaced. The server must be stopped, and Proxmox not busy with it. Start it again once the restore has finished. Where the plan includes backups, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "volid": "backups:backup/vzdump-qemu-101-2026_11_01-02_00_00.vma.zst"
}
Responses
  • 202 Started.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server (or you do not manage the organization's servers), or no such backup of it.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no backups, the server is suspended, or backups are not set up for it yet.
  • 429 A backup was taken or restored less than ten minutes ago.
  • 502 The server could not be reached just now.
GET /api/account/orgs/{orgId}/servers/{serverId}/console

The server's text console (a websocket)

Upgrades to a websocket relayed to the server's text console: its first serial port (serial0, which must be a socket) for a VM, or the container's console. Send JSON text messages: {"t": "d", "d": "ls\r"} types keys, {"t": "r", "c": 120, "r": 40} gives the terminal's size in columns and rows. Binary messages back are the console's output, for a terminal to show; a text message {"closed": "why"} comes before Zappocity closes it. The Origin must be this site's. For the organization's owners and admins, while the server is active with the console feature; checked again every minute while open, and closed when that no longer holds, after eight hours, or with three consoles open on the server already. Opening one is recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
csrf *querystringThe session's CSRF token (a browser cannot set headers on a websocket).
Responses
  • 101 Switched to the websocket.
  • 401 Not signed in
  • 403 Opened from another site (the Origin is not this site).
  • 404 No such server
  • 409 The server is still being set up.
  • 422 Suspended, the plan has no console, or three consoles are open on it already.
  • 502 The console could not be reached.
GET /api/account/orgs/{orgId}/servers/{serverId}/firewall

A server's firewall

The server's own Proxmox firewall: its options, every rule in order (managed ones are ours: the port 25 block until support opens it), aliases, IP sets, and the addresses its IP filter lets it send from (its IPv4 addresses, management IPv6 address and routed /64; ours, kept in step with its addresses). Reading it first puts our part back if it was changed in Proxmox. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/firewall/aliases

Add a firewall alias

A name for an address or network, for rules' source and dest. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "cidr": "198.51.100.0/24",
  "comment": "Our office",
  "name": "office"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
PUT /api/account/orgs/{orgId}/servers/{serverId}/firewall/aliases/{name}

Change a firewall alias

Changes an alias's address or network and comment. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
name *pathstring
Request
{
  "cidr": "198.51.100.0/25"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
DELETE /api/account/orgs/{orgId}/servers/{serverId}/firewall/aliases/{name}

Remove a firewall alias

Removes an alias (Proxmox refuses while a rule uses it). With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
name *pathstring
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/firewall/ipsets

Make a firewall IP set

An empty set of addresses and networks, for rules as +name. Names starting ipfilter are the IP filter's. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "name": "monitoring"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
DELETE /api/account/orgs/{orgId}/servers/{serverId}/firewall/ipsets/{name}

Remove a firewall IP set

Removes one of the server's IP sets and its entries; never the IP filter's. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
name *pathstring
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/firewall/ipsets/{name}/entries

Add to a firewall IP set

Adds an address or network (nomatch leaves it out of the set). Never to the IP filter's. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
name *pathstring
Request
{
  "cidr": "203.0.113.5"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/firewall/ipsets/{name}/entries/delete

Take an address out of a firewall IP set

Removes an address or network from one of the server's IP sets; never the IP filter's. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
name *pathstring
Request
{
  "cidr": "203.0.113.5"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
PATCH /api/account/orgs/{orgId}/servers/{serverId}/firewall/options

Set a server's firewall policies and options

The policies for traffic no rule matches, log levels, and DHCP and NDP; fields left out stay as they are. The firewall itself, its IP and MAC filters and router advertisements are ours: asking to change them is refused (set both policies to ACCEPT to let everything through). With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "policyIn": "DROP",
  "policyOut": "ACCEPT"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/firewall/rules

Add a firewall rule

Adds a rule at pos in the whole list (default the end), never above ours, so the port 25 block always comes first. At most 200 rules. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "action": "ACCEPT",
  "comment": "Web",
  "dport": "443",
  "enable": true,
  "proto": "tcp",
  "type": "in"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
PUT /api/account/orgs/{orgId}/servers/{serverId}/firewall/rules/{pos}

Change a firewall rule

Replaces the rule at pos (fields left out are removed), and moves it to moveTo when given, never above ours. Ours cannot be changed: the port 25 rule says to open a support ticket. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
pos *pathinteger
Request
{
  "action": "ACCEPT",
  "enable": true,
  "macro": "SSH",
  "moveTo": 1,
  "source": "office",
  "type": "in"
}
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
DELETE /api/account/orgs/{orgId}/servers/{serverId}/firewall/rules/{pos}

Remove a firewall rule

Removes the rule at pos. Ours cannot be removed: the port 25 rule says to open a support ticket. With the firewall feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
pos *pathinteger
Responses
  • 200 The firewall as it is now.
    {
      "addresses": [
        "192.0.2.10",
        "2001:db8:ffff::100",
        "2001:db8:1000:1::/64"
      ],
      "aliases": [
        {
          "cidr": "198.51.100.0/24",
          "comment": "Our office",
          "name": "office"
        }
      ],
      "ipsets": [
        {
          "comment": "",
          "entries": [
            {
              "cidr": "203.0.113.5"
            }
          ],
          "name": "monitoring"
        }
      ],
      "options": {
        "dhcp": true,
        "logLevelIn": "nolog",
        "logLevelOut": "nolog",
        "ndp": true,
        "policyIn": "DROP",
        "policyOut": "ACCEPT"
      },
      "rules": [
        {
          "action": "REJECT",
          "comment": "zappocity: outgoing mail (port 25) is blocked; open a support ticket to have it opened",
          "dport": "25",
          "enable": true,
          "managed": true,
          "pos": 0,
          "proto": "tcp",
          "type": "out"
        },
        {
          "action": "ACCEPT",
          "comment": "SSH from anywhere",
          "enable": true,
          "macro": "SSH",
          "managed": false,
          "pos": 1,
          "type": "in"
        }
      ],
      "smtpOpen": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such rule, alias or IP set on it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a field not one Proxmox takes, or a change to our part (the port 25 rule says to open a support ticket; the IP filter and the firewall staying on are ours).
  • 502 The server could not be reached just now.
GET /api/account/orgs/{orgId}/servers/{serverId}/history

A server's usage history

The server's average use over each step of a timeframe, oldest first, as Proxmox keeps it (about 70 steps: a minute each for an hour, half an hour for a day, and so on). cpu is a fraction of the server's vCPUs (vcpus, as the plan gives now; 1 is all of them), mem and maxmem are bytes, and netin, netout, diskread and diskwrite are bytes a second. Steps with no data (the server was stopped) are left out. Where the plan includes usage graphs, for the organization's owners and admins, while the server is active.

ParameterInType
orgId *pathstring
serverId *pathstring
timeframequerystring
Responses
  • 200 The history.
    {
      "points": [
        {
          "cpu": 0.12,
          "diskread": 4096,
          "diskwrite": 20480,
          "maxmem": 2147483648,
          "mem": 1073741824,
          "netin": 1520.5,
          "netout": 830.2,
          "time": 1793500200
        }
      ],
      "timeframe": "day",
      "vcpus": 2
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no usage graphs, the server is suspended, or the timeframe is not one of those listed.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/power

Start, shut down, stop, reboot or reset a server

Where the plan includes power controls, for the organization's owners and admins. shutdown asks the server's system to shut down and turns it off after three minutes if it has not; stop turns it off at once, like pulling the plug; reboot asks it to restart; reset (not for containers) restarts it at once. A suspended server cannot be started. Actions are at least five seconds apart. The action is carried out after the answer: read the server again to see its state.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "action": "reboot"
}
Responses
  • 202 Taken.
    {
      "action": "reboot"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no power controls, the server is suspended, or the action is unknown.
  • 429 Another action was taken a moment ago.
  • 502 The server could not be reached just now.
PUT /api/account/orgs/{orgId}/servers/{serverId}/rdns

Name one of a server's addresses (reverse DNS)

Sets the PTR record of one of the server's addresses: an IPv4 address of a pool with a reverse zone, its management address, or any address of its routed /64 (at most 32 of those), in the reverse zones staff chose on dnso.city. Not in a delegated /64 (see rdns-delegation). The name must resolve (A or AAAA) to the address, so nobody can name an address after a domain they do not control. An empty hostname removes the name. Names go when their address is taken back. For the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "address": "192.0.2.10",
  "hostname": "mail.example.com"
}
Responses
  • 200 The server, with its network's names under reverseDns.
    {
      "network": {
        "reverseDns": {
          "delegation": null,
          "ipv4": [
            "192.0.2.10"
          ],
          "management": true,
          "names": {
            "192.0.2.10": "mail.example.com"
          },
          "prefix": true
        }
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 422 Not the server's address, reverse DNS not offered for it, a name that does not resolve to it, or the server is suspended.
PUT /api/account/orgs/{orgId}/servers/{serverId}/rdns-delegation

Delegate a server's routed /64 to name servers (reverse DNS)

Puts NS records for the server's routed /64 (its 16-nibble name under ip6.arpa) in the pool's reverse zone, so its people keep every name in the /64 on name servers of their own; the names Zappocity kept in the /64 go, and new ones are refused while it is delegated. Each name server must resolve. dnso.city's own name servers can be used only once the /64's zone is on dnso.city in a DNS account the caller owns (only staff and the people who manage the server can add that zone); its DS record is then put beside the NS records, unless the zone is linked to another provider. An empty list removes the delegation; it also goes when the /64 is taken back. For the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "nameservers": [
    "ns1.example.com",
    "ns2.example.com"
  ]
}
Responses
  • 200 The server, with the delegation under reverseDns.
    {
      "network": {
        "reverseDns": {
          "delegation": {
            "ds": [],
            "nameservers": [
              "ns1.example.com",
              "ns2.example.com"
            ],
            "prefix": "2001:db8:1000:1::/64",
            "updatedAt": "2026-11-07T12:00:00Z"
          },
          "ipv4": [
            "192.0.2.10"
          ],
          "management": true,
          "names": {},
          "prefix": true
        }
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server
  • 422 No routed /64, reverse DNS not offered for it, name servers that do not resolve, dnso.city's without the zone in a DNS account you own, or the server is suspended.
GET /api/account/orgs/{orgId}/servers/{serverId}/reinstall

The images a server may be reinstalled from

Its cluster's templates. VMs only. With the rescue (or reinstall) feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The templates.
    {
      "templates": [
        {
          "id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e",
          "name": "Debian 12"
        },
        {
          "id": "6c7d8e9f-0a1b-4c2d-9e3f-4a5b6c7d8e9f",
          "name": "Ubuntu 24.04"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, or a request not as described (the reason says which).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/reinstall

Reinstall a server

Erases the VM (it is stopped and destroyed with its disks and snapshots; backups stay) and starts building a new one from the template, with the same addresses. The server's name must be typed as confirm. SSH keys left out are the ones the VM has now; the template's user is used, and a password can be set on the Access tab afterwards. The build runs in the background: the server shows as being set up until it is ready. Spaced like power actions. With the rescue (or reinstall) feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "confirm": "web1",
  "templateId": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e"
}
Responses
  • 202 Erased; the build started.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, or a request not as described (the reason says which).
  • 429 Another action was taken on the server a moment ago.
  • 502 The server could not be reached just now.
GET /api/account/orgs/{orgId}/servers/{serverId}/rescue

A server's rescue state and images

Whether the VM is booting from an image (and which), and the ISO images it may boot from (its cluster's: rescue systems and installers). VMs only. With the rescue (or reinstall) feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The rescue state.
    {
      "active": true,
      "image": "SystemRescue 11",
      "images": [
        {
          "id": "3f1c2a9e-7b4d-4e1a-9c2f-5d6e7f8a9b0c",
          "name": "SystemRescue 11"
        },
        {
          "description": "Installs Debian 12 over the console, with an encrypted disk if wanted",
          "id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
          "installer": true,
          "name": "Debian 12 installer"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, or a request not as described (the reason says which).
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/rescue

Boot a server from an image

Puts the image in a free CD slot (or the VM's CD drive) first in the boot order, with its disks still attached, and starts the VM, or reboots it if running. It stays so, across reboots, until rescue is ended. Refused while in rescue, with no free CD slot, or when the image isn't on the VM's node. Spaced like power actions. With the rescue (or reinstall) feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "imageId": "3f1c2a9e-7b4d-4e1a-9c2f-5d6e7f8a9b0c"
}
Responses
  • 200 In rescue.
    {
      "active": true,
      "image": "SystemRescue 11",
      "images": [
        {
          "id": "3f1c2a9e-7b4d-4e1a-9c2f-5d6e7f8a9b0c",
          "name": "SystemRescue 11"
        },
        {
          "description": "Installs Debian 12 over the console, with an encrypted disk if wanted",
          "id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
          "installer": true,
          "name": "Debian 12 installer"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, or a request not as described (the reason says which).
  • 429 Another action was taken on the server a moment ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/rescue/end

End rescue

Takes the image out, puts the CD slot and the boot order back as they were, and reboots the VM if it is running. With the rescue (or reinstall) feature, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 Out of rescue.
    {
      "active": false,
      "image": "",
      "images": [
        {
          "id": "3f1c2a9e-7b4d-4e1a-9c2f-5d6e7f8a9b0c",
          "name": "SystemRescue 11"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Refused: not in the plan, the server suspended, a container, or a request not as described (the reason says which).
  • 429 Another action was taken on the server a moment ago.
  • 502 The server could not be reached just now.
GET /api/account/orgs/{orgId}/servers/{serverId}/snapshots

A server's snapshots

The server's snapshots in Proxmox, newest first (limit is how many the plan lets the server keep), and whether Proxmox is taking, rolling back or deleting one now. Only the snapshots Proxmox lists for the server's own VM are given. ram says the running memory was saved too. Where the plan includes snapshots, for the organization's owners and admins, while the server is active.

ParameterInType
orgId *pathstring
serverId *pathstring
Responses
  • 200 The snapshots.
    {
      "items": [
        {
          "description": "Zappocity: web1",
          "name": "before-upgrade",
          "parent": "",
          "ram": false,
          "time": 1793500000
        }
      ],
      "limit": 3,
      "running": false
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such snapshot of it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no snapshots or keeps no more, the name is taken or not one Proxmox takes, the server is suspended, or Proxmox is busy with it.
  • 429 A snapshot was taken or rolled back less than two minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/snapshots

Snapshot a server now

Takes a snapshot of the server's disks (and, with ram, a running VM's memory, so a rollback resumes it running; never a container's). Every field is optional (send {}): a name is made from the time. Its people may keep up to the plan's snapshots limit and take or roll back one every two minutes. Refused while Proxmox is busy with the server. Where the plan includes snapshots, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "description": "Before the PHP upgrade",
  "name": "before-upgrade",
  "ram": false
}
Responses
  • 202 Started; the snapshot appears in the list once Proxmox has finished.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such snapshot of it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no snapshots or keeps no more, the name is taken or not one Proxmox takes, the server is suspended, or Proxmox is busy with it.
  • 429 A snapshot was taken or rolled back less than two minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/snapshots/delete

Delete one of a server's snapshots

Removes one of the server's own snapshots; the server keeps what is on it now. Refused while Proxmox is busy with the server. Where the plan includes snapshots, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "name": "before-upgrade"
}
Responses
  • 202 Started.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such snapshot of it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no snapshots or keeps no more, the name is taken or not one Proxmox takes, the server is suspended, or Proxmox is busy with it.
  • 429 A snapshot was taken or rolled back less than two minutes ago.
  • 502 The server could not be reached just now.
POST /api/account/orgs/{orgId}/servers/{serverId}/snapshots/rollback

Roll a server back to one of its snapshots

Puts the server's disks (and its configuration, and memory where it was saved) back as they were at the snapshot: whatever changed since is lost. A snapshot taken with more vCPUs or memory than the plan has now is refused (ask staff). Once the rollback has finished the plan's vCPUs and memory are put on the VM again. Where the plan includes snapshots, for the organization's owners and admins, while the server is active. Recorded in the organization's activity.

ParameterInType
orgId *pathstring
serverId *pathstring
Request
{
  "name": "before-upgrade"
}
Responses
  • 202 Started.
    {
      "started": true
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such server, no such snapshot of it, or you do not manage the organization's servers.
  • 409 The server is still being set up.
  • 422 Not allowed: the plan has no snapshots or keeps no more, the name is taken or not one Proxmox takes, the server is suspended, or Proxmox is busy with it.
  • 429 A snapshot was taken or rolled back less than two minutes ago.
  • 502 The server could not be reached just now.
GET /api/vps/dhcp

The DHCP subnets and reservations, for a Kea host (kea-sync)

For kea-sync on the Kea host, with the host's own token (made by staff with role dhcp) in Authorization: Bearer zk_..., not a session; a router's token is refused, and a Kea host's token reads nothing else. subnets4 are the on-link IPv4 pools and the IPv4 management network that have a Kea subnet id, each with the router's address on it, its bridge and a reservation (MAC to address) for each address of a linked server; routed addresses have none (their server's management address is reserved instead). subnets6 is the management /64 when it has a Kea DHCPv6 subnet id, with each linked server's management address and routed /64 (as a delegated prefix) for the MAC of the card its IPv6 is on. Each read is recorded against the host (when, from where, how many reservations).

Responses
  • 200 The subnets.
    {
      "host": "kea1",
      "subnets4": [
        {
          "bridge": "vmbr0",
          "id": 1,
          "reservations": [
            {
              "hwAddress": "bc:24:11:aa:bb:01",
              "ipAddress": "192.0.2.2",
              "serverId": "1f0c2b9e-3a5d-4a8e-9c1b-7d2e4f6a8b0c"
            }
          ],
          "routers": "192.0.2.1",
          "subnet": "192.0.2.0/24"
        }
      ],
      "subnets6": [
        {
          "bridge": "vmbr1",
          "id": 6,
          "reservations": [
            {
              "hwAddress": "bc:24:11:aa:bb:61",
              "ipAddress": "2001:db8:ffff::100",
              "prefix": "2001:db8:1000:1::/64",
              "serverId": "1f0c2b9e-3a5d-4a8e-9c1b-7d2e4f6a8b0c"
            }
          ],
          "subnet": "2001:db8:ffff::/64"
        }
      ]
    }
  • 401 No Kea host token
GET /api/vps/routes

The routes, for a router (prefixd)

For prefixd on a router, with the router's own token (made by staff) in Authorization: Bearer zr_..., not a session. Answers the management /64, its gateway, the pool and each /64 to route via its server's management address. Suspended servers have no route. metadata is the IPv4 address of the Proxmox host running the metadata service, which prefixd routes 169.254.169.254/32 to ("" for no route). management4, gateway4 and pools4 are the IPv4 management network, the router's address on it and the routed IPv4 pools; routes4 are each routed address, a /32, via its server's management address. Each read is recorded against the router (when, from where, how many).

Responses
  • 200 The routes.
    {
      "gateway": "2001:db8:ffff::1",
      "gateway4": "10.255.0.1",
      "management": "2001:db8:ffff::/64",
      "management4": "10.255.0.0/24",
      "metadata": "192.0.2.42",
      "pool": "2001:db8:1000::/40",
      "pools4": [
        "203.0.113.0/24"
      ],
      "router": "edge1",
      "routes": [
        {
          "prefix": "2001:db8:1000:1::/64",
          "serverId": "1f0c2b9e-3a5d-4a8e-9c1b-7d2e4f6a8b0c",
          "via": "2001:db8:ffff::100"
        }
      ],
      "routes4": [
        {
          "prefix": "203.0.113.10/32",
          "serverId": "1f0c2b9e-3a5d-4a8e-9c1b-7d2e4f6a8b0c",
          "via": "10.255.0.10"
        }
      ]
    }
  • 401 No router token

Sign-in

POST /api/account/session/passkey

Sign in with a passkey (start)

No email or password; the passkey names its account. Returns options for navigator.credentials.get() and a challengeId.

Responses
  • 200 The options and a challengeId.
POST /api/account/session/passkey/finish

Sign in with a passkey (finish)

The browser's navigator.credentials.get() result. The passkey must have verified the user (PIN or biometrics), so the session counts as two-step for every product signed in to through Zappocity.

Request
{
  "challengeId": "5b0b4c1e-7d1a-4f5e-9a3b-1c2d3e4f5a6b",
  "credential": {
    "id": "q8V2...",
    "response": {
      "authenticatorData": "SZYN...",
      "clientDataJSON": "eyJ0...",
      "signature": "MEUC...",
      "userHandle": "dXNl..."
    },
    "type": "public-key"
  }
}
Responses
  • 201 Signed in; as POST /api/account/session.
  • 400 The challenge expired or was used.
  • 401 The passkey did not check out.
POST /api/account/session/security-key

Sign in with a password and a security key (start)

When POST /api/account/session answers twoFactorRequired with securityKey: true, send the email and password here for options for navigator.credentials.get().

Request
{
  "email": "amy@example.com",
  "password": "correct horse battery"
}
Responses
  • 200 The options and a challengeId.
  • 401 Wrong email or password.
  • 409 The account has no security key.
  • 429 Too many failed sign-ins.
POST /api/account/session/security-key/finish

Sign in with a password and a security key (finish)

The browser's navigator.credentials.get() result. A key whose signature counter went backwards (a copy) is refused.

Request
{
  "challengeId": "5b0b4c1e-7d1a-4f5e-9a3b-1c2d3e4f5a6b",
  "credential": {
    "id": "q8V2...",
    "response": {
      "authenticatorData": "SZYN...",
      "clientDataJSON": "eyJ0...",
      "signature": "MEUC..."
    },
    "type": "public-key"
  }
}
Responses
  • 201 Signed in; as POST /api/account/session.
  • 400 The challenge expired or was used.
  • 401 The key did not check out.

Signing in

POST /api/account/handoff

A one-time code for a desk's host

For the signed-in person, a code that signs them in on one supporto.city desk's host, once, within two minutes, and only in the browser holding the state the host started with. Answers where to send the browser. 422 when the host is not a desk's.

Request
{
  "host": "acme.supporto.city",
  "next": "/desk/acme",
  "state": "q1w2e3..."
}
Responses
  • 200 Where to go.
    {
      "url": "https://acme.supporto.city/api/account/handoff/redeem?code=...\u0026next=%2Fdesk%2Facme"
    }
  • 400 Not JSON, or unknown fields.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/handoff/redeem

Use a handoff code

On the desk's host: with this browser's state cookie, uses up the code, opens a session here (two-step sign-in carried over) and redirects to next. 400 when the code is wrong, used, expired, for another host or another browser.

ParameterInType
code *querystring
nextquerystring
Responses
  • 302 Signed in; to the desk.
  • 400 The code cannot be used here.
  • 403 The account is suspended.
GET /api/account/handoff/start

Sign in on a supporto.city desk's host

On a desk's own host (acme.supporto.city): keeps a state in a cookie of this host (__Host-zappocity_handoff, ten minutes) and redirects to the Zappocity account page with #handoff=<host>&state=<state>&next=<next>, where the signed-in page asks for a code. On any other host it redirects to /account#next=<next>. next is a desk's agent app (/desk or /desk/{desk}); anything else becomes /desk.

ParameterInType
nextquerystring
Responses
  • 302 To the account page.
POST /api/account/reset

Email a password reset link

The answer is the same whether or not the address has an account. Links work once, for an hour.

Responses
  • 202 If the address has an account
  • 403 The captcha answer is missing or wrong (`captchaRequired` is true).
  • 429 Too many attempts from this address, or too many recent links, or the account is locked.
POST /api/account/reset/confirm

Set a new password from a reset link

The link also confirms the address. Every session ends.

Responses
  • 200 The password is set.
  • 400 The link has expired or was already used.
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/session

Who is signed in, and the CSRF token

Responses
  • 200 Signed in.
    {
      "csrfToken": "Yk3...",
      "expiresAt": "2026-11-07T12:00:00Z",
      "person": {
        "email": "amy@example.com",
        "emailVerifiedAt": null,
        "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
        "name": "Amy"
      }
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/session

Sign in

Responses
  • 201 Signed in.
    {
      "csrfToken": "Yk3...",
      "expiresAt": "2026-11-07T12:00:00Z",
      "person": {
        "email": "amy@example.com",
        "emailVerifiedAt": null,
        "id": "6c1f0b7e-2d4a-4b8e-9f3c-1a2b3c4d5e6f",
        "name": "Amy"
      }
    }
  • 400 Not JSON, or unknown fields.
  • 401 Wrong email or password.
  • 403 The account is suspended (only said after the right password)
  • 429 Too many attempts from this address, or too many recent links, or the account is locked.
DELETE /api/account/session

Sign out

Ends this session, and the webmail, panel and help desk sessions signed in through it. On a help desk's own address, it ends the zappo.city session it was handed off from too.

Responses
  • 204 Signed out.
POST /api/account/session/confirm

Prove who you are again

For an application that asked (OpenID Connect prompt=login, or a max_age the session's last proof is older than): the password, and the code when two-step sign-in is on. The session stays the same; its next ID tokens carry the new auth_time. A wrong password or code counts like one at sign-in. Needs X-CSRF-Token.

Request
{
  "code": "123456",
  "password": "correct horse battery staple"
}
Responses
  • 204 Confirmed.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Wrong password or code; twoFactorRequired is true when a code is needed.
  • 429 Too many failed attempts.

Signing up

POST /api/account/signup

Create an account

Makes an unconfirmed account and emails a link to confirm the address (which also signs in). If the address already has an account, its owner is emailed instead; the answer is the same.

Request
{
  "email": "amy@example.com",
  "name": "Amy",
  "password": "correct horse battery"
}
Responses
  • 202 Check your email.
  • 400 Not JSON, or unknown fields.
  • 403 The captcha answer is missing or wrong (`captchaRequired` is true).
  • 422 A value is not acceptable, such as a password under 12 characters.
  • 429 Too many attempts from this address, or too many recent links, or the account is locked.
POST /api/account/verify

Confirm an email address from its link, and sign in

Responses
  • 200 Confirmed and signed in (as Session), or, for an account with two-step sign-in, confirmed with signIn true: sign in the usual way.
  • 400 The link has expired or was already used.
POST /api/account/verify/resend

Send a new confirmation link

Responses
  • 202 Sent.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 409 The address is already confirmed.
  • 429 Too many attempts from this address, or too many recent links, or the account is locked.

Status

GET /api/account/status

Service status

Incidents and maintenance: those not ended that have started or start within two weeks, and those ended in the last week, each with its updates (newest first) and the products it affects (none: every product). Anyone may read it, signed in or not; never who on staff posted it. For someone signed in, affectsYou says whether it affects every product or one that an organization they are in subscribes to.

Responses
  • 200 The notices.
    {
      "items": [
        {
          "affectsYou": true,
          "createdAt": "2026-11-05T09:05:00Z",
          "endsAt": null,
          "id": "7d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6",
          "kind": "incident",
          "productNames": [
            "Mailocity"
          ],
          "products": [
            "mailocity"
          ],
          "resolvedAt": null,
          "severity": "major",
          "startsAt": "2026-11-05T09:00:00Z",
          "status": "identified",
          "title": "Slow mail delivery",
          "updatedAt": "2026-11-05T09:40:00Z",
          "updates": [
            {
              "body": "A full disk on one relay; mail is queued, not lost.",
              "createdAt": "2026-11-05T09:40:00Z",
              "id": 2,
              "status": "identified"
            }
          ]
        }
      ]
    }

Support

GET /api/account/desk

The desk this host is

On a supporto.city desk's own host, the desk's name, for its customer page (/support). On that host the support and help routes (tickets, queues, help articles) are the desk's, not Zappocity's. 404 on any other host.

Responses
  • 200 The desk.
    {
      "name": "Acme Support",
      "slug": "acme",
      "status": "active"
    }
  • 404 Not a desk's host.
GET /api/account/help

The help center

Its categories and articles: published public ones, and those for customers when signed in. q searches them (the best matches first, with a snippet); limit=5 gives a few suggestions while writing a request. Signing in is not needed.

ParameterInType
qquerystring
categoryquerystring
limitqueryinteger
Responses
  • 200 The categories and articles.
    {
      "categories": [
        {
          "articles": 6,
          "name": "Billing",
          "slug": "billing"
        }
      ],
      "items": [
        {
          "category": "billing",
          "kind": "faq",
          "slug": "refund",
          "snippet": "Open a request under Billing within 30 days",
          "title": "How do I get a refund?"
        }
      ],
      "signedIn": false
    }
  • 404 No such category.
GET /api/account/help/{slug}

A help article, rendered

ParameterInType
slug *pathstring
Responses
  • 200 The article.
    {
      "category": "billing",
      "html": "\u003cp\u003eOpen a request under \u003cstrong\u003eBilling\u003c/strong\u003e within 30 days.\u003c/p\u003e",
      "kind": "faq",
      "slug": "refund",
      "title": "How do I get a refund?",
      "updatedAt": "2026-10-09T12:00:00Z"
    }
  • 404 No such article
POST /api/account/help/{slug}/rating

Say whether an article helped

Counted at most 20 times an hour from one address for an article.

ParameterInType
slug *pathstring
Request
{
  "helpful": true
}
Responses
  • 204 Counted.
  • 404 No such article.
  • 422 helpful is missing.
GET /api/account/support/queues

What a request can be about

The active, public queues (support, billing, abuse...), each with the questions it asks (fields: answer them in fields when opening a request; required ones must be answered).

Responses
  • 200 The queues.
    {
      "items": [
        {
          "allowContacts": true,
          "description": "Invoices, payments, plans and refunds.",
          "fields": [
            {
              "help": "On your invoice, such as ZI-1042.",
              "key": "order_number",
              "kind": "text",
              "label": "Order number",
              "options": [],
              "required": true
            }
          ],
          "name": "Billing",
          "slug": "billing"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
GET /api/account/tickets

Your support requests

Opened by you, or sent from or naming your confirmed address as a contact. Newest activity first. tag is the request as it is shown (its queue's prefix and the number).

Responses
  • 200 The tickets.
    {
      "items": [
        {
          "number": 41,
          "prefix": "BIL",
          "priority": "normal",
          "queue": "billing",
          "requesterEmail": "amy@example.com",
          "status": "pending",
          "subject": "Invoice question",
          "tag": "BIL-41",
          "updatedAt": "2026-10-08T12:00:00Z"
        }
      ]
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
POST /api/account/tickets

Open a support request

You get a confirmation by email; contacts (when the queue takes them) also get the replies.

Request
{
  "body": "Why was I charged twice?",
  "fields": {
    "order_number": "ZI-1042"
  },
  "queue": "billing",
  "subject": "Invoice question"
}
Responses
  • 201 The ticket.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 422 A value is not acceptable, such as a password under 12 characters.
GET /api/account/tickets/{number}

A request with its messages and contacts

Internal notes are never shown. queue says whether contacts can be added and whether you may close it. fields are the answers to the queue's questions.

ParameterInType
number *pathstringThe number, with or without its prefix (BIL-41, or ZC-41 from before queues had their own).
Responses
  • 200 OK.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such ticket
POST /api/account/tickets/{number}/close

Close your request

Where the queue lets customers; abuse reports, for one, stay open until staff close them (403).

ParameterInType
number *pathstring
Responses
  • 200 OK.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Only staff close requests in this queue.
  • 404 No such ticket
POST /api/account/tickets/{number}/contacts

Add someone to the replies

Only where the queue allows contacts (409 otherwise); at most 20.

ParameterInType
number *pathstring
Responses
  • 200 OK.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such ticket
  • 409 The queue does not take contacts
DELETE /api/account/tickets/{number}/contacts/{email}

Take someone off the replies

ParameterInType
number *pathstring
email *pathstring
Responses
  • 200 OK.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such ticket
POST /api/account/tickets/{number}/messages

Reply

Reopens a resolved request; a closed one takes no more (409).

ParameterInType
number *pathstring
Responses
  • 201 The ticket with its messages.
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 404 No such ticket
  • 409 The request is closed.
POST /api/account/tickets/{number}/rating

Rate a resolved or closed request

The requester rates a resolved or closed request, 1 to 5 with an optional comment, where its queue asks (the ticket's queue has askRating); again to change it. Contacts on the ticket cannot. Staff see it on the ticket, as an internal note, and in the ratings report.

ParameterInType
number *pathstring
Request
{
  "comment": "Quick, thanks!",
  "rating": 5
}
Responses
  • 200 The ticket with its messages
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Not the requester
  • 404 No such ticket
  • 409 The request is not resolved or closed.
  • 422 The rating is not 1 to 5
POST /api/account/tickets/{number}/secrets/{secretId}/view

View the secure information in a reply, once

A staff reply may carry secure information (its message has a secret with its id and label). Only the ticket's requester, signed in, can view it, and only once: afterwards it is deleted, or kept for staff only (afterView). Contacts on the ticket, and notification links, see that it is there but cannot view it. Answers carry Cache-Control no-store.

ParameterInType
number *pathstring
secretId *pathstring
Responses
  • 200 The data.
    {
      "afterView": "delete",
      "data": "c0rrect-h0rse",
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
    }
  • 401 Not signed in, or the CSRF token is missing or wrong.
  • 403 Not the ticket's requester.
  • 404 No such ticket or secure information
  • 410 Already viewed
GET /api/support/links/{token}

Open a request from a notification link, without signing in

Each notification link is for one address and works for 30 days, where the queue allows replying on the web without signing in. Otherwise the answer is 403 with signIn true and the ticket's number.

ParameterInType
token *pathstring
Responses
  • 200 The ticket with its messages.
  • 403 The queue needs signing in.
  • 404 The link has expired.
POST /api/support/links/{token}/messages

Reply from a notification link

ParameterInType
token *pathstring
Responses
  • 201 The ticket with its messages.
  • 403 The queue needs signing in.
  • 404 The link has expired.
  • 409 The request is closed.
POST /api/support/links/{token}/rating

Rate a request from a notification link

As POST /api/account/tickets/{number}/rating, without signing in, through a link sent to the requester's own address (one sent to a contact gets 403), where the queue allows replying on the web without signing in.

ParameterInType
token *pathstring
Request
{
  "rating": 4
}
Responses
  • 200 The ticket with its messages
  • 403 Not the requester's link
  • 404 The link has expired.
  • 409 The request is not resolved or closed.
  • 422 The rating is not 1 to 5