Notes
Zero-touch CRM notes — paste text or an email, and Pact writes a note plus reviewable proposed updates to your contacts, deals, and accounts.
Notes
Pact's note-taker turns unstructured text — a call recap, a pasted email, a few bullet points — into a structured note plus a queue of proposed updates to your CRM records. Nothing mutates your data until a human accepts it.
Live, behind a flag
The note-taker is real: routes in api/routes/notes.py, generation and
proposal logic in core.note_taker. Every route sits behind the AUTO_NOTES
tenant feature flag (default off) and the sales module — when the flag is
disabled, /v1/notes/* returns 404 as if the surface didn't exist. Enable it
per tenant via POST /v1/flags without a redeploy.
From text or email
Two ingestion paths create a note and its proposals in one shot:
POST /v1/notes/from-text { "text": "Had a call with Acme..." }
POST /v1/notes/from-email { raw RFC 2822 message bytes }
from-email accepts the raw message and parses it server-side. You can pass
optional related_to hints (contact_id, account_id, opportunity_id,
company_name) to target specific records; without them the proposer infers
the entities from the note body.
Ingestion glue is caller-side
Live email plumbing — Gmail webhook signature verification, IMAP polling — is
intentionally out of scope for these routes. /from-email accepts the raw
bytes that a future webhook worker will hand off, which keeps the testable
business logic separate from the I/O glue.
The review queue
Generated notes never write to your CRM directly. Instead they produce
proposed_updates you review:
- 1
Inbox
GET /v1/notes/inboxlists recent notes;GET /v1/notes/{id}returns the note, its source artifact, and the proposal bundle. - 2
Review
GET /v1/notes/proposed-updatesis the global queue across all notes. - 3
Accept or reject
POST /v1/notes/proposed-updates/{id}/acceptapplies the mutation and writes an audit event;.../rejectrecords the rejection and a reason. - 4
Bulk accept
POST /v1/notes/proposed-updates/bulk-acceptaccepts everything at or above a confidence floor (the request may override the default floor,0.0–1.0).
An admin sweep, POST /v1/notes/proposed-updates/expire-stale, idempotently
retires pending proposals that have gone stale.
Permissions
- Read (
inbox, note detail, list proposals, calibration) requiresCONTACTS_READ— everyone on the tenant can review the AI's suggestions. - Write (
from-text,from-email, accept, reject, bulk-accept) requiresCONTACTS_WRITE— proposals mutate CRM state, so only seat-holding reps can approve them. - Expire-stale is admin-only (
ADMIN_QUOTAS).
Tenant isolation comes from get_tenant_id, never the body. Accept/reject
actions are attributed to the caller's user id; when the API key isn't bound to
a user, the audit event records "ai-note-taker" as the actor instead.
Calibration
GET /v1/notes/calibration returns per-field acceptance rates — how often you
accept the note-taker's suggestions for each field. It's the feedback signal
that tunes future proposals as your reject corpus grows.
Voice notes
A separate surface, POST /v1/voice-notes, records a spoken note against a
deal, account, or contact. Audio is Fernet-encrypted at rest, transcription
uses your tenant's AssemblyAI key from the credential store, and the summary is
written to the audit log. Voice notes are gated by the voice_notes feature
flag and require per-user recording consent before the first capture.