# Domains and DNS API

> Ask the Falak API which domain choices a new service can use, and check whether a domain's DNS points at the right servers and which certificate is served.

Source: https://falak.sh/docs/api/domains-and-dns/

## Domain choices

A **domain choice** is `{"type": "generated" | "test" | "custom", "name"?: string}`; a plain string is a custom domain. It is accepted by `POST /api/v1/sites` (`domain`), Compose `public_services[].domain`, and template deploys (`domains.<service>`). A service without a choice gets the organization default: the test domain when `FALAK_TEST_DOMAIN` is set, else a generated name, else a domain is required.

| Type | Result |
|---|---|
| `generated` | `<label>.<ipv4-with-dashes>.<suffix>`, e.g. `minio-files.63-182-218-247.sslip.io`. Label: the site slug (Compose: `<service>-<slug>`). IP: the leader's public IPv4, or the load balancer's. `422` when generated names are off or the server has no public IPv4 yet. |
| `test` | `<slug>.` (Compose: `<service>-<slug>.…` after the first service) |
| `custom` | Your domain, with automatic TLS once DNS points at the server |

## `GET /api/v1/domains/options` — `edge.view`

What a create form offers for a set of servers, or for an existing site.

```text
GET /api/v1/domains/options?server=01k…,01k…
GET /api/v1/domains/options?server[]=01k…&server[]=01k…
GET /api/v1/domains/options?site=shop
```

```json title="200 OK"
{"data": {
  "test_domain": null,
  "generated": {"suffix": "sslip.io", "ipv4": "63.182.218.247", "target": "app-1", "available": true, "reason": null},
  "default": "generated",
  "targets": [{"server_id": "01k…", "name": "app-1", "ipv4": "63.182.218.247", "ipv6": null, "load_balancer": false}]
}}
```

`targets` lists where DNS must point: the site's load balancer, else each server, leader first.

## `GET /api/v1/dns/check` — `edge.view`

Resolves a name from the control plane and compares it with the targets. Rate limited to 60/minute.

```text
GET /api/v1/dns/check?name=shop.example.com&server=01k…
GET /api/v1/dns/check?name=shop.example.com&site=shop&tls=1
```

The name is resolved over DNS-over-HTTPS (`FALAK_DNS_RESOLVER=doh`, default resolver `https://cloudflare-dns.com/dns-query`) or the system resolver (`system`), with a 3-second timeout.

```json title="200 OK"
{"data": {
  "name": "shop.example.com", "status": "ok", "message": "Points to app-2 (63.182.218.247)",
  "addresses": ["63.182.218.247"], "cnames": [], "targets": [], "matched": [],
  "instructions": {"zone": "example.com", "host": "shop", "apex": false, "ttl": 300,
    "records": [{"type": "A", "name": "shop.example.com", "host": "shop", "value": "63.182.218.247", "target": "app-2"}],
    "alternative": {"type": "CNAME", "host": "shop", "value": "shop.63-182-218-247.sslip.io"}, "notes": ["…"]},
  "certificate": null, "checked_at": "2026-09-28T12:00:00+00:00"}}
```

| `status` | Meaning |
|---|---|
| `ok` | Every address is a target |
| `mismatch` | Resolves elsewhere ("Resolves to 1.2.3.4 — expected …"), or has extra records to remove |
| `proxied` | Cloudflare proxy addresses; HTTP-01 fails until the record is "DNS only" |
| `missing` | No A/AAAA record yet |
| `error` | Lookup failed, invalid name, or no server IP to compare with |

`instructions` lists the records to add: an `A` per target IPv4, an `AAAA` per IPv6, apex vs subdomain, and for subdomains of single-target sites a `CNAME` to the generated name as an alternative.

With `site` and `tls=1`, `certificate` reports what the site's server serves for the name — `{status: issued|pending, issuer, expires_at, message}` — probed only when the name already points at the site.

## Rate limits

`GET` (`edge.view`), `PUT` and `DELETE` (`edge.manage`) on `/api/v1/sites/{site}/domains/{domain}/rate-limit`: a domain's Cloudflare rate limit. `{domain}` is the domain's id or name. Rate limited to 30/minute. See [Cloudflare → Rate limits](/docs/guides/cloudflare/#rate-limits).

```json title="GET → 200 OK"
{"data": {
  "domain": "shop.example.com", "rule": {"path": "/login", "requests": 20, "period": 10, "action": "block", "timeout": 10},
  "zone": "example.com", "proxied": true,
  "limits": {"plan": "free", "rules": 1, "host": false, "periods": [10], "timeouts": [10], "challenge_timeout": false, "note": "…"},
  "zone_rule": null}}
```

- `limits` is `null` outside a managed zone. `zone_rule` (`{domain, path}`) is another domain's Free-plan rule that applies to this domain too.
- `PUT {path?, requests, period, action: block|managed_challenge, timeout}` writes the zone's rules and returns the same shape. For `managed_challenge` with `challenge_timeout: false` (below Enterprise), `timeout` is ignored and stored as `0`.
- `422` when the domain isn't proxied, the plan doesn't allow the window or duration or has no rule left, or Cloudflare refuses (the token needs Zone → Zone WAF → Edit).
- `DELETE` removes the rule.
