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
{
"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
curl https://api.thunderphone.com/v1/client-portals \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"The list is sorted by name, then id.
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. |
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.
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:
{
"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:
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:
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:
{
"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:
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.