# Clients

> Group zones by the end customers of an agency and limit members and tokens to one client's zones.

Clients are the end customers of an agency. Each zone may belong to one client, and members, invitations and API tokens limited to a client only see that client's zones. These routes have no SDK methods yet; the examples use curl.

| Operation | Endpoint |
|---|---|
| [List clients](https://developers.openwaf.dev/api/clients/#list-clients) | `GET /v1/orgs/{org}/clients` |
| [Create client](https://developers.openwaf.dev/api/clients/#create-client) | `POST /v1/orgs/{org}/clients` |
| [Get client](https://developers.openwaf.dev/api/clients/#get-client) | `GET /v1/orgs/{org}/clients/{client}` |
| [Update client](https://developers.openwaf.dev/api/clients/#update-client) | `PATCH /v1/orgs/{org}/clients/{client}` |
| [Delete client](https://developers.openwaf.dev/api/clients/#delete-client) | `DELETE /v1/orgs/{org}/clients/{client}` |
| [Set zone client](https://developers.openwaf.dev/api/clients/#set-zone-client) | `PATCH /v1/orgs/{org}/zones/{zone}` |

## List clients

```http
GET /v1/orgs/{org}/clients
```

Lists the clients of the organization with their number of zones.

**Required role:** `readonly` or higher. A token limited to a client only sees its own client.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |

#### Response

- `200`: Returns `items`, an array of [Client](https://developers.openwaf.dev/api/schemas/#client).

| Field | Type | Description |
|---|---|---|
| `items[].id` | string (uuid) | Id. |
| `items[].org_id` | string (uuid) | Organization id. |
| `items[].name` | string | Name of the client, unique in the organization. |
| `items[].contact_email` | string | Contact email (may be empty). |
| `items[].note` | string | Internal note of the agency. |
| `items[].zone_count` | integer | Number of zones of the client (in any status). |
| `items[].created_at` | string (date-time) | Creation time. |
| `items[].updated_at` | string (date-time) | Last change. |

#### Errors

| Status | Description |
|---|---|
| `403` | The token's role or limits do not allow it (`forbidden`). |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/clients" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

```json
{
  "items": [
    {
      "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
      "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
      "name": "example.com",
      "contact_email": "it@acme.example",
      "note": "Managed by the platform team",
      "zone_count": 128,
      "created_at": "2026-10-27T09:30:00Z",
      "updated_at": "2026-10-27T09:30:00Z"
    }
  ]
}
```

## Create client

```http
POST /v1/orgs/{org}/clients
```

Creates a client. `name` is required and unique in the organization (case-insensitive).

**Required role:** `tech` or higher. Organization-wide.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |

#### Request body

Content type `application/json`. Schema: [ClientInput](https://developers.openwaf.dev/api/schemas/#client-input).

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | No | Required when creating; no commas or line breaks. At most 200 characters. |
| `contact_email` | string (email) | No | At most 320 characters. |
| `note` | string | No | At most 2000 characters. |

#### Response

- `201`: Created. Returns [Client](https://developers.openwaf.dev/api/schemas/#client).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name of the client, unique in the organization. |
| `contact_email` | string | Contact email (may be empty). |
| `note` | string | Internal note of the agency. |
| `zone_count` | integer | Number of zones of the client (in any status). |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `409` | `conflict`: a client with that name already exists. |
| `422` | Validation error (`invalid`). |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/clients" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "Acme Retail",
    "contact_email": "it@acme.example"
  }'
```

```json
{
  "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "contact_email": "it@acme.example",
  "note": "Managed by the platform team",
  "zone_count": 128,
  "created_at": "2026-10-27T09:30:00Z",
  "updated_at": "2026-10-27T09:30:00Z"
}
```

## Get client

```http
GET /v1/orgs/{org}/clients/{client}
```

Returns one client.

**Required role:** `readonly` or higher.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `client` | path | string (uuid) | Yes | Client id. |

#### Response

- `200`: Returns [Client](https://developers.openwaf.dev/api/schemas/#client).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name of the client, unique in the organization. |
| `contact_email` | string | Contact email (may be empty). |
| `note` | string | Internal note of the agency. |
| `zone_count` | integer | Number of zones of the client (in any status). |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |

#### Errors

| Status | Description |
|---|---|
| `404` | Not found, or not visible to the token (`not_found`). |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/clients/7c1e3a5d-9b2f-4d6a-8e0c-2f4a6c8e0b1d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

```json
{
  "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "contact_email": "it@acme.example",
  "note": "Managed by the platform team",
  "zone_count": 128,
  "created_at": "2026-10-27T09:30:00Z",
  "updated_at": "2026-10-27T09:30:00Z"
}
```

## Update client

```http
PATCH /v1/orgs/{org}/clients/{client}
```

Changes the name, contact email or note of a client. Fields left out keep their value.

**Required role:** `admin` or higher. Organization-wide.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `client` | path | string (uuid) | Yes | Client id. |

#### Request body

Content type `application/json`. Schema: [ClientInput](https://developers.openwaf.dev/api/schemas/#client-input).

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | No | Required when creating; no commas or line breaks. At most 200 characters. |
| `contact_email` | string (email) | No | At most 320 characters. |
| `note` | string | No | At most 2000 characters. |

#### Response

- `200`: Returns [Client](https://developers.openwaf.dev/api/schemas/#client).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name of the client, unique in the organization. |
| `contact_email` | string | Contact email (may be empty). |
| `note` | string | Internal note of the agency. |
| `zone_count` | integer | Number of zones of the client (in any status). |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `409` | `conflict`: a client with that name already exists. |
| `422` | Validation error (`invalid`). |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl -X PATCH "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/clients/7c1e3a5d-9b2f-4d6a-8e0c-2f4a6c8e0b1d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "note": "Renewal in March"
  }'
```

```json
{
  "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "contact_email": "it@acme.example",
  "note": "Managed by the platform team",
  "zone_count": 128,
  "created_at": "2026-10-27T09:30:00Z",
  "updated_at": "2026-10-27T09:30:00Z"
}
```

## Delete client

```http
DELETE /v1/orgs/{org}/clients/{client}
```

Deletes a client without zones. Move its zones to another client (or to none) with [Set zone client](https://developers.openwaf.dev/api/clients/#set-zone-client), or delete them, first.

**Required role:** `admin` or higher. Organization-wide.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `client` | path | string (uuid) | Yes | Client id. |

#### Response

- `204`: Deleted. No body.

#### Errors

| Status | Description |
|---|---|
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `409` | `client_has_zones`: the client still has zones; move them to another client or delete them first. |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl -X DELETE "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/clients/7c1e3a5d-9b2f-4d6a-8e0c-2f4a6c8e0b1d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

## Set zone client

```http
PATCH /v1/orgs/{org}/zones/{zone}
```

Assigns a zone to a client, or to none with `null`. The client is part of the zone's configuration, so the change creates a new version.

**Required role:** `tech` or higher. Organization-wide.

**Change controls:** [`dry_run`](https://developers.openwaf.dev/api/#configuration-changes) · [`If-Match`](https://developers.openwaf.dev/api/#configuration-changes) (optional) · [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (required)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |
| `X-Change-Reason` | header | string | Yes | Reason of the change: 1-500 UTF-8 characters after trimming spaces, no control characters. If it is missing or invalid: 422 `reason_required`. 1 to 500 characters. |
| `If-Match` | header | string | No | Expected configuration version, e.g. `"12"` (`W/` accepted). If it is not the current one: 412 `version_conflict`. |
| `dry_run` | query | string | No | `1`/`true` simulates the change: answers 200 with the plan (`Applied`) and saves nothing. `0`/`false` accepted; any other value: 400. One of `1`, `true`, `0`, `false`. |

#### Request body

Content type `application/json`.

| Field | Type | Required | Description |
|---|---|---|---|
| `client_id` | string (uuid) or null | Yes | Client id, or null so that the zone belongs to no client. |

#### Response

- `200`: Zone changed (with X-Config-Version and ETag) or, with `dry_run`, the plan (`Applied`). Returns [Zone](https://developers.openwaf.dev/api/schemas/#zone) or [Applied](https://developers.openwaf.dev/api/schemas/#applied).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name. |
| `status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `ns_pair` | array of string | Name servers the delegated zones use. Exactly 2 items. |
| `delegation_checked_at` | string (date-time) | Last delegation check. |
| `delegation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | Last check: CheckResult (delegated) or VerifyResult (external). |
| `activated_at` | string (date-time) | When the zone became active. |
| `moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `disabled_at` | string (date-time) | When the zone was disabled. |
| `verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `cname_target` | string | External zones only: CNAME target for each site. |
| `edge_addresses` | [EdgeAddresses](https://developers.openwaf.dev/api/schemas/#edge-addresses) |  |
| `config_version` | integer | Current configuration version (also in ETag). |
| `proxied_names` | integer | Names with a proxied record. |
| `merged_into` | string (uuid) | Merged zones only (status disabled): the parent zone. |
| `merged_at` | string (date-time) | Merged zones only; they are purged after 14 days. |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |
| `client_id` | string (uuid) or null | Client of the organization the zone belongs to; null = none. |

Response headers: `ETag`, `X-Config-Version`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `412` | `version_conflict`: If-Match does not match the current version. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |

#### Example

No SDK method yet: use the HTTP request.

```sh
curl -X PATCH "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Move the zone to its client" \
  -H 'If-Match: "12"' \
  -H "Content-Type: application/json" \
  --data '{
    "client_id": "7c1e3a5d-9b2f-4d6a-8e0c-2f4a6c8e0b1d"
  }'
```

```json
{
  "id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "status": "active",
  "dns_mode": "delegated",
  "ns_pair": [
    "ns1.example-dns.net",
    "ns2.example-dns.net"
  ],
  "delegation_checked_at": "2026-10-27T09:00:00Z",
  "activated_at": "2026-03-02T11:40:00Z",
  "config_version": 12,
  "proxied_names": 3,
  "client_id": null,
  "created_at": "2026-03-02T10:20:00Z",
  "updated_at": "2026-10-27T09:00:00Z"
}
```
