Skip to content

External contacts

An external contact is a person your org attributes work to who is not a SessionFS user: a clinical lead at a partner, a data engineer on another team, a vendor contact. Contacts let meetings, chat threads and tickets point at that person instead of a loose name.

Contacts belong to an organization, on every plan that has one. Personal projects have no contacts.

A person creates every contact. Extraction never does. When a meeting or chat names someone nobody has confirmed, the name stays an unresolved participant and shows up in the People to confirm queue. One click there creates the contact or adds the name as an alias of an existing one.

A contact is a minimal record, not a CRM entry:

Field Notes
display_name Up to 120 characters. Names don’t have to be unique
aliases Other names the person goes by, such as a first name (“Tolu” for “Tolulope Adeyemi”). Up to 20
email Optional. One active contact per address in an org. An address that belongs to a member is refused: assign the member instead
role Optional, such as “clinical lead”
tags Up to 10 lowercase slugs, such as vendor
status active or archived

There are no free-text notes. Name, aliases, role and tags go through your org’s DLP policy (the strictest policy across your orgs) before they are stored. A BLOCK finding refuses the save, and a redaction is stored redacted.

  • The directory (list, search, details): active members of the org, and service keys of that org with contacts:read. Anyone else gets the same “Contact not found”, whether the contact exists in another org or not at all.
  • Through a meeting, chat or ticket: anyone who can read that meeting, chat or ticket sees the contact’s name and role inline, including captured-session readers who aren’t org members. The email is never shown there. If the meeting or ticket now belongs to a different org, the contact isn’t shown: the participant shows its original name and the ticket shows “Unassigned”.
  • Email: org owners, org admins and the person who created the contact see it. Other members see a masked form (t***@vendor.example). Service keys never see it.
  • Where a contact appears: the contact’s page lists the meetings, chats, action items and tickets that name it, but only those you can read yourself.

Any active member can create and edit contacts. Owners and admins merge, delete and export.

When a meeting or chat is extracted, each participant name that literally appears in the text is matched in this order. Nothing is fuzzy.

  1. The exact email of an active org member.
  2. An exact persona name (a persona that represents a member also fills in that member).
  3. A display name that matches exactly one active member.
  4. The exact email of an active contact. For chat exports, the exporter’s email is only a matching key and is never stored.
  5. A name or alias that matches exactly one active contact.
  6. Otherwise the name stays unresolved.

Two contacts that share a name or alias never match: the name stays unresolved until you merge them or fix the alias. Re-running extraction keeps names resolved to a contact. Archived contacts drop out of matching.

You can also resolve a participant by hand to a member, a persona or a contact:

Terminal window
sfs meeting resolve-participant <meeting_id> <participant_id> --contact ec_1a2b3c4d5e6f
sfs chat resolve-participant <thread_id> <participant_id> --contact ec_1a2b3c4d5e6f

The To confirm queue lists the unresolved names in the last 90 days of your org’s meetings and chats that you can read, grouped by name, with how often each appears and up to three sources. Candidate contacts come with a hint (“same first name”, “name starts the same”). Hints never resolve anything on their own.

  • Create contact creates it with the name filled in, then resolves every listed mention in a meeting or chat you can edit. Mentions you can’t edit stay unresolved and resolve on their next extraction.
  • Add as alias adds the name to an existing contact and applies it the same way.
  • Dismiss hides the name from your own queue.

Nothing is stored for a suggestion until a person acts.

A ticket has one assignee: a persona, a person or a contact. Assign a contact with assigned_contact_id (API), --assign-contact (CLI) or assigned_contact (MCP). The contact must be active and belong to the project’s org; every other case gets the same 422 assignee_not_eligible.

A contact can’t log in, so nobody gets a “your turn” notification for it. The creator still follows the ticket. Jira publish sends no assignee. When a project moves to another org, tickets assigned to the old org’s contacts become unassigned, with a timeline event.

Promoting a meeting or chat action item whose owner resolved to a contact drafts the ticket assigned to that contact.

A contact links to a member only by verified email: the contact’s email must equal the member’s verified address, and that person must be an active member of the contact’s org. The link is checked when someone joins the org (invite, domain auto-join, SSO), when someone verifies their email, and when an owner or admin sets a contact’s email to a member’s verified address. No route links an arbitrary user to a contact.

When the link happens, in the same transaction:

  • the contact’s open tickets move to the new member in every project the member can read, each with a timeline event, and the member follows them;
  • meetings and chats show the member wherever the contact appeared;
  • the contact stays as “now a member” for history.

Tickets in projects the member can’t read stay on the contact.

  • Merge (owners and admins) folds a duplicate into a contact: participants and tickets move, the duplicate’s name becomes an alias, and empty email, role and tags are filled from it. Two different emails refuse the merge (409 email_conflict). Preview first, then confirm. The merged id still opens the contact it was merged into.
  • Archive keeps the contact on its meetings and tickets but drops it from matching and pickers. You can make it active again.
  • Delete (owners and admins) erases the contact and its aliases. Participants go back to unresolved and tickets become unassigned; the ticket event records only that a contact was removed, never the name. Transcripts still contain the name as text, because the transcript is the record; meeting retention covers that.
  • GET /api/v1/orgs/{org_id}/contacts?format=csv exports the directory as CSV (owners and admins, user keys, at most 10,000 rows, formula-like cells prefixed with ', X-Truncated: true when cut off).
  • GET /api/v1/orgs/{org_id}/contacts/{id}/export returns everything stored about one contact for a data request: the row with its email, aliases, and every reference as ids and kinds.
  • Contacts follow the org’s lifecycle. An optional org setting, general.contact_retention_days (30–3650, off by default), lets the POST /api/v1/admin/sweeps/contact-retention sweep delete contacts that are archived, or that nothing references, older than that many days.

We never email or message a contact.

Terminal window
sfs contact list [--q TEXT] [--status active|archived|all] [--tag TAG]
sfs contact show <id> [--refs]
sfs contact create "Tolulope Adeyemi" --email tolu@vendor.example --role "clinical lead" --alias Tolu
sfs contact edit <id> [--name N] [--email E | --clear-email] [--role R | --clear-role] [--tag T]
sfs contact alias add <id> "Tolu"
sfs contact alias rm <id> "Tolu"
sfs contact archive <id> [--undo]
sfs contact merge <target_id> <source_id> [--yes]
sfs contact delete <id> [--yes]
sfs contact export <id> [-o contact.json]
sfs contact export-all [-o contacts.csv]
sfs contact suggestions [--apply NAME --contact ID] [--dismiss NAME]
sfs ticket create --title "Chase the data sample" --assign-contact <id>
sfs ticket update <ticket_id> --assign-contact <id|none>

Every command takes --org (default: your default org, sfs config default-org).

Tool Purpose
list_external_contacts The org’s contacts (q, status, tag, before, limit)
get_external_contact One contact and where it appears, limited to what you can read
create_external_contact Create a contact when a person asks. Never from names found in transcripts
update_external_contact Edit name, role, tags, email, status, and add or remove aliases

create_ticket and update_ticket accept assigned_contact. Merge and delete aren’t available over MCP. Contact text is user-entered: agents treat it as data, not instructions.

Method Path Scope Who
GET /api/v1/orgs/{org_id}/contacts contacts:read Members; format=csv owners/admins with a user key
POST /api/v1/orgs/{org_id}/contacts contacts:write Members
GET /api/v1/orgs/{org_id}/contacts/{id} contacts:read Members
GET /api/v1/orgs/{org_id}/contacts/{id}/references contacts:read Members (readable sources only)
PATCH /api/v1/orgs/{org_id}/contacts/{id} contacts:write Members
POST, DELETE /api/v1/orgs/{org_id}/contacts/{id}/aliases contacts:write Members
POST /api/v1/orgs/{org_id}/contacts/{id}/merge-from/{source_id} user key Owners/admins (?confirm=true to merge)
DELETE /api/v1/orgs/{org_id}/contacts/{id} user key Owners/admins
GET /api/v1/orgs/{org_id}/contacts/{id}/export user key Owners/admins
GET /api/v1/orgs/{org_id}/contacts/suggestions user key Members
POST /api/v1/orgs/{org_id}/contacts/suggestions/apply, /dismiss user key Members

Service keys are limited to their own org, never see an email, and get 403 service_key_not_allowed on merge, delete, export and suggestions.