Contact detail
The per-person record view: identity, account context, timeline, consent, buyer-lens, tags, and AI panels — backed by the people projection.
Contact detail
The contact detail view at /contacts/{id} is the per-person record: who they are, which account they belong to, everything that has happened with them, and their consent posture. It is served by GET /v1/people/{canonical_id} (api/routes/people.py).
Two contact models — this page is the people projection
Pact has two parallel "contact" data models. This detail view reads the people projection: each row in the companies table carries up to nine named executives in denormalized columns (ceo_name, ceo_corporate_email, ceo_title, cfo_*, cto_*, …), and the people route explodes those wide rows into per-person records. The separate contacts table (real per-person rows used by sequence enrollment and buyer-lens) is a different surface. Know which one you are looking at before "fixing missing contacts."
How a person is addressed
The canonical_id in the URL is opaque but encodes c{company_id}-{role} — for example c42-ceo. The route parses it, loads the company row, and returns the exploded person plus a slice of account context (name, website, industry, HQ) so the page can render company stats without a second round-trip. An id that doesn't resolve to a real named executive returns 404.
What the record shows
The header renders the person's name, normalized title (role defaults fill in when a title is blank), and account. Contact actions — call, compose email, add note, schedule meeting, enrich — sit alongside. Below, a tabbed timeline aggregates activity:
| Tab | Contents |
|---|---|
| All | Every timeline event for the person's account. |
| Email engagement events. | |
| Signals | Behavioral / tracking signals. |
| Notes | Notes logged against the record. |
| Meetings | Calendar meetings. |
| Comments | Collaborative comments (with live presence). |
| Consent | The contact's per-channel consent state. |
| Buyer Lens | Buyer-intelligence view for the person. |
Side panels layer on relationship strength, sentiment trend, engagement, custom fields, tags, territory visibility, and AI panels (action suggestions, "what's happening"). Comments carry real-time presence via a collaboration provider.
Identifiers and email
Each exploded person exposes a primary_email, primary_linkedin, and primary_phone drawn from the company's {role}_corporate_email / {role}_linkedin / {role}_corporate_phone columns, packaged as typed identifiers with confidence and source.
Consent state on this projection is 'unknown'
The people projection currently returns consent_state: "unknown" for every person. The consent ledger (consent_records / consent_events) keys on the contact's email_hash via the contacts table, not on the person-graph canonical_id — so the projection cannot yet join a person to their consent record. The badge shows a stable "unknown" rather than a blank until the identity-graph join lands. This is a known gap, not a bug in your data.
Roadmap: a real people table
The people route is explicitly a read-side projection on top of the existing schema. The docstring notes that when a proper people / identity_records table lands, the routes flip to query it without changing the contract. Until then, "contacts" here are a computed view of company executive columns.
Editing
POST /v1/people creates a person and the record supports inline edits, note-adding, and enrichment. Because the underlying store is the wide companies row, edits write back to that role's columns.