> ## Documentation Index
> Fetch the complete documentation index at: https://www.unkey.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unkey is two separate products. Compute builds, deploys, and runs apps behind a gateway. API Management issues API keys, enforces rate limits, manages identities and permissions, and reports usage. Say which product a page belongs to; a reader can use either without the other.
> Every Unkey API endpoint is an HTTP POST to https://api.unkey.com/v2/{service}.{procedure} with a root key in the Authorization: Bearer header. Root keys are workspace scoped.
> Error codes have the form err:{system}:{category}:{specific} and each has a page at /errors/{system}/{category}/{specific}.
> The word environment means production or preview in Compute. Rate limiting has four meanings on this site; the glossary lists them.

# Portal custom domains

> Serve your developer portal from a hostname you own.

A portal custom domain serves your [developer portal](https://www.unkey.com/docs/api-management/portal/overview.md) from a hostname you own, such as `developers.acme.com`, instead of Unkey's portal host. You attach the domain to a portal, publish two DNS records, and Unkey verifies them, routes the hostname to your portal, and issues a certificate for it.

## Before you start

A portal domain follows the same naming rules as a [Compute custom domain](https://www.unkey.com/docs/compute/networking/custom-domains.md).

* Each name can be attached once in your workspace, to one portal or one environment, not both.
* The name must be under a registrable domain. `developers.acme.co.uk` works, but `co.uk` on its own doesn't. Wildcards and IP addresses aren't allowed.
* A workspace can hold up to 10 portal domains.
* Port 80 must reach Unkey as well as 443, so Let's Encrypt can check the name before issuing a certificate.

## Attach a domain

> [!NOTE]
>
> Create a root key with the permissions listed on this page under **Root Keys** in the dashboard sidebar. CLI commands can instead use a session from [unkey login](https://www.unkey.com/docs/platform/cli/login.md) with the required permissions. See [Permission reference](https://www.unkey.com/docs/platform/root-keys/permissions-legacy.md) for every permission.

Portal domains have their own permission, under the portal they belong to:

| Action | Permission |
| - | - |
| Attach a domain | `unkey:v1:<workspaceId>:projects/<projectId>/portals/<portalId>/domains/*#write` |
| Read or list domains | `unkey:v1:<workspaceId>:projects/<projectId>/portals/<portalId>/domains/*#read` |
| Retry verification | `unkey:v1:<workspaceId>:projects/<projectId>/portals/<portalId>/domains/<domainId>#write` |
| Remove a domain | `unkey:v1:<workspaceId>:projects/<projectId>/portals/<portalId>/domains/<domainId>#delete` |

Permission on the portal itself doesn't cover its domains. Without the permission, the request fails with 404, or with 403 naming the missing permission if your root key can read the portal.

### 1. Add the domain

Call `portal.createDomain` with the portal's ID or slug and the hostname:

```bash theme={"system"}
curl -X POST https://api.unkey.com/v2/portal.createDomain \
  -H "Authorization: Bearer <root key>" \
  -H "Content-Type: application/json" \
  -d '{ "portal": "acme-portal", "domain": "developers.acme.com" }'
```

The response has the domain's `pdom_` ID and the `dnsRecords` to publish. If your DNS provider supports [Domain Connect](https://www.domainconnect.org/), it also has `domainConnect.url`, a link that creates the records at your provider in one step. `portal.getDomain` returns the same link.

### 2. Publish the DNS records

Create both records at your DNS provider, exactly as `dnsRecords` lists them, with a TTL of 60 seconds:

| Type | Name | Value | When |
| - | - | - | - |
| `CNAME` | `developers.acme.com` | the target from `dnsRecords` | Subdomains |
| `ALIAS` | `acme.com` | the same target | Apex domains |
| `TXT` | `_unkey.developers.acme.com` | `unkey-domain-verify=<token>` | Always |

`ALIAS` means whatever your provider calls an alias at the apex: ALIAS, ANAME, or a flattened CNAME. If your provider proxies traffic, create the routing record as DNS-only.

### 3. Wait for verification

Unkey checks DNS about once a minute for up to 24 hours. Poll `portal.getDomain` with the portal and the `domainId` to follow it. The status goes from `pending` to `verifying` to `verified`, and each record has a `verified` flag showing whether Unkey has found it.

Once the domain is verified, it serves your portal, and Unkey requests a Let's Encrypt certificate for it under your workspace. Certificates renew automatically.

`portal.listDomains` returns every domain on a portal, verified or not.

## Send users to your domain

`portal.createSession` returns a URL on Unkey's portal host. To keep users on your domain, replace the host in that URL with your domain before you redirect them. The rest of the URL, including the one-time code, stays the same. See [Portal sessions](https://www.unkey.com/docs/api-management/portal/sessions.md).

## Verification failed

After 24 hours without the right records, the status becomes `failed` and `verificationError` says why. Fix the records, then call `portal.verifyDomain` to start a new 24 hour window. Calling it on a domain that's already verified succeeds and does nothing.

Things to check:

* Publish both records, even for a subdomain. A subdomain can pass with the CNAME alone, but an apex domain or a record your provider proxies or flattens can only pass with the TXT record.
* Copy values exactly. Unkey compares them character for character.

## Move a domain from another workspace

If another workspace has already verified the name, publish the TXT record. When your verification passes, the domain moves to your workspace, and the other workspace's claim is marked `failed`.

## Remove a domain

Call `portal.deleteDomain` with the portal and the `domainId`. A verified domain stops serving your portal right away. The certificate isn't revoked, and you remove the DNS records yourself.

Deleting a portal with `portal.deletePortal` removes its domains the same way.

Adding, verifying, and removing a domain write `portal.domain.create`, `portal.domain.verify`, and `portal.domain.delete` [audit log](https://www.unkey.com/docs/platform/workspace/audit-logs/event-types.md) entries.

## Errors

| Status | Code | Meaning |
| - | - | - |
| 404 | `err:unkey:data:portal_not_found` | The portal does not exist, is not in your workspace, or your root key can't read it or its domains. |
| 403 | `err:unkey:authorization:insufficient_permissions` | Your root key can read the portal but lacks the domain permission. The error names it. |
| 404 | `err:unkey:data:domain_not_found` | The domain is not attached to that portal. |
| 409 | `err:unkey:data:domain_already_exists` | The name is already attached to a portal or an environment in your workspace. |
| 403 | `err:unkey:limits:portal_domain_limit_exceeded` | The workspace already holds 10 portal domains. |
| 400 | `err:unkey:application:invalid_input` | The name is malformed, a wildcard, an IP address, or not under a registrable domain. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.