# Domains API
URL: https://openship.io/docs/api/domains.md

Attach custom domains to a project, preview and verify DNS, and manage SSL certificates.

import { TypeTable } from 'fumadocs-ui/components/type-table';

The Domains API attaches web addresses (like `app.example.com`) to a project, checks that the DNS is set up
correctly, and manages the HTTPS certificate. In the dashboard this is the project's **Domains** tab; from
the terminal it's the [`openship domain`](/docs/cli/projects) command.

<Callout title="Base path & auth">
All paths are relative to your instance, under **`/api`** — e.g. `https://your-host/api/domains`.
Send a personal access token as a bearer header (`Authorization: Bearer <token>`), created with
[`openship token create`](/docs/cli/access). The dashboard uses your session cookie instead. See the
[API overview](/docs/api) for the full auth model.
</Callout>

## Endpoints

| Method & path | Permission | What it does |
|---|---|---|
| `GET /api/domains` | `domain:list` | List domains (optionally `?projectId=`). |
| `POST /api/domains` | `domain:write` | Add a domain to a project. |
| `POST /api/domains/preview` | `domain:read` | Preview the DNS records a hostname will need — creates nothing. |
| `DELETE /api/domains/:id` | `domain:admin` | Remove a domain. |
| `POST /api/domains/:id/verify` | `domain:write` | Verify ownership / DNS for a domain. |
| `POST /api/domains/:id/primary` | `domain:write` | Make this the project's primary domain. |
| `GET /api/domains/:id/records` | `domain:read` | Get the DNS records for a domain. |
| `POST /api/domains/:id/renew` | `domain:write` | Renew the SSL certificate. |
| `POST /api/domains/:id/verify-ssl` | `domain:write` | Check the SSL certificate. |
| `POST /api/domains/renew-all` | `domain:write` | Renew every certificate that's due. |
| `POST /api/domains/verify-pending` | `domain:write` | Re-verify all domains still pending. |

## Add a domain

```
POST /api/domains
```

<TypeTable
  type={{
    projectId: { type: 'string', description: 'The project to attach the domain to.', required: true },
    hostname: { type: 'string', description: 'The fully-qualified domain, e.g. app.example.com.', required: true },
    isPrimary: { type: 'boolean', description: "Make this the project's primary domain.", default: 'false' },
    externalIngress: { type: 'boolean', description: 'TLS is terminated upstream (Cloudflare Tunnel, a load balancer). Openship verifies ownership by DNS TXT only, does not require the domain to resolve to this box, and issues no certificate.', default: 'false' },
  }}
/>

```bash
curl -X POST https://your-host/api/domains \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"proj_123","hostname":"app.example.com"}'
```

```bash
# Same thing from the CLI:
openship domain add app.example.com
```

<Callout title="Adding a domain doesn't make it live by itself" type="info">
After adding, you must point your DNS at Openship and then **verify**. Use `POST /api/domains/preview`
(or `openship domain preview <hostname>`) to see the exact records to add at your DNS provider first.
</Callout>

## Preview required DNS

Side-effect-free: pass a hostname, get back the records to create. `POST` is used only so the hostname
travels in the body.

```
POST /api/domains/preview
```

```bash
curl -X POST https://your-host/api/domains/preview \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"app.example.com"}'
```

The response's `mode` tells you which setup applies: `selfhosted` (an A record to your server), `cloud`
(a CNAME to Openship Cloud), or `external` (a TXT record only — you front TLS yourself).

## Verify a domain

Once DNS is in place, verify it. Openship checks the routing record and the ownership TXT record, marks the
domain active, and (unless it's external-ingress) provisions SSL in the background.

```
POST /api/domains/:id/verify
```

```bash
openship domain verify <domainId>
```

## Errors you might see

<Callout title="422 — verification failed" type="warn">
The DNS records aren't resolving yet, or don't match. DNS can take minutes to hours to propagate. Confirm
the records match `preview` exactly, then retry. The response's `cnameVerified` / `txtVerified` flags tell
you which check is still failing.
</Callout>

<Callout title="404 on a per-domain route" type="error">
The `:id` doesn't belong to your organization (or was deleted). List your domains with `GET /api/domains`
to get valid IDs.
</Callout>
