Skip to content

Mailspace domains

A mailspace serves mail for its primary domain — the domain it was purchased for — plus any number of secondary domains you add to it. These endpoints cover that domain list, the DNS TXT ownership proof a domain needs before it is served, and the mail DNS records (MX, SPF, DKIM, DMARC and the client-autoconfig hosts) each domain needs published.

The plan-level endpoints — purchase, resize, delete — are on Mailspace.

OAuth scopes: reads require mailspace:read, writes require mailspace:write. Session and API-key credentials bypass scope checks entirely (see OAuth).

Every endpoint on this page is nested under one mailspace and inherits the same chain of guards. They are documented once, in Error Codes — read that section before this one. A mailspace that is not visible to your credential returns 404 with an empty body.

The primary domain is verified first, and nothing else works until it is

A mailspace bought for a domain the workspace is not the registrar of record for is created unprovisioned: there is no mail server tenant behind it yet. In that state every endpoint on this page except the three primary-domain verification endpoints answers 409 not_provisioned.

The order is fixed:

  1. GET /api/mailspace/:mailspace_id/domain_verification for the TXT record to publish, then publish it.
  2. POST the same path. Ownership is checked live and provisioning starts.
  3. Poll until state reads provisioned.
  4. Everything else on this page becomes usable.

Those three endpoints are the only ones that skip the provisioning guard — they are what the guard sends you to.


List Domains

GET /api/mailspace/:mailspace_id/domains

Requires mailspace:read.

Every domain on the mailspace, in two lists: domains — read live from the mail server — is the primary plus every verified secondary the tenant actually serves, and pending_domains are the local rows still awaiting a DNS TXT ownership proof. A domain is in exactly one of them; state tells the three kinds apart. This endpoint is not paginated.

Params (all optional)
  • q: String | case-insensitive substring of the domain name. Filters both lists

An empty domains list is authoritative

The mail-server read is strict: a read that could not be performed answers 503 domains_unavailable rather than an empty list, so domains can be reconciled against. The empty-list version of this failure was particularly hard to spot, because primary_domain comes from local records and stayed populated — an outage looked exactly like a mailspace serving nothing.

The code covers that one read and nothing else. Note the whole response is the error envelope, so pending_domains and primary_domain are not returned alongside it even though both are local. See Read failures.

Returned Params
  • primary_domain: String | the mailspace's own mail domain
  • domains: Array | live from the mail server
    • name: String
    • description: String | null when none is set
    • state: String | primary for the mailspace's own domain, served for a verified secondary
    • verified: Boolean | always true in this list
    • dns_state: String | managed (the domain's DNS zone is hosted with CloudPress), setup_needed (external DNS, and the last probe found records missing), or manual (external DNS, nothing outstanding that we know of)
  • pending_domains: Array | local rows awaiting their TXT proof; these accept no mail
    • name: String
    • description: String | null when none is set
    • state: String | always pending_verification
    • verified: Boolean | true once ownership is proven but the domain has not been created on the mail server yet
    • verification: Object
      • txt_host: String | the record host, _mailspace-verify. followed by the domain
      • txt_value: String | the 32-character hex token to publish as the TXT value, minted per domain
      • txt_type: String | always TXT
    • last_checked_at: DateTime | when the TXT record was last looked up, null until the first check
    • created_at: DateTime
    • updated_at: DateTime
Errors
  • 503 domains_unavailable | the live domain read could not be performed. Retriable, and not an empty mailspace — see Read failures
  • plus the shared guards
curl -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domains

View a Domain

GET /api/mailspace/:mailspace_id/domains/:name

Requires mailspace:read.

The domain is addressed by name, not by an ID — .../domains/example.com. The path segment accepts dots, and the name is normalized the same way Add a Domain normalizes it.

A pending secondary domain is readable here too, and is the only thing you can do with it at this path: PATCH and DELETE refuse it with 409 domain_pending_verification. state tells you which shape you got.

If the mail server can't be reached, local state answers first

The lookup behind this path — shared by GET, PATCH and DELETE on /domains/:name — asks the mail server for the domain. When that read fails, rather than answering "no such domain", CloudPress consults its own table of pending domains before giving up. The order matters: a domain still awaiting its TXT proof is not on the mail server at all, so a client polling while verification is outstanding keeps getting 200 from local state instead of being stalled by an outage that does not affect it. A pending row resolved this way behaves exactly as it always does — readable here, 409 domain_pending_verification for PATCH and DELETE.

503 stalwart_unavailable is only for the case where neither the mail server nor a local pending row can resolve the name. A read that failed is never reported as 404 unknown_domain: that code means the mail server answered and the domain is not on this mailspace.

Returned Params
  • domain: Object | the served shape (state primary or served) or the pending shape (state pending_verification), field for field as in List Domains

Add a Domain

POST /api/mailspace/:mailspace_id/domains

Requires mailspace:write. Returns 201.

Adds a secondary domain. Which of two things happens depends on whether ownership is already proven:

The domain is created on the mail server immediately and comes back with state served. No TXT record, no waiting.

Proof means the billing family registered the domain with CloudPress. That is the only signal that counts here — merely hosting the domain's DNS zone with us does not qualify, because creating a zone needs nothing but edit access.

An expired registration still counts. The check fails only once the registration has been purged; a domain in its redemption period, or merely flagged at risk because a renewal is overdue, is still proof of ownership and still takes the immediate path.

The domain is parked as a pending row and the response carries the verification TXT record to publish. It is not created on the mail server and accepts no mail until you publish that record and call Verify a Domain.

Params
  • name: String | required. Normalized before use: a leading http:// or https://, a leading www., anything from the first / onward and a trailing dot are stripped, and the result is lowercased. So www.example.com is stored — and must be addressed — as example.com. A name that is not a valid hostname after that is 400 invalid_domain
  • description: String | optional free-text label. Blank is stored as no description
Returned Params
  • domain: Object | as in View a Domainstate served on the proven path, pending_verification otherwise
curl -X POST \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{"name":"second.example.com","description":"Second brand"}' \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domains
Errors
  • 400 invalid_domain | name is missing, or is not a valid hostname once normalized
  • 409 domain_exists | the domain is already served by this mailspace
  • 409 domain_pending_verification | the domain is already parked awaiting its TXT proof — use Domain Verification Status
  • 422 domain_create_failed | the mail server refused the create, or the pending row could not be saved. A name already registered on the mail server under a different tenant lands here

Update a Domain

PATCH /api/mailspace/:mailspace_id/domains/:name

Requires mailspace:write. Returns 200 with the domain object.

The description is the only thing this changes. A domain cannot be renamed — the name in the path identifies the domain, and a name in the body is ignored.

description is three-way, and the three cases are distinct

  • Omit the key — the description is left exactly as it is. Nothing about the field is sent to the mail server.
  • Send "" — the description is cleared. It reads back as null rather than "", because the mail server rejects an empty string for the field and CloudPress sends the null it requires on your behalf.
  • Send a value — set to that value.

If you relied on omitting the key to clear the description, send "" instead.

Params
  • description: String | the new description. Omit the key to leave it untouched; send "" to clear it
Errors
  • 404 unknown_domain | no such domain on this mailspace
  • 409 domain_pending_verification | the domain is still awaiting its TXT proof, so there is nothing on the mail server to change
  • 422 domain_update_failed | the domain is no longer on this mailspace, or the mail server refused the change
  • 503 stalwart_unavailable | the mail server couldn't be reached and the name isn't a pending domain either — see View a Domain

The 200 body is trustworthy even when the read-back fails

The write is confirmed by re-reading the domain, and that read can fail even though the write succeeded. When it does, the response falls back to what is known to be true: the description you just sent, or the pre-write description when you omitted the key. Every other field is derived rather than read back, so nothing here degrades to a placeholder.


Remove a Domain

DELETE /api/mailspace/:mailspace_id/domains/:name

Requires mailspace:write. Returns 200 with an empty body.

Removes a secondary domain from the mail server, drops CloudPress's local record of it, and removes the mail records CloudPress wrote for it in the workspace's own hosted DNS zone — without that last step the zone would keep advertising a mail host that no longer accepts mail for the domain.

A domain still in use is refused, not emptied

A domain that still has mailboxes, groups or mailing lists on it is refused with 422 domain_delete_failed, and the message names what is blocking (for example Can't delete example.com — it still has 3 mailboxes and 1 group. Delete those first, then remove the domain.). Nothing is destroyed by the refused call.

Aliases are not counted in that summary — they live on the mailbox or group they belong to, so removing the objects that hold them clears them too.

The primary domain cannot be removed: it is the mailspace's identity and goes away only with the mailspace itself. It answers 409 primary_domain.

To abandon a domain that never got past verification, use Abandon a Pending Domain instead — this path answers 409 domain_pending_verification for it.

Errors
  • 404 unknown_domain | no such domain on this mailspace
  • 409 primary_domain | the mailspace's own domain — see above
  • 409 domain_pending_verification | the domain is still awaiting its TXT proof
  • 422 domain_delete_failed | the domain still has mailboxes, groups or mailing lists on it; the domain is no longer on this mailspace; or the mail server refused the delete
  • 503 stalwart_unavailable | the mail server couldn't be reached and the name isn't a pending domain either — see View a Domain

View Mail DNS Records

GET /api/mailspace/:mailspace_id/domains/dns

Requires mailspace:read.

The DNS records one domain needs for mail to work, plus — on external DNS — the per-record result of the last probe. Optional domain selects which domain on the mailspace to report on; omit it for the primary domain. A domain this mailspace does not have answers 404 unknown_domain.

Read the top-level managed flag first, because it decides the shape of the rest of the response.

Params (all optional)
  • domain: String | which domain on the mailspace to report on. Defaults to the primary domain when omitted. Send the bare FQDN exactly — see the warning below

domain is not normalized the way the :name path segment is

The :name segment on View a Domain is cleaned up before use: a leading http:// or https://, a leading www., anything from the first / onward and a trailing dot are all stripped. This query parameter gets none of that. It is only trimmed of surrounding whitespace and lower-cased, then matched against the mailspace's domains exactly.

So https://mail.example.com, www.example.com, example.com/ and example.com. all answer 404 unknown_domain here, even where the same value would resolve as a path segment. Send the bare, exact FQDN — and if you are passing through user input, normalize it yourself first.

The same applies to domain on Re-check Mail DNS.

The domain's DNS zone is hosted with CloudPress, and the mail records are maintained there for you. The record list is reference only: no row carries a status or found field, and the detected, total and caa keys are omitted entirely (not null). checked_at and all_detected are null.

A zone we host but that has not been published to the DNS provider carries no provider id, and is deliberately reported as managed: false instead — the records cannot be written into it, so reporting "managed, nothing to do" would leave the mail records unpublished indefinitely.

External DNS: you publish these records yourself. Every row carries the status and found of the last probe, and the response adds detected, total and the caa advisory. Until a probe has ever run, checked_at is null and every row reads unchecked — this endpoint runs no lookups of its own. Re-check Mail DNS is what refreshes them.

The record values live in the response, not in this page

Publish exactly the value each row carries. The DKIM value is your mailspace's own signing key and the SPF and DMARC rows are built from the policy currently in force for the domain, so they are per-domain values that this page deliberately does not reproduce.

Returned Params
  • domain: String | the domain this report is for
  • managed: Boolean | see the two shapes above
  • checked_at: String | ISO 8601 timestamp of the last probe, null if none has run and always null when managed
  • detected: Integer | required rows currently detected. Omitted entirely when managed
  • total: Integer | required rows. Optional rows are excluded from both counts. Omitted entirely when managed
  • all_detected: Boolean | true only when every required row is detected; null when managed
  • records: Array
    • type: String | MX, A, AAAA, TXT or CNAME
    • name: String | the record host
    • value: String | the exact value to publish
    • priority: Integer | MX priority; null on every other type
    • purpose: String | what the record is for, e.g. Mail delivery, SPF – authorised senders, DKIM – message signing
    • optional: Boolean | true on the IPv6 AAAA row only. An optional row is reported like any other but never counts toward detected / total / all_detected — IPv4-only mail is fully working mail
    • placeholder: Boolean | true when the DKIM value is an instructional placeholder because the real signing key could not be read. Do not publish a placeholder value — retry the read instead
    • status: String | ok, mismatch, missing, or unchecked. Omitted entirely when managed
    • found: String | what is actually published, when it disagrees; null otherwise. Omitted entirely when managed
  • caa: Object | advisory only, and gates nothing. Omitted entirely when managed
    • status: String | ok, mismatch (a CAA policy at the mail host excludes the certificate authority our mail certificates come from — the mail certificate cannot be issued until it is widened), unknown, or unchecked

A record of your own is never overwritten in a hosted zone

In a zone CloudPress hosts, a name that already holds a record we did not write is left exactly as it is — the mail record is not published there, and the domain's mail DNS stays incomplete until you remove the existing record. This matters on a migration, where mail., webmail., autodiscover. and a DKIM selector often already point at the old provider.

These responses do not report that. A managed domain returns no per-record status at all, so compare the list above against the zone's actual records with List all records, or ask support at https://cloudpress.com/contact/.

curl -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  "https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domains/dns?domain=second.example.com"

Re-check Mail DNS

POST /api/mailspace/:mailspace_id/domains/dns_check

Requires mailspace:write. Returns 200 with the fresh result.

Runs the DNS lookups now, synchronously — the response carries the outcome. There is no task to poll and no 202. The result is stored, so View Mail DNS Records reports the same figures afterwards without looking anything up again.

Params (all optional)
  • domain: String | which domain on the mailspace to check. Defaults to the primary domain when omitted. It is not normalized like the :name path segment — send the bare, exact FQDN. See the warning on the read

A write, even though it changes no configuration

This is a POST, and the guards that care about mutation key on the HTTP verb rather than the action name. So it needs mailspace:write and edit permission on the owning workspace, and it is refused on a mailspace that is scheduled for deletion — while the GET beside it is not.

Rate limited to one run per domain every 5 minutes

The limit is keyed to the mailspace and the domain, so checking one domain never blocks another. A call inside the window is refused with 429 rate_limited and performs no lookups.

Two things deliberately do not spend the window: a domain whose DNS CloudPress hosts (it short-circuits before the limiter — see below), and a probe that fails, whose token is released so you can retry at once.

A domain whose DNS zone is hosted with CloudPress has nothing for you to check, so it returns 200 with managed: true, records: [], and checked_at and all_detected null — no lookups are run.

Returned Params
curl -X POST \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domains/dns_check
Errors
  • 404 unknown_domain | domain is not a domain on this mailspace
  • 429 rate_limited | checked too recently for this domain — see above
  • 422 dns_check_failed | the lookups could not be completed; the rate-limit token is released so you can retry immediately

Domain Verification Status

GET /api/mailspace/:mailspace_id/domains/:domain_name/verification

Requires mailspace:read.

Ownership verification for a secondary domain: the TXT record to publish and whether it has been seen yet.

The path segment is :domain_name, not :name

Nesting renames the parent segment, so the domain is :domain_name on this path and :name on the /domains/:name paths above. It is the same value.

This path addresses pending domains only. A domain the mailspace already serves — and a domain it has never heard of — both answer 404 unknown_domain; once verification succeeds, the domain moves to View a Domain and this path stops answering for it.

Returned Params
  • verification: Object
    • domain: String
    • state: String | awaiting_verification, or verified once the TXT record has been seen but the domain has not been created on the mail server yet
    • verified: Boolean
    • txt_type: String | always TXT
    • txt_host: String | the record host, _mailspace-verify. followed by the domain
    • txt_value: String | the 32-character hex token to publish, minted per domain
    • verified_at: DateTime | when ownership was proven, null until then
    • last_checked_at: DateTime | when the record was last looked up, null until the first check
    • create_attempts: Integer | how many times the unattended sweep has tried to create this verified domain on the mail server
    • create_attempts_exhausted: Boolean | true once the sweep has given up (at 10 attempts). Not a dead end — a POST below resets the counter and re-arms it

Verify a Domain

POST /api/mailspace/:mailspace_id/domains/:domain_name/verification

Requires mailspace:write. Returns 200.

Looks up the TXT record now. When the token is found, the domain is created on the mail server and the pending row is dropped in the same call — the response is the served domain, and from then on you address it at /api/mailspace/:mailspace_id/domains/:name.

A successful call also resets create_attempts, so a domain the unattended sweep gave up on can be retried here. A call whose TXT lookup fails returns 422 verification_failed and leaves the counter where it was.

The TXT record is re-read on every call, even after the domain verified

Unlike the primary-domain flow, this endpoint does not skip the lookup for an already-verified pending row. If the TXT record has since been removed, the call keeps failing with 422 verification_failed — which also means it cannot clear an exhausted create_attempts counter. Put the record back before retrying.

Returned Params
  • domain: Object
    • name: String
    • state: String | always served
    • verified: Boolean | always true
curl -X POST \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domains/second.example.com/verification
Errors
  • 404 unknown_domain | no domain is awaiting verification under that name on this mailspace
  • 422 verification_failed | the TXT record is not visible yet. DNS changes take a few minutes to propagate; nothing is changed, retry the same call
  • 422 domain_create_failed | ownership was proven but the mail server refused to create the domain — most often because the name is already registered on the mail server under another tenant. The proof stands and the pending row survives, so the same call is the retry

Abandon a Pending Domain

DELETE /api/mailspace/:mailspace_id/domains/:domain_name/verification

Requires mailspace:write. Returns 200 with an empty body.

Gives up on a secondary domain that never got verified: the pending row is dropped. Nothing exists on the mail server at this point, so no mail is affected and no records are removed anywhere. Adding the domain again later simply mints a new token.

Errors
  • 404 unknown_domain | no domain is awaiting verification under that name on this mailspace

Primary Domain Verification Status

GET /api/mailspace/:mailspace_id/domain_verification

Requires mailspace:read.

The primary domain's verification and setup state. This is the pre-flight path: it runs before the mailspace has a mail server tenant, and it is the only endpoint group on this page that skips the provisioning guard.

state walks in one direction:

state Meaning What to do
awaiting_verification the TXT record has not been seen publish txt_host / txt_value, then POST this path
provisioning ownership proven, the mail server tenant is being built poll
provision_failed ownership proven, setup errored POST this path to retry it
provisioned done the rest of this page is now usable
Returned Params
  • verification: Object
    • domain: String | the mailspace's primary domain
    • state: String | one of the four above
    • verified: Boolean | ownership has been proven
    • provisioned: Boolean | the mail server tenant exists
    • verified_at: DateTime | null until ownership is proven
    • txt_type: String | always TXT. Present only while a record is outstanding — omitted entirely (not null) once the domain is verified, and never present at all for a mailspace bought on a domain the workspace already owned
    • txt_host: String | the record host, _mailspace-verify. followed by the primary domain. Same condition as txt_type
    • txt_value: String | the 32-character hex token to publish. Same condition as txt_type
    • last_checked_at: DateTime | when the record was last looked up, null until the first check
    • provision_error: String | the last setup error, null when there is none
    • provision_attempts: Integer | setup attempts so far

Verify the Primary Domain

POST /api/mailspace/:mailspace_id/domain_verification

Requires mailspace:write. Returns 202.

Two jobs in one call, chosen by the mailspace's own state:

  • Not verified yet — the TXT record is looked up now. If the token is found, ownership is recorded and provisioning is queued.
  • Already verified (provision_failed, or a stalled provisioning) — this is the retry: no TXT lookup is performed, the stale error is cleared and provisioning is re-driven. That also re-arms the unattended sweep if it had given up.

202 means queued, not finished

The mail server tenant is built asynchronously. The body is the same verification object as the GET, with state provisioning. Poll this path — or provisioned on View a Mailspace — until it reads provisioned. Nothing else on this page works before then.

curl -X POST \
  -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  -H "X-Auth-Account: $ACCOUNT_ID" \
  https://my.cloudpress.com/api/mailspace/$MAILSPACE_ID/domain_verification
Errors
  • 409 already_provisioned | the mailspace is already set up, so its domain needs no verification
  • 422 verification_failed | the TXT record is not visible yet; retry the same call
  • 422 provisioning_failed | provisioning could not be re-driven

Cancel an Unverified Mailspace

DELETE /api/mailspace/:mailspace_id/domain_verification

Requires mailspace:write. Returns 200 with an empty body.

Abandons the purchase of a mailspace that never got provisioned.

Irreversible, and it moves money

The mailspace record is destroyed (it disappears from List Mailspaces) and its domain is released immediately, so re-purchasing mail for that domain succeeds straight away.

The money half is conditional: when the originating order was actually charged, the full first term is credited back and the subscription seat is released. An uncharged order — a zero-total one, for instance — is simply dropped, with no credit and no seat to release.

There is no confirmation parameter, and no step-up authentication over the API — a bearer token re-authenticates on every request. Every cancellation is written to the platform audit log with the credential that made the call.

This is the same outcome as the never-provisioned branch of Delete a Mailspace.

Errors
  • 409 already_provisioned | the mailspace is set up; cancel it with Delete a Mailspace instead
  • 422 cancel_failed | the credit could not be issued. The mailspace is deliberately left intact rather than deleted without a refund — retry, and contact support if it persists

Error Codes

All errors use the standard {"errors": [...], "code": "..."} envelope described in Error Responses.

Shared Guards

Every endpoint on this page runs the same chain before its own logic, in this order. The first one that fails answers the request.

Condition Response Applies to
mail hosting is not configured on the platform 503 stalwart_unavailable all
the mailspace is not visible to your credential 404, empty body all
the credential has no edit permission on the mailspace's own workspace 403 not_authorized writes
the mailspace is on hold (staff block or an unpaid-invoice hold) 403 mailspace_suspended all, reads included
the mailspace is scheduled for deletion 403 pending_delete writes
the mailspace has no mail server tenant yet 409 not_provisioned all except the three domain_verification endpoints

A few consequences worth knowing:

  • "Writes" means the HTTP verb, not the action. Anything that is not a GET or HEAD is a write, which is why Re-check Mail DNS is gated like any other write while the DNS read and the two verification-status reads are not.
  • Permission is checked on the workspace that owns the mailspace, not on the workspace in X-Auth-Account.
  • A suspended mailspace refuses reads too, unlike one scheduled for deletion, which stays readable for its whole retention window.
  • A mailspace outside your credential's reach is a 404, never a 403 — the API does not confirm that a mailspace exists elsewhere on the platform.
  • stalwart_unavailable has a second cause. Besides the platform-level guard above, the per-domain lookup behind GET, PATCH and DELETE /domains/:name answers 503 when the mail server cannot be reached and the name is not a pending domain either — see View a Domain.
  • The three domain_verification endpoints skip only the provisioning guard. Everything above it still applies. On a mailspace that is provisioned, the POST and the DELETE answer 409 already_provisioned, while the GET keeps answering 200 with state provisioned.

Endpoint Codes

Code Status Raised by
invalid_domain 400 create
domains_unavailable 503 list — the live domain read could not be performed. Not an empty mailspace
unknown_domain 404 show, update, destroy, dns, dns_check, verification (all three verbs)
domain_exists 409 create
domain_pending_verification 409 create, update, destroy
primary_domain 409 destroy
already_provisioned 409 primary-domain verify, primary-domain cancel
rate_limited 429 dns_check
domain_create_failed 422 create, verify a domain
domain_update_failed 422 update
domain_delete_failed 422 destroy
dns_check_failed 422 dns_check
verification_failed 422 verify a domain, primary-domain verify
provisioning_failed 422 primary-domain verify
cancel_failed 422 primary-domain cancel