Appearance
Domains API
All endpoints require a bearer token (a login JWT or an API key) and a permission, listed with each endpoint.
A domain is a hostname you connect to MicroApps and prove you own. Once verified, you can map it to a pod and turn on SSL. Domain names are globally unique across all accounts - see Add a domain for what happens when a name is already taken.
The domain object
json
{
"id": "6a4d5b83b7006ccae6391c41",
"name": "example.com",
"type": "fqdn",
"status": "Mapped",
"hasEncryption": true,
"ownershipStatus": "verified",
"ownershipLocked": true,
"podId": "6a4d5b83b7006ccae6391810",
"accountId": "6a4d5b83b7006ccae639100a",
"emailServerId": null,
"delegationLabel": "3f9c1a7d2e6b48059c1d7e2a4b60f835",
"mapping": {
"trafficType": "https",
"internalPort": 3000,
"internalIp": "10.20.0.5"
},
"validationData": { "name": "", "type": "", "ttl": 0, "value": "" },
"dnsStatus": "configured",
"dnsCheckedAt": "2026-09-16T14:15:00Z",
"dnsIssue": "",
"dnsIssueDetail": "",
"dnsRecords": [
{
"type": "TXT",
"recordType": "TXT",
"name": "microapps-verify",
"hostname": "microapps-verify.example.com",
"value": "8f2c1d0a-6a41-4c9e-9a77-3f2b8e5d1c04",
"status": "verified",
"ttl": 300,
"category": "ownership",
"purpose": "Domain Ownership Verification",
"description": "TXT record proving you control this domain. Publish the value exactly as shown; it verifies ownership and locks the domain to your account.",
"required": true
}
],
"uptimeStatus": "healthy",
"uptimeCode": 200,
"uptimeCheckedAt": "2026-09-16T14:30:00Z",
"createdAt": "2026-09-01T12:00:00Z",
"updatedAt": "2026-09-01T12:05:00Z"
}| Field | Meaning |
|---|---|
name | The hostname. |
type | Derived from the name, not something you set: fqdn (a root domain like example.com), subdomain (app.example.com), or local. It is unknown for a moment right after you add the domain, while classification runs. |
status | Unmapped, Configuring, Mapped, or ConfigFailed. Empty on a domain that has never been mapped. |
hasEncryption | true once SSL is active. |
ownershipStatus | owned, pending, verified, or unverified. |
ownershipLocked | true once you have proven ownership. Latches on, and is what gates email on the domain. |
podId | The mapped pod, or null when unmapped. |
mapping | The current mapping: trafficType (http/https), internalPort, internalIp. Not null when unmapped - every field is just zeroed. Check podId instead. |
delegationLabel | Your domain's stable token in the _acme-challenge CNAME target. |
dnsRecords | Every record we expect on the domain. See below. |
dnsStatus | Roll-up: unknown, incomplete, operational (every required record verified, some optional ones missing), configured (everything verified), or misconfigured (a required record that used to work has stopped). |
dnsIssue / dnsIssueDetail | See When DNS is broken at the registrar. |
uptimeStatus | healthy, degraded, down, or empty if not checked yet. uptimeCode carries the last HTTP status seen. History is on the Monitoring API. |
Each entry in dnsRecords looks like the example above:
| Field | Meaning |
|---|---|
type | The record's role: TXT, MX, SRV, CNAME, ALIAS, plus DKIM, SPF and DMARC for the email records. |
recordType | The DNS record type you actually publish, so DKIM, SPF and DMARC all show TXT. Use this one at your DNS provider. |
name | The host label to create, relative to the zone (@ means the domain itself). |
hostname | The same thing spelled out as a full name, for checking with dig. |
status | verified, unverified (looked up, not there or wrong), or unknown (the lookup itself failed). |
required | Whether a missing value counts against dnsStatus. MX, SPF, DKIM, the ownership TXT and the pointing record are required; DMARC, the SRV records and the _acme-challenge CNAME are not. |
ttl | Always 300. |
List domains
http
GET /api/domains?page=1&limit=20&search=exampleNeeds domains:read. Paginated, newest first; search matches name, type, and status. Domains you have an open claim on are listed first, with their validationData filled in.
bash
curl "https://cloud-api.microapps.io/api/domains?limit=20" \
-H "Authorization: Bearer $MICROAPPS_API_KEY"json
{
"data": [ { "id": "6a4d5b83b7006ccae6391c41", "name": "example.com", "type": "fqdn", "status": "Mapped" } ],
"total": 1,
"page": 1,
"limit": 20,
"totalPages": 1
}Add a domain
http
POST /api/domainsNeeds domains:create, $3 of credit, and a free slot under your domain limit.
bash
curl -X POST https://cloud-api.microapps.io/api/domains \
-H "Authorization: Bearer $MICROAPPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "example.com" }'The name is the only input. The type is worked out from the name and returned in the response - you never choose it.
Because domain names are globally unique, one request has four possible outcomes:
| Status | When | What it means |
|---|---|---|
201 Created | Nobody has this domain yet | Added. The response already carries the microapps-verify TXT record to publish. |
200 OK | You already own it | Returns your existing domain, unchanged. |
428 Precondition Required | Someone else added it but never verified | You can claim it. Publish the microapps-verify TXT record within 48 hours to take it over. Body: {"message":"Requires validation"}. |
409 Conflict | Verified and locked by another account | It cannot be claimed. Body: {"message":"This domain is already verified by another account and cannot be claimed."}. |
To prove ownership, publish a TXT record at microapps-verify.example.com with the value from that record in dnsRecords. Verification is rechecked on a schedule, or on demand with revalidate. If a claim is not completed within 48 hours it is dropped.
Credit gate
Like every create action, this needs at least $3 of available credit on your account, or it returns 402.
Describe a domain
http
GET /api/domains/:idNeeds domains:read. Returns the full domain object, including the current dnsRecords set and their verification status.
bash
curl https://cloud-api.microapps.io/api/domains/6a4d5b83b7006ccae6391c41 \
-H "Authorization: Bearer $MICROAPPS_API_KEY"Domain stats
http
GET /api/domains/:id/statsNeeds domains:read. Traffic and response stats measured at the domain's public entry point:
json
{
"requests": 128400,
"status2xx": 121000,
"status3xx": 2100,
"status4xx": 5100,
"status5xx": 200,
"bytesIn": 44210000,
"bytesOut": 981200000,
"avgResponseMs": 42
}Every value is zero when the domain has no pod mapped.
Map to a pod
http
POST /api/domains/:id/mapNeeds domains:update.
| Field | Type | Notes |
|---|---|---|
trafficType | string | http or https, case-insensitive. Anything else is 400 "Traffic type must be either 'http' or 'https'". |
port | number | Required. The internal port your app listens on. Omitting it is 400 "Port is required (the port your app listens on inside the pod)". |
podId | string | The pod to forward to. Must be one of yours. |
bash
curl -X POST https://cloud-api.microapps.io/api/domains/6a4d5b83b7006ccae6391c41/map \
-H "Authorization: Bearer $MICROAPPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "trafficType": "https", "port": 3000, "podId": "6a4d5b83b7006ccae6391810" }'Response is 201 with no body; the domain moves to Configuring, then Mapped (or ConfigFailed). Poll describe to see which. You cannot map to a database pod (400 "Cannot map domain to a database pod").
Mapping adds the pointing record (category web-ingress) to dnsRecords. It always points your domain at a hostname, never at an IP address:
Domain type | Record | Name | Value |
|---|---|---|---|
subdomain | CNAME | @ | the target hostname in the record's value |
fqdn (root domain) | ALIAS | @ | the target hostname in the record's value |
A plain CNAME cannot sit at the root of a zone next to your MX, NS and TXT records, which is why a root domain gets an ALIAS - your provider may call it ANAME or CNAME flattening. If yours offers none of those, see root domains and ALIAS records.
Unmap
http
DELETE /api/domains/:id/mapNeeds domains:update. Response 204. Stops forwarding, zeroes the mapping, clears podId and uptimeStatus, and drops the pointing record from dnsRecords (the next map re-adds it, aimed at the new pod's Region). Status becomes Unmapped. Returns 400 "Domain is not mapped to a pod" if it wasn't mapped.
The SSL certificate is kept, so re-mapping later does not mean re-issuing it.
Enable SSL
http
POST /api/domains/:id/sslNeeds domains:ssl. Response 201 with no body; issuance runs in the background and hasEncryption flips to true when it lands.
bash
curl -X POST https://cloud-api.microapps.io/api/domains/6a4d5b83b7006ccae6391c41/ssl \
-H "Authorization: Bearer $MICROAPPS_API_KEY"Provisions a certificate covering the domain and its subdomains, then auto-renews it for as long as the domain exists. There is no mapping prerequisite - an unmapped domain gets its certificate now and has it installed when you map it. What is required is the domain's ssl record (the _acme-challenge CNAME in dnsRecords): issuance and every renewal run through it, so leave it published.
INFO
Enabling SSL is a sensitive action - a team member needs the domains:ssl permission to call it. Calling it on a domain that already has SSL is a no-op that still returns 201. There is no disable-SSL endpoint; unmapping keeps the certificate.
When DNS is broken at the registrar
If the domain's registry DS record no longer matches the keys its DNS host serves, validating resolvers refuse every record under the domain. We cannot verify your records and visitors cannot reach you. The domain carries it on two fields:
| Field | Value |
|---|---|
dnsIssue | "dnssec", or "" when there is no such problem. |
dnsIssueDetail | The resolver's own explanation when it gave one, e.g. "DNSKEY Missing: no SEP matching the DS found for example.com.". Often empty. |
The fix is at your registrar, not here: check DNSSEC.
Delete a domain
http
DELETE /api/domains/:idNeeds domains:delete. Returns 204. If the domain still has mailboxes, you get 400 "Cannot delete domain with active emails" - delete the mailboxes first.