# Create domains — POST /v1/project/{slug}/domains

URL: https://docs.hanzo.ai/docs/openapi/project/post-project-by-slug-domains

> Attaches one or more CUSTOM public hostnames to this org's site.

`POST /v1/project/{slug}/domains`

> [Project →](https://docs.hanzo.ai/docs/openapi/project) · [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|               |                                                  |
| ------------- | ------------------------------------------------ |
| **Address**   | `https://api.hanzo.ai/v1/project/{slug}/domains` |
| **Method**    | `POST`                                           |
| **Operation** | `post_project_by_slug_domains`                   |
| **Auth**      | `Authorization: Bearer $HANZO_API_KEY`           |

Attaches one or more CUSTOM public hostnames to this org's site.

Binding a host you do not own would let you shadow it at the edge, so which
outcome you get depends on whether ownership is already established: a SuperAdmin
vouches (the operator manages the customer's DNS, so its bind IS the proof) and
binds VERIFIED immediately; every other caller, INCLUDING an admin of the
deployment's own brand org, has the host CLAIMED as pending and gets the DNS
challenge back in `bound[].records`. A pending claim HOLDS the name so nobody
else can take it, but it does not route until POST .../domains/{host}/verify
proves control.

A hostname we operate is refused to a non-vouched caller (those are assigned
by the platform, never claimed), a host another site already holds is a 409,
and a name the platform holds is a 400 for EVERY caller — a vouch skips the
ownership proof, never the host table's own invariant. Claims and binds are
idempotent for the same
(org, slug), and re-claiming returns the SAME token rather than invalidating a
record the customer has already published. The edge cache-tag is flushed
afterwards so a newly-verified host serves the current build immediately.

Scope: a validated principal is required (401 without one) and the site is
resolved within that principal's org, so another tenant's slug is a 404.

## Request

3 fields, body `application/json` (required).

| Field     | In   | Type      | Required | Description                                           |
| --------- | ---- | --------- | -------- | ----------------------------------------------------- |
| `slug`    | path | string    | yes      | Slug is the site the hosts attach to, from the path.  |
| `domains` | body | string\[] | —        | Domains are the custom hostnames to attach, in order. |
| `slug`    | body | string    | —        | Slug is the site the hosts attach to, from the path.  |

## Response

| Status    | Body                         | Meaning |
| --------- | ---------------------------- | ------- |
| `200`     | project.projectsBoundDomains | ok      |
| `default` | problem-details              | refused |

`200` body — 14 fields.

| Field                     | In   | Type                      | Always | Description                                                                                                                                             |
| ------------------------- | ---- | ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bound`                   | body | project.projectsDomain\[] | —      | Bound is the result of THIS call, one row per host in the request: live for an already-vouched host, pending with the DNS records to publish otherwise. |
| `bound[].createdAt`       | body | integer (int64)           | —      | CreatedAt is when the host was claimed, as Unix seconds — not when it went live.                                                                        |
| `bound[].detail`          | body | string                    | —      | Detail is what is holding the claim up, in words a person can act on.                                                                                   |
| `bound[].host`            | body | string                    | —      | Host is the custom hostname claimed for this site.                                                                                                      |
| `bound[].records`         | body | project.Record\[]         | —      | Records are EXACTLY the DNS records to publish to prove ownership and route the host.                                                                   |
| `bound[].records[].name`  | body | string                    | —      | the record name the customer creates                                                                                                                    |
| `bound[].records[].type`  | body | string                    | —      | TXT \| CNAME                                                                                                                                            |
| `bound[].records[].value` | body | string                    | —      | the record value                                                                                                                                        |
| `bound[].status`          | body | string                    | —      | Status is `live` when the edge answers for this host now, `pending` while the claim is waiting on DNS proof of ownership.                               |
| `bound[].url`             | body | string                    | —      | URL is where the host will serve once it is live — present on a pending claim too, so a console can show the destination before it works.               |
| `bound[].verified`        | body | boolean                   | —      | Verified is the same fact as a boolean, for a caller that only needs the yes or no.                                                                     |
| `domains`                 | body | string\[]                 | —      | Domains are the hostnames that are VERIFIED and routing right now, after this bind.                                                                     |
| `org`                     | body | string                    | —      | Org is the organisation that owns the site.                                                                                                             |
| `slug`                    | body | string                    | —      | Slug is the site the hosts were bound to.                                                                                                               |

Failure carries the platform error shape — see [Errors](https://docs.hanzo.ai/docs/errors).

## Examples

**CLI**

```bash
hanzo project domains create <slug>
```

**SDK**

**.ts**

```ts
import { Configuration, ProjectApi } from 'hanzoai';

const api = new ProjectApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postProjectBySlugDomains({ slug: 'slug', domains: ["<domains>"], slug: "<slug>" });
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ProjectApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ProjectApi(client).post_project_by_slug_domains(slug='slug', domains=["<domains>"], slug="<slug>")
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.ProjectAPI.PostProjectBySlugDomains(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, project_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = project_api::post_project_by_slug_domains(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ProjectApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new ProjectApi(client).postProjectBySlugDomains();
```

**HTTP**

```bash
curl -X POST https://api.hanzo.ai/v1/project/<slug>/domains \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "domains": [
         "<domains>"
       ],
       "slug": "<slug>"
     }'
```

**MCP**

Tool `project`, op `post_project_by_slug_domains` — POST the JSON-RPC envelope to `https://api.hanzo.ai/v1/mcp`.

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "project",
         "arguments": {
           "op": "post_project_by_slug_domains",
           "input": {
             "slug": "<slug>",
             "domains": [
               "<domains>"
             ]
           }
         }
       }
     }'
```

---

[Project API](https://docs.hanzo.ai/docs/openapi/project) · [All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
