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
| MCP | CLI | API |
|---|---|---|
crm_summary_get | cofounder crm summary get | GET /cofounder-cli/v1/crm/summary |
crm_search | cofounder 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
| MCP | CLI | API |
|---|---|---|
crm_accounts_list | cofounder crm accounts list | GET /cofounder-cli/v1/crm/accounts |
crm_accounts_get | cofounder crm accounts get --account-id <id> | GET /cofounder-cli/v1/crm/accounts/{account_id} |
crm_accounts_create | cofounder crm accounts create --name <name> --domain <domain> | POST /cofounder-cli/v1/crm/accounts |
crm_accounts_update | cofounder crm accounts update <account-id> --status customer | PATCH /cofounder-cli/v1/crm/accounts/{account_id} |
crm_accounts_delete | cofounder crm accounts delete <account-id> | DELETE /cofounder-cli/v1/crm/accounts/{account_id} |
crm_contacts_list | cofounder crm contacts list --account-id <id> | GET /cofounder-cli/v1/crm/contacts |
crm_contacts_get | cofounder crm contacts get --contact-id <id> | GET /cofounder-cli/v1/crm/contacts/{contact_id} |
crm_contacts_create | cofounder crm contacts create --name <name> --email <email> --account-id <id> | POST /cofounder-cli/v1/crm/contacts |
crm_contacts_update | cofounder crm contacts update --contact-id <id> --title <job-title> | PATCH /cofounder-cli/v1/crm/contacts/{contact_id} |
crm_contacts_delete | cofounder crm contacts delete <contact-id> | DELETE /cofounder-cli/v1/crm/contacts/{contact_id} |
crm_contacts_merge | cofounder crm contacts merge <contact-id> --merge-contact-id <id> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/merge |
crm_opportunities_list | cofounder crm opportunities list --account-id <id> | GET /cofounder-cli/v1/crm/opportunities |
crm_opportunities_get | cofounder crm opportunities get --opportunity-id <id> | GET /cofounder-cli/v1/crm/opportunities/{opportunity_id} |
crm_opportunities_create | cofounder crm opportunities create --account-id <id> --name <name> --stage lead | POST /cofounder-cli/v1/crm/opportunities |
crm_opportunities_update | cofounder crm opportunities update <opportunity-id> --stage proposal | PATCH /cofounder-cli/v1/crm/opportunities/{opportunity_id} |
crm_opportunities_delete | cofounder 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.
Resolve and link people
| MCP | CLI | API |
|---|---|---|
crm_identities_resolve | cofounder crm identities resolve | POST /cofounder-cli/v1/crm/identities/resolve |
crm_contacts_identities_get | cofounder crm contacts identities get --contact-id <id> | GET /cofounder-cli/v1/crm/contacts/{contact_id}/identities |
crm_contacts_identities_link | cofounder crm contacts identities link <contact-id> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/identities/link |
crm_contacts_identities_unlink | cofounder crm contacts identities unlink <contact-id> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/identities/unlink |
crm_contacts_upsert | cofounder crm contacts upsert | POST /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:
| MCP | CLI | API |
|---|---|---|
crm_company_profiles_search | cofounder 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.
| MCP | CLI | API |
|---|---|---|
crm_commitments_create | cofounder crm commitments create --title <text> --contact-id <id> --idempotency-key <uuid> | POST /cofounder-cli/v1/crm/commitments |
crm_commitments_get | cofounder crm commitments get --commitment-id <id> | GET /cofounder-cli/v1/crm/commitments/{commitment_id} |
crm_commitments_query | cofounder crm commitments query --state open --contact-id <id> | GET /cofounder-cli/v1/crm/commitments |
crm_commitments_update | cofounder crm commitments update --commitment-id <id> --state done --expected-revision <n> | PATCH /cofounder-cli/v1/crm/commitments/{commitment_id} |
crm_commitments_claim | cofounder crm commitments claim --commitment-id <id> --expected-revision <n> --idempotency-key <uuid> | POST /cofounder-cli/v1/crm/commitments/{commitment_id}/claim |
crm_commitments_renew | cofounder crm commitments renew --commitment-id <id> --expected-revision <n> --lease-token <token> | POST /cofounder-cli/v1/crm/commitments/{commitment_id}/renew |
crm_commitments_release | cofounder 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.
| MCP | CLI | API |
|---|---|---|
crm_relationships_create | cofounder 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_query | cofounder crm relationships query --source-contact-id <id> | GET /cofounder-cli/v1/crm/relationships |
crm_relationships_get | cofounder crm relationships get --relationship-id <id> | GET /cofounder-cli/v1/crm/relationships/{relationship_id} |
crm_relationships_update | cofounder 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_delete | cofounder 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
| MCP | CLI | API |
|---|---|---|
crm_accounts_enrich | cofounder crm accounts enrich --account-id <id> --request-id <uuid> | POST /cofounder-cli/v1/crm/accounts/{account_id}/enrich |
crm_contacts_enrich | cofounder crm contacts enrich --contact-id <id> --request-id <uuid> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/enrich |
crm_enrichments_get | cofounder 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
| MCP | CLI | API |
|---|---|---|
crm_activity_create | cofounder crm activity create --title <title> --links <json> | POST /cofounder-cli/v1/crm/activity |
crm_activity_list | cofounder 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
| MCP | CLI | API |
|---|---|---|
crm_calendar_sync_settings_get | cofounder crm calendar-sync settings get | GET /cofounder-cli/v1/crm/calendar-sync/settings |
crm_calendar_sync_preview | cofounder crm calendar-sync preview --calendar-id <id> | POST /cofounder-cli/v1/crm/calendar-sync/preview |
crm_calendar_sync_enable | cofounder crm calendar-sync enable --calendar-id <id> | POST /cofounder-cli/v1/crm/calendar-sync/enable |
crm_calendar_sync_disable | cofounder crm calendar-sync disable | POST /cofounder-cli/v1/crm/calendar-sync/disable |
crm_calendar_sync_retry | cofounder crm calendar-sync retry | POST /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
| MCP | CLI | API |
|---|---|---|
meetings_availability_query | cofounder meetings availability query --start <time> --end <time> | GET /cofounder-cli/v1/meetings/availability |
meetings_holds_create | cofounder meetings holds create --contact-id <id> --touch-id <id> --slots <json> | POST /cofounder-cli/v1/meetings/holds |
meetings_confirm | cofounder meetings confirm --meeting-id <id> | POST /cofounder-cli/v1/meetings/{meeting_id}/confirm |
meetings_cancel | cofounder 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
| MCP | CLI | API |
|---|---|---|
crm_prospects_preview | cofounder crm prospects preview --entity-type company --targeting <json> --max-cost-usd <usd> | POST /cofounder-cli/v1/crm/prospects/preview |
crm_prospects_find | cofounder crm prospects find --entity-type company --targeting <json> --max-cost-usd <usd> | POST /cofounder-cli/v1/crm/prospects/find |
crm_prospects_import | cofounder crm prospects import --job-id <id> --row-id <row-id> | POST /cofounder-cli/v1/crm/prospects/import |
crm_prospects_imports_get | cofounder 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
| MCP | CLI | API |
|---|---|---|
people_search | cofounder 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 1people 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 --jsonA 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"}' --jsonField 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.
| MCP | CLI | API |
|---|---|---|
crm_outreach_claim | cofounder crm outreach claim --contact-id <id> --request-id <uuid> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/claim |
crm_outreach_renew | cofounder crm outreach renew --contact-id <id> --token-file <path> --expected-revision <revision> | POST /cofounder-cli/v1/crm/contacts/{contact_id}/outreach/renew |
crm_outreach_release | cofounder 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_release | cofounder 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.