PPactDocs
Productivity

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:

code
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. 1

    Inbox

    GET /v1/notes/inbox lists recent notes; GET /v1/notes/{id} returns the note, its source artifact, and the proposal bundle.
  2. 2

    Review

    GET /v1/notes/proposed-updates is the global queue across all notes.
  3. 3

    Accept or reject

    POST /v1/notes/proposed-updates/{id}/accept applies the mutation and writes an audit event; .../reject records the rejection and a reason.
  4. 4

    Bulk accept

    POST /v1/notes/proposed-updates/bulk-accept accepts everything at or above a confidence floor (the request may override the default floor, 0.01.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) requires CONTACTS_READ — everyone on the tenant can review the AI's suggestions.
  • Write (from-text, from-email, accept, reject, bulk-accept) requires CONTACTS_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.