# Contact Channels Panel
URL: /docs/web/docyrus/contact-channels-panel
A Docyrus-backed panel for managing a record's contact channels, the append-only consent ledger and the validation engine.
`ContactChannelsPanel` wires the full **Contact Channels & Consent API** to a single component, laid
out as a **Contact Points & Consent** detail view: a preferred email / phone **summary**, grouped
**Emails** / **Phones** (and messaging / social / web when present) cards showing each channel's
validation status, validating provider and source, per-brand **consent** breakdowns, and a merged
consent + validation **history** trail.
Give it an authenticated `client` and the owning `(appSlug, dataSourceSlug, recordId)` and it lists
the record's channels, lets you add / edit / archive / restore / delete them, promote a primary per
kind, append to the **consent ledger** (scoped by brand → medium → purpose), record a validation
result and run the **validation engine** — all through [`useDocyrusContactChannels`](#hook), which
you can also use standalone to build a custom UI.
**Demo:**
```tsx
'use client';
import { Button } from '@docyrus/ui/primitives/ui/button';
export function ContactChannelsPanelDemo() {
return (
{''} requires an authenticated{' '}
RestApiClient plus the owning{' '}
(appSlug, dataSourceSlug, recordId). It talks to the Contact
Channels & Consent API — listing channels, recording the consent
ledger (brand · medium · purpose) and running the validation engine — so
the preview here is informational only. Wire it against your tenant (or
the playground page on base.contact) to see it live.
);
}
```
## Installation
```bash
pnpm dlx @docyrus/cli add @docyrus/ui-contact-channels-panel
```
**Dependencies:**
- [@docyrus/api-client](https://www.npmjs.com/package/@docyrus/api-client)
- [@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query)
- [lucide-react](https://www.npmjs.com/package/lucide-react)
## Usage
```tsx
import { useDocyrusAuth } from '@docyrus/signin';
import { ContactChannelsPanel } from '@docyrus/ui/components/contact-channels-panel';
function ContactChannels({ recordId }: { recordId: string }) {
const { client } = useDocyrusAuth();
if (!client) return null;
return (
);
}
```
### As a side drawer
Pass `open` / `onOpenChange` to render the panel inside an
[`AwesomeDialog`](/docs/web/components/awesome-dialog) — a **right-side sheet** by default. The
typical pairing is the compact [`ContactChannelsField`](/docs/web/components/contact-channels-field)
summary whose **Details** link opens this panel, with the contact / organization name as the
`subtitle`:
```tsx
const [open, setOpen] = useState(false);
setOpen(true)} />
```
## Endpoints
The panel (via `useDocyrusContactChannels`) targets, under the record base path
`/v1/apps/{appSlug}/data-sources/{dataSourceSlug}/items/{recordId}`:
| Group | Operations |
|-------|-----------|
| Channels | `GET /channels` · `POST /channels` · `PATCH /channels/{id}` · `POST /channels/{id}/make-primary` · `POST /channels/{id}/archive` · `POST /channels/{id}/unarchive` · `DELETE /channels/{id}` |
| Consent | `GET /consent` (record-wide) · `GET /channels/{id}/consent` · `POST /channels/{id}/consent` · `POST /channels/{id}/consent/bulk` · `POST /channels/consent/bulk` |
| Validation | `GET /validations` (record-wide) · `GET /channels/{id}/validations` · `POST /channels/{id}/validations` · `POST /channels/{id}/validate` |
| Brands | `GET /v1/tenant/brands` (for brand-aware consent labels) |
Read endpoints require the `DS.Read.All` scope; write endpoints require `DS.ReadWrite.All`. The
record-wide consent / validation queries avoid a per-channel N+1 in the panel. The consent and
validation ledgers are **append-only**; the channel's `consent` / `validation_status` caches are
maintained server-side by DB triggers and treated as read-only.
## API Reference
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `client` | `ContactChannelsClient` | — | Authenticated REST client (`@docyrus/api-client`'s `RestApiClient` works). |
| `appSlug` | `string` | — | App slug of the owning record. |
| `dataSourceSlug` | `string` | — | Data source slug of the owning record. |
| `recordId` | `string` | — | Owning record id (uuid). |
| `title` | `string` | `'Contact Points & Consent'` | Dialog heading. |
| `subtitle` | `string` | — | Sub-heading under the title (e.g. the contact and organization names). |
| `includeArchived` | `boolean` | `false` | Initial state of the "Show archived" toggle. |
| `enabled` | `boolean` | `true` | Defer queries until ready (e.g. auth resolved). |
| `loadBrands` | `boolean` | `true` | Fetch tenant brands for brand-aware consent. |
| `readOnly` | `boolean` | `false` | Hide every write affordance. |
| `maxHeight` | `string` | `'40rem'` | Max height when rendered inline (the dialog body scrolls). |
| `onChange` | `(channels: ContactChannel[]) => void` | — | Fired with the refreshed channel list after mutations. |
| `className` | `string` | — | Additional class on the root. |
| `open` | `boolean` | — | Dialog open state. Providing `open`/`onOpenChange` renders the panel in an `AwesomeDialog`. |
| `onOpenChange` | `(open: boolean) => void` | — | Dialog open-change handler. |
| `container` | `'sheet' \| 'drawer' \| 'modal'` | `'sheet'` | Dialog container when shown as a dialog. |
| `side` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'right'` | Side for the sheet / drawer container. |
| `size` | `'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'` | `'xl'` | Dialog size preset. |
## Hook
`useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId })` returns the data and a
typed mutation for every endpoint:
```tsx
const channels = useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId });
// data
channels.channels; // ContactChannel[]
channels.brands; // ContactBrand[]
channels.consentHistory; // ConsentRecord[] (record-wide)
channels.validationHistory; // ValidationRecord[] (record-wide)
channels.isLoading;
channels.error;
// mutations (all return promises)
await channels.createChannel({ channelKind: 'email', channelType: 'email', value: 'a@b.com' });
await channels.updateChannel(id, { label: 'work' });
await channels.makePrimary(id);
await channels.archiveChannel(id, 'old work email');
await channels.unarchiveChannel(id);
await channels.deleteChannel(id);
await channels.recordConsent(id, { purpose: 'marketing', action: 'opt_in', consentStatus: 'opted_in', consentChannel: 'sms' });
await channels.bulkRecordConsent(id, items);
await channels.bulkRecordRecordConsent(items); // spans many channels
await channels.recordValidation(id, { method: 'manual', status: 'valid' });
await channels.runValidation(id); // POST /channels/{id}/validate
```
## Components
| Component | Description |
|-----------|-------------|
| `ContactChannelsPanel` | Full Docyrus-backed panel (inline or as an `AwesomeDialog` drawer). |
| `PanelSummary` | Preferred email / phone summary cards. |
| `ChannelCard` | Per-channel read card with status grid (validation, provider, source) and the action menu. |
| `BrandConsentCard` | One brand's consent breakdown (EMAIL / PHONE mediums + capture metadata). |
| `ConsentHistory` | Merged, collapsible consent + validation audit trail. |
| `ChannelFormDialog` | Add / edit dialog (wraps the backend-agnostic `ChannelEditor`). |
| `ConsentDialog` | Records a single consent action (channel · purpose · action · status · medium · brand). |
| `ValidationDialog` | Records an externally-supplied validation result (`POST /channels/{id}/validations`). |
| `useDocyrusContactChannels` | The data + mutation hook. |
## Type Exports
| Type | Description |
|------|-------------|
| `ContactChannelsPanelProps` | Props for `ContactChannelsPanel`. |
| `ContactChannelsClient` | Minimal `{ get, post, patch, delete }` client contract. |
| `UseDocyrusContactChannelsOptions` / `UseDocyrusContactChannelsResult` | Hook options + return shape. |
| `ConsentEntryWithChannel` | A consent entry plus its target `channelId` (record-wide bulk). |
| `ContactChannel` / `ConsentRecord` / `ValidationRecord` / `ContactBrand` | Re-exported from the field package. |