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

# Client Portals

> Create and administer branded client portals over the ThunderPhone API, including agent scope, viewers, logos, custom domains, and DNS verification.

Client portals give external viewers a branded, read-only call-history
experience for a selected set of agents. The management API is organization
scoped and requires an **admin or owner** authenticated with an `sk_live_` key.

Viewer login, sessions, call browsing, transcripts, and audio use the hosted
portal's separate viewer authentication. They are not `sk_live_` integration
endpoints.

## Endpoints

| Method             | Path                                                        | Description                                              |
| ------------------ | ----------------------------------------------------------- | -------------------------------------------------------- |
| `GET`              | `/v1/client-portals`                                        | List portals.                                            |
| `POST`             | `/v1/client-portals`                                        | Create a portal.                                         |
| `GET`              | `/v1/client-portals/{portal_id}`                            | Retrieve a portal.                                       |
| `PATCH`            | `/v1/client-portals/{portal_id}`                            | Update details, active state, branding color, or agents. |
| `DELETE`           | `/v1/client-portals/{portal_id}`                            | Permanently delete a portal.                             |
| `POST`             | `/v1/client-portals/{portal_id}/logo`                       | Upload or replace a logo.                                |
| `DELETE`           | `/v1/client-portals/{portal_id}/logo`                       | Remove the logo.                                         |
| `GET` / `POST`     | `/v1/client-portals/{portal_id}/viewers`                    | List or add viewers.                                     |
| `PATCH` / `DELETE` | `/v1/client-portals/{portal_id}/viewers/{viewer_id}`        | Activate/deactivate or delete a viewer.                  |
| `GET` / `POST`     | `/v1/client-portals/{portal_id}/domains`                    | List or add custom domains.                              |
| `DELETE`           | `/v1/client-portals/{portal_id}/domains/{domain_id}`        | Remove a custom domain.                                  |
| `POST`             | `/v1/client-portals/{portal_id}/domains/{domain_id}/verify` | Check the ownership TXT record.                          |

## Portal object

```json theme={null}
{
  "id": 18,
  "name": "Acme Dental",
  "slug": "acme-dental",
  "accent_color": "#2e8fff",
  "is_active": true,
  "logo_url": "https://…",
  "agent_ids": [12, 19],
  "agent_count": 2,
  "viewer_count": 3,
  "calls_last_30d": 147,
  "created_at": "2026-07-16T18:24:10.113Z",
  "updated_at": "2026-08-05T09:40:12.551Z"
}
```

| Field                         | Type             | Description                                              |
| ----------------------------- | ---------------- | -------------------------------------------------------- |
| `id`                          | integer          | Portal id used by management endpoints.                  |
| `name`                        | string           | Client-facing name, maximum 255 characters.              |
| `slug`                        | string           | Globally unique hosted URL slug, maximum 100 characters. |
| `accent_color`                | string           | Empty for the default, or a full six-digit hex color.    |
| `is_active`                   | boolean          | Inactive portals cannot be opened by viewers.            |
| `logo_url`                    | string \| null   | Temporary or hosted URL for the current logo.            |
| `agent_ids`                   | array of integer | Agents whose calls are in the portal scope.              |
| `agent_count`, `viewer_count` | integer          | Current related-resource counts.                         |
| `calls_last_30d`              | integer          | Calls from assigned agents in the preceding 30 days.     |
| `created_at`, `updated_at`    | timestamp        | ISO 8601 timestamps.                                     |

## List and create portals

```bash theme={null}
curl https://api.thunderphone.com/v1/client-portals \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```

The list is sorted by name, then id.

```bash theme={null}
curl -X POST https://api.thunderphone.com/v1/client-portals \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Dental","slug":"acme-dental"}'
```

| Field  | Required | Validation                                                                                                                                                         |
| ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | yes      | Non-empty after trimming; maximum 255 characters.                                                                                                                  |
| `slug` | no       | Maximum 100 characters. Lowercase letters, numbers, and single hyphens only; must be unique and not reserved. A unique slug is generated from `name` when omitted. |

Returns `201 Created` with the portal object.

## Retrieve, update, and delete

`PATCH /v1/client-portals/{portal_id}` accepts any subset of:

| Field          | Type             | Behavior                                                                                                                                   |
| -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`         | string           | Replaces the client-facing name.                                                                                                           |
| `slug`         | string           | Replaces the hosted URL slug after uniqueness validation.                                                                                  |
| `is_active`    | boolean          | Enables or disables all viewer access.                                                                                                     |
| `accent_color` | string           | Six-digit hex such as `#2e8fff`, or `""` for the default.                                                                                  |
| `agent_ids`    | array of integer | Replaces the complete assigned-agent set. Duplicates are removed; every id must belong to the organization. Use `[]` for no visible calls. |

```bash theme={null}
curl -X PATCH https://api.thunderphone.com/v1/client-portals/18 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_ids":[12,19],"accent_color":"#2e8fff"}'
```

`DELETE` returns `204 No Content` and permanently removes the portal, its
viewers, domain records, and logo reference. It does not delete agents or calls.

## Upload or remove a logo

Upload multipart form field `file`. The accepted formats are **PNG**, **JPEG**,
and **WebP**, with a maximum size of **2 MB**. The file contents must match the
declared MIME type.

```bash theme={null}
curl -X POST https://api.thunderphone.com/v1/client-portals/18/logo \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -F "file=@acme-logo.png"
```

The response is `{"logo_url":"https://…"}`. Uploading a new logo replaces
the old one. `DELETE` removes it and returns `204`.

## Viewers

The viewer object is:

```json theme={null}
{
  "id": 31,
  "email": "client@example.com",
  "is_active": true,
  "has_password": false,
  "created_at": "2026-08-05T09:40:12.551Z",
  "updated_at": "2026-08-05T09:40:12.551Z"
}
```

Add a unique email address:

```bash theme={null}
curl -X POST https://api.thunderphone.com/v1/client-portals/18/viewers \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"client@example.com"}'
```

Returns `201`. `PATCH` accepts only `is_active`; setting it to `false` blocks
login without deleting the viewer. `DELETE` returns `204`.

## Custom domains

Create a domain with its hostname only:

```bash theme={null}
curl -X POST https://api.thunderphone.com/v1/client-portals/18/domains \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"calls.client.com"}'
```

Returns `201`:

```json theme={null}
{
  "id": 7,
  "hostname": "calls.client.com",
  "verification_token": "tp_verify_…",
  "status": "pending",
  "verified_at": null,
  "cname_record": {
    "name": "calls.client.com",
    "value": "CNAME_TARGET_FROM_RESPONSE"
  }
}
```

The hostname is normalized, can be at most 253 characters, and must be unique
across portals. Add the returned CNAME, then add this TXT record:

| Type | Name                                 | Value                              |
| ---- | ------------------------------------ | ---------------------------------- |
| TXT  | `_tp-portal-verify.calls.client.com` | The returned `verification_token`. |

Check it with:

```bash theme={null}
curl -X POST https://api.thunderphone.com/v1/client-portals/18/domains/7/verify \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```

On success, the domain object has `status: "active"` and `verified_at`. While
the TXT record is not visible, the response remains pending and repeats the
expected TXT and CNAME records. DNS propagation can take several minutes.

`DELETE /domains/{domain_id}` returns `204` and releases the hostname for reuse.

## Errors and authorization

| Status | Meaning                                                                  |
| ------ | ------------------------------------------------------------------------ |
| `400`  | Invalid slug, color, agent list, upload, email, or hostname.             |
| `401`  | Missing or invalid API key.                                              |
| `403`  | The authenticated user or key is not authorized for admin management.    |
| `404`  | Portal or nested resource is outside the current organization or absent. |
| `204`  | Successful deletion.                                                     |

For the dashboard workflow and viewer experience, see
[Client portals](/guides/client-portals).
