ProductOSDocs

Entity reference · v1

meetings

MTG-NNNNNNtable meetingssoft delete

Meetings with their transcripts (Microsoft Teams, an uploaded .vtt file, pasted text or the API). The transcript itself is read and written through import_meeting_transcript and GET /api/v1/meetings/{id}/transcript, not as a field.

A meeting is a call with its transcript: a customer check-in, an interview, a sales call. The meeting row holds who was there and which accounts it was with; the transcript sits beside it as speaker turns. Meetings are turned on per workspace with the meetings flag.

Importing

Meetings are created by importing a transcript, not with POST /api/v1/meetings:

POST /api/v1/meetings/import
{
  "title": "Acme Robotics renewal check-in",
  "transcript": "WEBVTT\n\n00:00:02.100 --> 00:00:05.400\n<v Jordan Rivera>Thanks for making time.</v>\n...",
  "source": "teams",
  "started_at": "2026-09-29T16:00:00-07:00",
  "external_transcript_id": "<the provider's transcript id>",
  "participants": [{ "name": "Jamie Ortiz", "email": "jamie@acme-robotics.com" }]
}
  • transcript is WebVTT, as Microsoft Teams, Zoom and Google Meet write it, or plain text with one Name: text line per turn. Send segments ([{ start_ms, end_ms, speaker, text }]) instead when you already have the turns.
  • source is teams, upload, paste or api. With an external_transcript_id it defaults to teams.
  • The same external_transcript_id imports once per workspace: a repeat returns 200 with created: false and the meeting already there, and writes nothing. A new import returns 201.
  • An empty transcript, both transcript and segments, or neither, returns 422.
  • When the workspace processes meetings on import (see below), the response's processing says whether a rule started a run (started, rule) or why it did not (started: false with a reason).

The MCP tool import_meeting_transcript runs the same import. With Claude's Microsoft 365 connector, Claude reads a Teams meeting's transcript from the calendar event's meetingTranscriptUrl and sends it here.

Who was there

Each speaker and each listed participant lands in participants, matched where ProductOS knows them:

  • Team members by email, or by name when the transcript only names the speaker. A name two team members share matches neither.
  • Contacts by email: the address of exactly one contact. The participant gets contact_id and the contact's account.
  • Accounts by email: an address one account's contacts use, then the address's domain when exactly one account uses it. Free mail domains and the team's own domains never match an account.
  • Contacts by name, once the meeting's accounts are known: a name exactly one contact at those accounts has. Speakers named only in the transcript are matched this way, never across the whole workspace.

Each participant is { name, email, team_member_id, contact_id, account_id }; the import's response says how many team members and contacts matched (team_members_matched, contacts_matched).

To match a meeting's people again, after adding contacts: POST /api/v1/meetings/{id}/rematch. It returns the meeting, changed (false when nothing new matched, and nothing was written), the match counts and any account it linked.

Matched accounts are linked through meeting_accounts, so GET /api/v1/accounts/{id}/links/meetings lists an account's meetings and GET /api/v1/meetings/{id}/links/accounts the reverse.

Reading the transcript

GET /api/v1/meetings/{id}/transcript
GET /api/v1/meetings/{id}/transcript?format=vtt

The first returns segments, speakers, duration_ms and expires_at. The second returns the stored file as a download. The MCP tool get_meeting_transcript returns the turns as text lines, in pages, each starting with the segment range it covers (#12-15). /meetings/{id}?t=12 opens a meeting at segment 12.

Processing with AI

POST /api/v1/meetings/{id}/process
GET  /api/v1/meetings/{id}/proposals
POST /api/v1/meetings/{id}/proposals

POST .../process reads the transcript with the model and returns 202 with a run_id at once; the meeting's status is processing until the run ends as processed or failed. It needs meetings edit and the ai.generate capability, and the caller's Anthropic key pays (their saved key, else the hosted key). A second call while a run is going returns 409; a meeting without a transcript returns 422.

GET .../proposals returns the run's state (processing), what it wrote on the meeting (insights: summary, decisions, action_items, risks, findings and other_tools, each with evidence), and the proposals. Each proposal has a kind (feedback, integration_request, feedback_link, deal_blocker, account_health, competitor_mention, market_signal, win_loss_signal, persona_insight, release_commitment, okr_progress, comment, work_status, work_lead, work_assignee, work_dates, task, work_candidate, product_change) and the area it writes to, confidence, evidence (segment_start, segment_end, quote, speaker, start_ms), matches (similar feedback), warnings, the catalog steps accepting runs (input.steps), other options (variants), and cannot_apply, which says why the caller cannot accept it (null when they can). ?all=1 includes proposals a later run replaced.

POST .../proposals decides proposals: { "items": [{ "id": "...", "decision": "accept" | "reject" | "undo", "variant": "create", "edits": { "title": "...", "description": "...", "urgency": "high" } }] }, at most 50. Accepting runs the steps as the caller, through the same catalog actions as Copilot, so their access applies and the activity log records it. A feedback_link proposal adds to feedback that already exists: a comment with the quote, one more vote (counted when accepted), and the meeting's account and Work links. A proposal whose input.merge_with_proposal names another meeting's proposal for the same request adds to that one's feedback when it was accepted first, and the result says merged_into. Feedback created this way has source Meeting (or Internal when the workspace's own team said it) and metadata.meeting with the meeting id, the segment range and the proposal id. undo reverses an accepted proposal or restores a rejected one.

Processing on import, by rule

GET /api/v1/meetings/processing-rules
PUT /api/v1/meetings/processing-rules

GET returns the workspace's rules and can_edit. PUT (owners and admins) sets them: { "when": "off" | "all" | "customers" | "organizers", "organizers": ["joy@example.com"], "daily_cap": 20, "auto_accept": { "feedback_link": false, "comment": false } }. A rule's run is paid by the key of the person who imported the meeting; past daily_cap runs in a day (UTC), meetings wait for someone to process them. With auto_accept, a feedback_link or comment proposal at high confidence with no warnings is accepted as the person who saved the rules and has auto_accepted: true; undo reverses it like any other. A rule's run has processing.trigger rule and processing.rule naming it; when a rule applied but did not run, GET .../proposals returns why in auto.

Proposals across meetings

GET /api/v1/proposals

Proposals from every meeting the caller can see, newest first, each with its meeting (id, title, started_at, accounts) and cannot_apply. status is open (pending and failed, the default), decided (accepted and rejected) or all; kind narrows to one kind; limit is 1 to 500 (default 300), and truncated says more matched. Proposals of meetings in the trash are left out. Decide them with POST /api/v1/meetings/{id}/proposals.

Asking someone else to apply a proposal

POST /api/v1/proposals/{id}/request
GET  /api/v1/proposals/{id}
POST /api/v1/proposals/{id}

A proposal with cannot_apply set can be requested: the Work item's Lead (else the workspace's owners and admins) is notified (proposal.requested) with a link to /proposals/{id}. Someone asked can GET that proposal and POST a decision ({ "decision": "accept" | "reject" | "undo", "variant", "edits" }) with view on the area it writes to, without access to meetings. A proposal whose input.steps is empty has no default (two people share a name): pass a variant.

Retention

A transcript is kept for 120 days from import, the same as Teams' own default, and then removed by a daily job. The meeting, its participants and its accounts stay. expires_at on the transcript says when.

Editing

PATCH /api/v1/meetings/{id} changes title, started_at, ended_at, status (imported, processing, processed, failed), participants and metadata. The transcript itself is not editable. DELETE moves the meeting to the trash.

Endpoints

GET/api/v1/meetings
POST/api/v1/meetings
GET/api/v1/meetings/{id}
PATCH/api/v1/meetings/{id}
DELETE/api/v1/meetings/{id}
POST/api/v1/meetings/{id}/restore
GET/api/v1/meetings/{id}/links/accounts
POST/api/v1/meetings/{id}/links/accounts
DELETE/api/v1/meetings/{id}/links/accounts/{targetId}

Writable fields

* required on create. PATCH treats every field as optional.

FieldTypeConstraintsNotes
title*string
started_attimestamp
ended_attimestamp
sourcestringteams | upload | paste | api | capture
statusstringimported | processing | processed | failed
join_urlstring
organizer_emailstring
participantsjsonWho attended: [{ name, email, team_member_id, account_id }]. Import matches names and emails to team members and accounts.
external_idstringThe provider's meeting id, e.g. the Microsoft Graph onlineMeeting id.
external_transcript_idstringThe provider's transcript id. A transcript is imported once per workspace: the same id finds the same meeting.
is_draftboolean
metadatajson

Listing & filtering

  • Pagination: ?limit=50&offset=0 · max limit 200
  • Default sort: ?sort=started_at&order=desc
  • Sortable fields: started_at · created_at · updated_at · title · id_number
  • Search (?q=): title, organizer_email
  • Filters: ?source= ?status= ?organizer_email= ?is_draft= , repeat any filter to OR multiple values
  • Trash: hidden by default; pass ?include_deleted=true
  • Expand: ?expand=accounts

Examples

curlbash
curl 'https://productos.ca/api/v1/meetings?limit=10' \
  -H "Authorization: Bearer $TOKEN"

Try it out

Paste a token and hit a real endpoint. The request goes from your browser straight to /api/v1/meetings. The token is held in this page only for the request and is never stored.

Try it out

Issue tokens in Settings → API tokens. Token never leaves this page.

GET/api/v1/meetings?limit=3

Generated from the API's entity configuration. This page is rebuilt on every deploy, so it always matches the live API.