Cofounder CLI

CRM

Track the companies, people, and deals you sell to, log what happened with each, and find new prospects.

Every company gets a built-in CRM: accounts (companies), contacts (people), and opportunities (deals), plus a log of calls, emails, meetings, and notes.

Get the overview

MCPCLIAPI
crm_summary_getcofounder crm summary getGET /cofounder-cli/v1/crm/summary
crm_searchcofounder crm search --query <text>GET /cofounder-cli/v1/crm/search

For questions like "what's going on with Acme?", start with the summary. It covers accounts, contacts, deals, and recent activity in one call. search finds records by name or text; add --object-type account, contact, or opportunity to narrow it. MCP clients can also show the CRM as a board with crm_summary_show.

Accounts, contacts, and deals

MCPCLIAPI
crm_accounts_listcofounder crm accounts listGET /cofounder-cli/v1/crm/accounts
crm_accounts_getcofounder crm accounts get --account-id <id>GET /cofounder-cli/v1/crm/accounts/{account_id}
crm_accounts_createcofounder crm accounts create --name <name> --domain <domain>POST /cofounder-cli/v1/crm/accounts
crm_accounts_updatecofounder crm accounts update <account-id> --status customerPATCH /cofounder-cli/v1/crm/accounts/{account_id}
crm_accounts_deletecofounder crm accounts delete <account-id>DELETE /cofounder-cli/v1/crm/accounts/{account_id}
crm_contacts_listcofounder crm contacts list --account-id <id>GET /cofounder-cli/v1/crm/contacts
crm_contacts_getcofounder crm contacts get --contact-id <id>GET /cofounder-cli/v1/crm/contacts/{contact_id}
crm_contacts_createcofounder crm contacts create --name <name> --email <email> --account-id <id>POST /cofounder-cli/v1/crm/contacts
crm_contacts_updatecofounder crm contacts update --contact-id <id> --title <job-title>PATCH /cofounder-cli/v1/crm/contacts/{contact_id}
crm_contacts_deletecofounder crm contacts delete <contact-id>DELETE /cofounder-cli/v1/crm/contacts/{contact_id}
crm_contacts_mergecofounder crm contacts merge <contact-id> --merge-contact-id <id>POST /cofounder-cli/v1/crm/contacts/{contact_id}/merge
crm_opportunities_listcofounder crm opportunities list --account-id <id>GET /cofounder-cli/v1/crm/opportunities
crm_opportunities_getcofounder crm opportunities get --opportunity-id <id>GET /cofounder-cli/v1/crm/opportunities/{opportunity_id}
crm_opportunities_createcofounder crm opportunities create --account-id <id> --name <name> --stage leadPOST /cofounder-cli/v1/crm/opportunities
crm_opportunities_updatecofounder crm opportunities update <opportunity-id> --stage proposalPATCH /cofounder-cli/v1/crm/opportunities/{opportunity_id}
crm_opportunities_deletecofounder crm opportunities delete <opportunity-id>DELETE /cofounder-cli/v1/crm/opportunities/{opportunity_id}

Search or list before you create. If a new account's domain, or a new contact's email, matches a record you already have, you get that record back instead of a duplicate. Every deal belongs to an account; --amount-cents is the amount times 100 in the deal's --currency (150000 is $1,500).

update changes only the fields you pass. To avoid overwriting someone else's edit, pass the record's updated_at as --expected-updated-at: the update is refused if the record changed since you read it. delete is permanent.

crm contacts merge folds a duplicate into the contact you keep (<contact-id>); --merge-contact-id is the duplicate, which is deleted. It is permanent, with no undo. Its activities, refs and opportunities move to the kept contact, and do-not-contact stays set if either contact had it. The merge and its crm.contacts.merged audit event commit together; if the event cannot be recorded, the merge rolls back. Retrying as the same actor with the same keeper returns the keeper's current record and does not add another event. A retry with a different actor or keeper conflicts.

MCPCLIAPI
crm_identities_resolvecofounder crm identities resolvePOST /cofounder-cli/v1/crm/identities/resolve
crm_contacts_identities_getcofounder crm contacts identities get --contact-id <id>GET /cofounder-cli/v1/crm/contacts/{contact_id}/identities
crm_contacts_identities_linkcofounder crm contacts identities link <contact-id>POST /cofounder-cli/v1/crm/contacts/{contact_id}/identities/link
crm_contacts_identities_unlinkcofounder crm contacts identities unlink <contact-id>POST /cofounder-cli/v1/crm/contacts/{contact_id}/identities/unlink
crm_contacts_upsertcofounder crm contacts upsertPOST /cofounder-cli/v1/crm/contacts/upsert

Resolve an exact email, phone, LinkedIn profile, or provider identifier before attaching new work. The result says resolved, ambiguous, unmatched, or conflicting; a shared phone can match several people. Names and company domains never merge people. Public identity links are recorded as assertions. Only trusted provider owners can record observed or verified identities. Link and unlink calls require a stable request_id; retry with the same ID to get the original result and event. Unlinking the primary email or phone promotes the first remaining alias. Upsert applies the supplied profile and relationship fields only when it creates a new contact; for a resolved contact it adds only the unmatched email, phone, or LinkedIn aliases and leaves curated profile and relationship fields unchanged. Upserts and explicit identity links for the same normalized identifiers serialize and apply their batch atomically. Ordinary contact creates and edits can still add a shared email or phone alias; later resolution reports ambiguous instead of choosing a contact or merging people.

To tie an account to a known company record, find the profile first and pass its id as --company-profile-id when creating the account:

MCPCLIAPI
crm_company_profiles_searchcofounder crm company-profiles search --query <name>GET /cofounder-cli/v1/crm/company-profiles

Record commitments and coordinate work

A commitment is work still to do, linked to at least one contact, account or opportunity. Its state is open, waiting, done or canceled. Responsibility is durable: it defaults to the authenticated caller, can be assigned to another company member, and survives an execution claim being released or expiring.

MCPCLIAPI
crm_commitments_createcofounder crm commitments create --title <text> --contact-id <id> --idempotency-key <uuid>POST /cofounder-cli/v1/crm/commitments
crm_commitments_getcofounder crm commitments get --commitment-id <id>GET /cofounder-cli/v1/crm/commitments/{commitment_id}
crm_commitments_querycofounder crm commitments query --state open --contact-id <id>GET /cofounder-cli/v1/crm/commitments
crm_commitments_updatecofounder crm commitments update --commitment-id <id> --state done --expected-revision <n>PATCH /cofounder-cli/v1/crm/commitments/{commitment_id}
crm_commitments_claimcofounder crm commitments claim --commitment-id <id> --expected-revision <n> --idempotency-key <uuid>POST /cofounder-cli/v1/crm/commitments/{commitment_id}/claim
crm_commitments_renewcofounder crm commitments renew --commitment-id <id> --expected-revision <n> --lease-token <token>POST /cofounder-cli/v1/crm/commitments/{commitment_id}/renew
crm_commitments_releasecofounder crm commitments release --commitment-id <id> --expected-revision <n> --lease-token <token>POST /cofounder-cli/v1/crm/commitments/{commitment_id}/release

Create returns {commitment, lease_token}. Reads and cursor-paginated queries return current state without the secret token. Query also accepts --due-before, --responsible-user-id, --account-id and --opportunity-id; deadline filtering is inclusive. Use --due-at with a timezone and --evidence-activity-id to point to existing CRM evidence. Evidence text stays in the activity.

Create and claim require an --idempotency-key; reuse the same key and request to recover a lost response. Claim recovery returns its original secret token. Updates, renewals and releases use the revision you read, and claim-holder writes also require the current lease token. A stale or missing result is recovered by reading current state before choosing the next mutation. Update only the fields supplied; JSON null clears a nullable field. Completing, canceling, reopening and reassignment are explicit updates. Ordinary updates must retain at least one context link.

Claim before executing shared work. A claim lasts 300 seconds by default; use --lease-seconds from 30 to 900. Claim and renew return the opaque lease_token. While a claim is active, updating the work requires that token, the current revision, and the same authenticated user and API key. Completion or cancellation clears the claim. Renew and release reject expired or superseded claims. Assignment does not grant claim ownership. The external agent executes and schedules work; a commitment claim does not itself send messages or enforce outreach admission.

Commitment writes and crm.commitment.* lifecycle facts commit together. Events contain identifiers, state, revision, attribution and evidence pointers; they omit the title and private evidence text. Contact merge moves the context and revokes execution claims. Deleting a context removes its link; losing the last context cancels outstanding work while preserving history. Reference cleanup facts use the crm-reference-cleanup source, explicitly mark system_reference_cleanup, and identify the commitment creator in actor_user_id with actor_attribution=commitment_creator, preserving the creator's historical UUID if that user is later deleted. Ordinary lifecycle facts identify the authenticated requester and API key. Resume from the event log, then read current commitment state before acting.

Evidenced relationships

Record explicit connections between people, or between a person and an organization. These are directed assertions: Alice introduced_by Bob means Bob introduced Alice; reversing the endpoints is a different fact. knows connects two people, and works_at connects a person to a CRM account. Co-attendance alone does not establish a relationship. Source labels describe an assertion; they do not confer provider verification.

MCPCLIAPI
crm_relationships_createcofounder crm relationships create --request-id <uuid> --source-contact-id <id> --target-contact-id <id> --relation introduced_by --evidence-ref <pointer> --source-label <label> --observed-at <timestamp>POST /cofounder-cli/v1/crm/relationships
crm_relationships_querycofounder crm relationships query --source-contact-id <id>GET /cofounder-cli/v1/crm/relationships
crm_relationships_getcofounder crm relationships get --relationship-id <id>GET /cofounder-cli/v1/crm/relationships/{relationship_id}
crm_relationships_updatecofounder crm relationships update --relationship-id <id> --expected-revision <revision> --evidence-ref <pointer> --source-label <label> --observed-at <timestamp>PATCH /cofounder-cli/v1/crm/relationships/{relationship_id}
crm_relationships_deletecofounder crm relationships delete --relationship-id <id> --expected-revision <revision>DELETE /cofounder-cli/v1/crm/relationships/{relationship_id}

For works_at, pass --target-account-id instead of --target-contact-id. Both endpoints must belong to your company; self edges and duplicate edges are refused. Query by source, target, or relation, with --offset and --limit. Use an HTTP(S) source URL or an activity:, library:, or research-job: reference as evidence, and a timezone-bearing observation timestamp. Results include the authenticated evidence author and revision. Contact-detail interaction timestamps and counts retain their existing meaning.

Updating replaces evidence, source label and observation time. Endpoints and relation remain fixed. Read the revision before updating or deleting; a stale revision returns a conflict. Create requires a request UUID: retry the same UUID, input and authenticated identity to recover its result after an uncertain response. Updates and deletes use only expected_revision. If a response is uncertain, read current state with get or query; a deleted relationship returns not found from get and disappears from current queries. Delete returns a snapshot with deleted_at. Deletion requires a managed CRM.

State and crm.relationship.created, updated, or deleted facts commit together. Facts include IDs, revisions and evidence pointers, without message bodies. Endpoint deletion removes its edges and records cleanup facts. Legacy cleanup uses source crm-reference-cleanup and attribution=assertion_author: the event is associated with the evidence author, who is not claimed to have requested the deletion. Explicit mutation events use the authenticated actor.

Contact merge moves edges to the kept person and increments their revisions. When edges collapse, the existing kept edge and its evidence win; the cleanup fact retains the removed evidence pointer and surviving edge ID. Edges that would become self edges are removed. Reference cleanup retains its explicit system attribution; the contact merge audit records the authenticated requester.

Fill in details

MCPCLIAPI
crm_accounts_enrichcofounder crm accounts enrich --account-id <id> --request-id <uuid>POST /cofounder-cli/v1/crm/accounts/{account_id}/enrich
crm_contacts_enrichcofounder crm contacts enrich --contact-id <id> --request-id <uuid>POST /cofounder-cli/v1/crm/contacts/{contact_id}/enrich
crm_enrichments_getcofounder crm enrichments get --job-id <id>GET /cofounder-cli/v1/crm/enrichments/{job_id}

Enriching looks up company or person data from a paid provider and fills in whatever it finds. Lookups can cost money, and there's no spending cap you can set. A lookup is billed whenever the provider runs it, including when it finds no match or when curated values or a concurrent edit prevent profile changes.

A lookup takes a minute or more, so enrich starts it as a job and returns at once; in a terminal the CLI waits for it to finish (--wait forces this under --json, --no-wait skips it). crm enrichments get shows a job's status and, once it finishes, the per-record result. Use a new UUID as --request-id for every new lookup: repeating a request with the same UUID returns the same job instead of paying for another lookup, and reusing the UUID for a different record is rejected. Complete records are skipped unless you pass --force-refresh.

Contact enrichment preserves curated values during routine and forced refresh. Populated fields without matching enrichment provenance count as curated. Provider-owned fields may refresh; explicit edits make those fields curated, even if you keep the same value. Email and phone aliases are retained, and enrichment never grants marketing consent or verifies an identity. Conflicting lookup emails produce identifier_conflict without applying profile facts.

Each result lists changed_fields, conflict_fields, unchanged_fields, and missing_fields. Contact reads expose enrichment_provenance with the provider, observation time, request UUID, and available provider record/run references. The same map is stored in system-owned metadata_json.enrichment_provenance; ordinary create/update requests cannot supply field ownership. Successful contact changes and the crm.contact.enriched event commit together. Events contain field names and references, with no provider payload or profile values.

Log activity

MCPCLIAPI
crm_activity_createcofounder crm activity create --title <title> --links <json>POST /cofounder-cli/v1/crm/activity
crm_activity_listcofounder crm activity list --object-type account --object-id <id>GET /cofounder-cli/v1/crm/activity

create logs a note, email, call, meeting, or task (--activity-type); --links is a JSON list of the accounts, contacts, or opportunities it's about. It can also store metadata and a --source-ref that keeps the same event from being logged twice. When there's a real email thread, recording, or document behind it, pass its link as --evidence-url.

activity list shows everything, newest first; pass --object-type and --object-id together for one record, or --occurred-after to fetch only what's new.

Capture site forms

Add a waitlist or contact form to a marketing or product site, then connect it:

cofounder company forms setup --app-binding-id <uuid>

Setup creates a forms:submit key limited to that site and writes COFOUNDER_FORMS_API_KEY and COFOUNDER_FORMS_SUBMIT_URL to its Production environment. The component posts from the site's server, so the key never reaches the browser. Each submission becomes a contact (found by email, never duplicated) with a note, and a form.submitted event. The marketing opt-in is stored as consent only when the visitor ticks the unchecked box. For local development, run cofounder company api-keys create --scope forms:submit --app-binding-id <uuid> and put the value in .env.local.

Sync from your calendar

MCPCLIAPI
crm_calendar_sync_settings_getcofounder crm calendar-sync settings getGET /cofounder-cli/v1/crm/calendar-sync/settings
crm_calendar_sync_previewcofounder crm calendar-sync preview --calendar-id <id>POST /cofounder-cli/v1/crm/calendar-sync/preview
crm_calendar_sync_enablecofounder crm calendar-sync enable --calendar-id <id>POST /cofounder-cli/v1/crm/calendar-sync/enable
crm_calendar_sync_disablecofounder crm calendar-sync disablePOST /cofounder-cli/v1/crm/calendar-sync/disable
crm_calendar_sync_retrycofounder crm calendar-sync retryPOST /cofounder-cli/v1/crm/calendar-sync/retry

With a calendar connected, the CRM can create accounts, contacts, and activity from your meetings. settings get lists your calendars and their ids and whether sync is on. preview counts what sync would create without writing anything. enable turns it on, looking back 30 days by default (--backsync-days). Attendees on personal email domains are skipped unless you pass --include-personal-domains, and sync reads only basic event details unless you pass --visibility-mode full. retry re-runs records that failed to sync; disable turns sync off, for all calendars or just the ones you name with --calendar-id.

Book meetings

MCPCLIAPI
meetings_availability_querycofounder meetings availability query --start <time> --end <time>GET /cofounder-cli/v1/meetings/availability
meetings_holds_createcofounder meetings holds create --contact-id <id> --touch-id <id> --slots <json>POST /cofounder-cli/v1/meetings/holds
meetings_confirmcofounder meetings confirm --meeting-id <id>POST /cofounder-cli/v1/meetings/{meeting_id}/confirm
meetings_cancelcofounder meetings cancel --meeting-id <id>POST /cofounder-cli/v1/meetings/{meeting_id}/cancel

availability query lists a company member's free times in a window of at most 31 days (--host-user-id, default you). The host must have connected Google Calendar. It returns free intervals only, never event details.

holds create reserves slots for a contact so nobody else can book them; --slots is a JSON list like [{"start":"2026-10-05T15:00:00Z","end":"2026-10-05T15:30:00Z"}]. --touch-id must be an outbound activity already logged for that contact; a hold without one is refused. A hold writes only to Cofounder and expires after --ttl-seconds (default 900, at most 3600); the response gives expires_at and one held meeting per slot.

confirm books one held slot: it creates the Google Calendar event with a Meet link and emails a real invite to the contact, releases the other slots, and records a calendar.meeting.booked event. A do-not-contact contact is refused. Confirm is idempotent by --idempotency-key (generated if omitted). After a retryable error such as calendar_provider_unavailable, the hold is intact: retry with the same key before it expires. cancel frees a booked slot, deletes the host's calendar event (Google emails the contact a cancellation), and records calendar.meeting.canceled; canceling twice is harmless.

Find new prospects

MCPCLIAPI
crm_prospects_previewcofounder crm prospects preview --entity-type company --targeting <json> --max-cost-usd <usd>POST /cofounder-cli/v1/crm/prospects/preview
crm_prospects_findcofounder crm prospects find --entity-type company --targeting <json> --max-cost-usd <usd>POST /cofounder-cli/v1/crm/prospects/find
crm_prospects_importcofounder crm prospects import --job-id <id> --row-id <row-id>POST /cofounder-cli/v1/crm/prospects/import
crm_prospects_imports_getcofounder crm prospects imports get --job-id <import-job-id>GET /cofounder-cli/v1/crm/prospects/imports/{job_id}
cofounder crm prospects preview --entity-type company \
  --targeting '{"industries":["dental clinics"],"hq_countries":["US"]}' \
  --max-cost-usd 25
cofounder research jobs get --job-id <job-id>
cofounder research jobs results get --job-id <job-id>

preview checks your targeting and estimates the cost without spending anything. find takes the same settings, spends up to --max-cost-usd, and returns a job id right away while the search runs in the background. Follow it with research jobs get, read the candidates with research jobs results get, then import the rows you want as accounts or contacts; no research is repeated. People without a LinkedIn profile come in as unverified contacts. import checks your selection right away, then creates the records in the background: in a terminal it waits and prints each row's outcome (--no-wait returns the import job at once), and prospects imports get shows it later. Pass your own --idempotency-key to find: if the response gets lost, run it again with the same key and settings and you won't start a second search. See Research for more on jobs.

Search for people

MCPCLIAPI
people_searchcofounder people search --query <text> --max-cost-usd <usd>POST /cofounder-cli/v1/people/search
cofounder people search \
  --query "founding engineers at seed-stage startups in New York" \
  --location "new york" --location brooklyn --title-keyword engineer \
  --company-stage seed \
  --min-years-experience 3 --limit 20 --max-cost-usd 1

people search spends money and answers in one response, usually within 5-20 seconds. Nobody is dropped: every candidate comes back in the provider's order with a pass, fail, or unknown check for each filter you set, plus the value that was compared. unknown means the data was missing, never a pass, so read the verdicts yourself and keep the people whose checks are all pass (or whose unknown checks you will confirm). Judgments such as quality or category fit are yours to make from the evidence. The call is rejected before any provider request when the estimated charge is above --max-cost-usd; a --company-stage filter adds one company lookup per employer. Save someone you want with crm contacts upsert.

Relationship processes and business fields

cofounder crm schema describe --json returns the Sales default and your company's custom pipeline and field definitions, including archived definitions. Pipelines have immutable keys and ordered stages with stable keys, editable labels, and descriptions. Opportunity status owns the explicit open, won, or lost outcome.

Custom process writes follow the shared COFOUNDER_AGENT_FIRST_API flag, together with outreach coordination and calendar auto-create. All backend reader workers must support the widened stage/account contracts before enabling the API. Disabling the flag closes the agent-first API and blocks custom process definition edits, new custom enrollment, and custom stage transitions. Legacy CRM reads of existing custom state, Sales writes, and typed business-field edits remain available.

When enabled, create a custom process and enroll a person without inventing an account:

cofounder crm pipelines create --key advisors --label Advisors \
  --stages '[{"key":"met","label":"Met"},{"key":"joined","label":"Joined"}]' --json
cofounder crm opportunities create --pipeline-key advisors --stage met \
  --name "Advisory relationship" --primary-contact-id CONTACT_ID --json

A person can participate in multiple opportunities. Reassign or delete a person-only process before deleting its last linked contact. Sales stays the default and requires an account; its existing stages and status behavior remain unchanged. Custom processes require a linked person or account. Stage changes preserve the explicit status unless you also update it. To switch processes, supply pipeline_key and a valid stage. Existing record reads, lists, search, and summary accept custom stages and nullable accounts. The existing web Sales editors keep their Sales controls; use the CLI for custom process stage edits.

cofounder crm pipelines update requires the last-read expected_revision and replacement label, stages, and archive state. An occupied stage cannot be removed: move its records first. Labels, descriptions, and ordering can change while occupied. Archival keeps records readable and prevents new enrollment.

Define a business field separately from system provenance:

cofounder crm fields create --object-type contact --key interest --label Interest \
  --value-type select --option advice --option investment --json
cofounder crm fields values update --object-type contact --record-id CONTACT_ID \
  --expected-updated-at LAST_READ_UPDATED_AT --values '{"interest":"advice"}' --json

Field types are text, number, boolean, date (YYYY-MM-DD), select, and multi_select. Numbers and booleans keep their JSON types; strings are not coerced. Read values in custom_fields_json on the existing account, contact, and opportunity commands. Null in a value patch removes a value; omitted keys are retained. metadata_json remains system provenance and is not editable through this operation. Stale record writes and schema revisions return a conflict.

cofounder crm fields update edits a field definition at its last-read revision. Field identity is immutable. Clear every value before changing a type, and keep options still used by any record. Archived field values remain readable and may be cleared, but cannot be changed or newly assigned. When contacts merge, kept values win (including false and zero); missing values fill from the merged contact.

Schema changes, business-value changes, and stage transitions append safe company facts in the same database transaction. Business field facts contain keys, never values. Sales-to-Sales crm.opportunity.stage_changed remains schema v1; transitions involving custom processes use v2 with pipeline identity, stage keys, and actual status. V2 allows a null account for person-only processes while preserving the Sales v1 contract for historical event readers.

Coordinate outreach across agents

Person coordination is available only in enrolled companies. Before enrollment, explicit lease commands return a disabled refusal and contact detail reports coordination.enabled: false. Ordinary company sends retain their existing purpose, contactability and allowance policies. Disabling enrollment does not clear stored fences: affected recipients and previously admitted operation keys still refuse a send. Inspect the original channel receipt; reenrollment allows confirmed receipts to replay through coordination. Unrelated legacy recipients remain sendable. Contact detail continues to show existing coordination facts.

Claim a contact before working on a proactive message when several agents or people share the company. Leases last 30–900 seconds and belong to the authenticated user and API key that claimed them. Session credentials and separate API keys are separate holders. Caller labels do not grant access.

MCPCLIAPI
crm_outreach_claimcofounder crm outreach claim --contact-id <id> --request-id <uuid>POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/claim
crm_outreach_renewcofounder crm outreach renew --contact-id <id> --token-file <path> --expected-revision <revision>POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/renew
crm_outreach_releasecofounder crm outreach release --contact-id <id> --token-file <path> --expected-revision <revision>POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/release
crm_outreach_force_releasecofounder crm outreach force-release --contact-id <id> --lease-id <lease-id> --expected-revision <revision>POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/force-release

Keep the returned token secret. Save it in a file for --token-file on renew and release. If a renew or release response is lost, read current coordination; the old expected revision will conflict and cannot repeat the mutation. Supply a JSON array of tokens with --lease-tokens-file on company mailbox send/reply/draft sends and company SMS sends. Renew with the current revision before expiry. Administrators can revoke a lease using force-release, which records an audit event; read coordination to confirm a lost response.

crm contacts get includes coordination: active holders, expiry and revision, the recipient keys covered at claim, and unresolved admission IDs. Claim returns these keys too. It never returns another holder's token. Coordination connects known normalized email and phone identifiers across contact aliases and duplicate records. A shared phone number conservatively coordinates all its contacts. Names, domains and unknown identifiers do not prove the same person; this is not a guarantee that every identifier for one human is known.

The company mailbox and SMS send owners reserve recipients durably before provider I/O, including when no manual lease exists. A current lease requires its holder and token. Campaign batches reserve their audience together and refuse active manual leases; they do not acquire interactive per-recipient tokens. Admission rechecks do-not-contact, email suppression, and current exact-address marketing consent. A duplicate with conflicting consent blocks the batch.

Use stable channel idempotency keys. A confirmed accepted response replays its receipt. A pending or uncertain admission refuses another dispatch, including with a new key, after lease expiry or release. Inspect the original channel receipt and resolve the provider outcome before attempting further outreach; there is no agent-facing override that makes an uncertain send retryable. If saving the admission result fails after a channel receipt was saved, the recipient remains blocked. Repair through the trusted send owner is required; automatic provider reconciliation is outside this release. crm.outreach.admitted records the committed reservation, not delivery. crm.outreach.resolved records the send owner's confirmed result. Lease changes produce crm.outreach.lease_changed; scan company events to catch up.

This coordination covers company mailbox, campaign and company SMS owners. Customer app transactional email, account-access mail and personal mailbox APIs retain their own purpose policies. Quiet hours, additional approval rules, social DMs and calls are outside this contract.