Entity reference · v1
meetings
MTG-NNNNNNtable meetingssoft deleteMeetings 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" }]
}transcriptis WebVTT, as Microsoft Teams, Zoom and Google Meet write it, or plain text with oneName: textline per turn. Sendsegments([{ start_ms, end_ms, speaker, text }]) instead when you already have the turns.sourceisteams,upload,pasteorapi. With anexternal_transcript_idit defaults toteams.- The same
external_transcript_idimports once per workspace: a repeat returns200withcreated: falseand the meeting already there, and writes nothing. A new import returns201. - An empty transcript, both
transcriptandsegments, or neither, returns422. - When the workspace processes meetings on import (see below), the response's
processingsays whether a rule started a run (started,rule) or why it did not (started: falsewith areason).
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_idand 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=vttThe 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}/proposalsPOST .../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-rulesGET 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/proposalsProposals 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
/api/v1/meetings/api/v1/meetings/api/v1/meetings/{id}/api/v1/meetings/{id}/api/v1/meetings/{id}/api/v1/meetings/{id}/restore/api/v1/meetings/{id}/links/accounts/api/v1/meetings/{id}/links/accounts/api/v1/meetings/{id}/links/accounts/{targetId}Writable fields
* required on create. PATCH treats every field as optional.
| Field | Type | Constraints | Notes |
|---|---|---|---|
| title* | string | ||
| started_at | timestamp | ||
| ended_at | timestamp | ||
| source | string | teams | upload | paste | api | capture | |
| status | string | imported | processing | processed | failed | |
| join_url | string | ||
| organizer_email | string | ||
| participants | json | Who attended: [{ name, email, team_member_id, account_id }]. Import matches names and emails to team members and accounts. | |
| external_id | string | The provider's meeting id, e.g. the Microsoft Graph onlineMeeting id. | |
| external_transcript_id | string | The provider's transcript id. A transcript is imported once per workspace: the same id finds the same meeting. | |
| is_draft | boolean | ||
| metadata | json |
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
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.
Issue tokens in Settings → API tokens. Token never leaves this page.
GET/api/v1/meetings?limit=3