Meeting Master API
Submit meeting transcripts, extract structured intelligence (decisions, business rules, workflows, action items, dependencies), and consume the result programmatically. Three calls — submit, wait, fetch — and you have the data.
https://your-host ·
Auth: X-API-Key header on every request ·
Get a key: Log into the dashboard → API Keys & Webhooks tab.
Quickstart
From curl to extracted JSON in three steps. Replace $KEY with the secret you minted from the dashboard.
Submit a transcript
POST one or more files to /api/upload/batch. The server queues each one and returns a job_id.
curl -X POST \
-H "X-API-Key: $KEY" \
-F "files=@meeting.pdf" \
-F "project_code=VCMACH" \
https://your-host/api/upload/batch
Supported: PDF, DOCX, DOC, RTF, ODT, MD, TXT, HTML, CSV, JSON. Up to 20 files per call. project_code is optional — auto-detected from filename and content.
Response
{
"message": "1 files queued for processing",
"jobs": [
{
"job_id": "f9e5b4a2-0c30-4e2c-9d1a-1f8c2b5d6e77",
"filename": "meeting.pdf",
"format": "pdf",
"extracted_length": 18342
}
],
"errors": []
}
Wait for completion
Two ways to find out when the job is done:
Option A — Poll /api/jobs/:job_id
curl -H "X-API-Key: $KEY" \
https://your-host/api/jobs/f9e5b4a2-0c30-4e2c-9d1a-1f8c2b5d6e77
Status progresses through pending → validating → processing → completed (or failed / duplicate). When status == "completed", the response also includes project_id and stats.
Option B — Register a webhook
One-time setup: in the dashboard's API Keys & Webhooks tab, add a URL that should receive job.completed events. Your endpoint receives a signed POST with the job_id, project_id, and a results_url. See webhooks ↓.
Fetch the structured data
Use the project_id returned from step 2 (or the results_url from a webhook):
curl -H "X-API-Key: $KEY" \
https://your-host/api/projects/42/data
Returns the full v4 export: meetings, modules, decisions, micro-decisions, people, business rules, formulas, permissions, workflows, action items, hidden dependencies, and conversation contexts. Schema ↓.
Authentication
Every /api/* call accepts one of two credentials:
- API key via
X-API-Key: mm_<tier>_<id>_<secret>— for programmatic clients (this is what you'll use). - Session cookie
mm_session— set byPOST /api/auth/login, used by the web dashboard.
API key tiers
| Tier | Allowed routes | Use for |
|---|---|---|
| admin | Everything (settings, model assignments, key & webhook management, all reads/writes) | Operator tooling, infra automation |
| ingest | Submit transcripts, read your own jobs and project data | Production callers — recommended default |
Minting a key
- Log into the dashboard at /.
- Open the API Keys & Webhooks tab.
- Click Create API Key — give it a name and pick a tier.
- Copy the full secret from the green box. You won't see it again. Only its hash is stored on the server.
DELETE /api/api-keys/:id) and mint a new one.
Authorization rules
- Missing or malformed key on a protected route →
401 Unauthorized. - Valid
ingestkey calling an admin route →403 Forbidden. - Revoked key →
401(treated as invalid). - Public routes (
/api/health,/api/auth/login,/api/auth/status,/api/supported-formats) accept anyone, no key needed.
Endpoints — auth & status
Returns {"status":"ok","version":"2.0.0"}. No auth.
Returns {authenticated: bool, type?: "session"|"apikey", tier?: "admin"|"ingest", adminConfigured?: bool}. Used by the dashboard to decide whether to show the login overlay.
Body: {"password": "..."}. Sets the mm_session cookie on success. Used by the dashboard, not by API clients.
Endpoints — upload & jobs
Multipart form fields: files[] (one or more), project_code (optional), project_name (optional). Each accepted file becomes a job; jobs auto-start. Returns {jobs: [{job_id, filename, format, extracted_length}, …], errors: […]}.
Older form returning {upload_id, document_info} — pair with POST /api/jobs/:job_id/start and SSE streaming. Prefer /api/upload/batch for new integrations.
Returns {job_id, status, progress, phase, project_id?, stats?, error?, logs}. status is one of pending, validating, processing, completed, failed, duplicate.
Real-time progress via SSE. Each event is a JSON object with type ∈ {status, log, complete, error, duplicate, retry, ping}. The connection closes on a terminal event. Use this if you don't want to poll.
/api/upload flow)
Not needed for /api/upload/batch — those auto-start.
Endpoints — projects & data
This is the main "give me the answers" endpoint. Returns the v4 schema for one project (meetings, decisions, micro-decisions, business rules, etc.). Schema ↓.
What the in-browser viewer at /viewer auto-loads.
Endpoints — admin
These require either an admin-tier API key or a logged-in session. ingest keys get 403.
Settings & models
Projects (mutating)
API keys
{name, tier} — secret returned onceWebhooks
{url, events[]} — secret returned once{active: bool} — pause/resumeWebhooks
Webhooks let you skip polling. When a job hits a terminal state and matches an active subscription's events, the server POSTs a signed payload to your URL.
Subscribing
Add a subscription via the dashboard or:
curl -X POST \
-H "X-API-Key: $ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://your.app/webhooks/meetingmaster",
"events": ["job.completed", "job.failed"]
}' \
https://your-host/api/webhooks
The response includes a secret. Store it — you'll use it to verify incoming requests, and it's not retrievable later.
Available events
job.completed— extraction succeeded;project_id,stats, andresults_urlpopulated.job.failed— extraction errored after retries;errorpopulated.job.duplicate— content hash matched an existing transcript; nothing was processed.
Payload
HTTP POST with these headers:
Content-Type: application/jsonX-MM-Event: job.completed(or.failed/.duplicate)X-MM-Signature: sha256=<hex>
Body:
{
"event": "job.completed",
"job_id": "f9e5b4a2-0c30-4e2c-9d1a-1f8c2b5d6e77",
"filename": "meeting.pdf",
"project_code": "VCMACH",
"project_id": 7,
"stats": { "decisions": 12, "microDecisions": 41, "businessRules": 9 },
"error": null,
"results_url": "/api/projects/7/data",
"timestamp": "2026-04-25T12:34:56.789Z"
}
Single attempt, 10 s timeout. There is no retry queue — design your endpoint to be idempotent. The dashboard tracks last_status per webhook for visibility.
Verifying the signature
Compute sha256=HMAC_SHA256(secret, raw_body) as lower-case hex and constant-time-compare against X-MM-Signature. Use the raw bytes of the body (don't re-serialize parsed JSON).
import hmac, hashlib
def verify(raw_body: bytes, secret: str, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header or "")
import crypto from 'crypto';
function verify(rawBody, secret, header) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func Verify(rawBody []byte, secret, header string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(header))
}
Errors
All error responses have the shape {"error": "human-readable message"} (or {"detail": "..."} for FastAPI validation errors). HTTP status codes:
| Code | Meaning |
|---|---|
400 | Malformed request — missing field, bad format, unsupported file type. |
401 | Missing/invalid/revoked API key (or no session). |
403 | Authenticated but tier insufficient (ingest key on admin route). |
404 | Resource not found (job, project, transcript). Also: /api/sample-data with no projects. |
500 | Unexpected server error — please report. |
Response schemas
The full v4 export from /api/projects/:id/data looks like this (truncated):
{
"meta": {
"schemaVersion": "4.0.0",
"extractedAt": "2026-04-27T...",
"project": "VCMACH",
"qualityMetrics": {
"totalEntities": 142,
"coveragePercentage": 95,
"confidenceAverage": 0.87
}
},
"meetings": [{ "meetingId": "MTG-VCMACH-20251207", ... }],
"modules": [{ "moduleId": "MOD-VCMACH-001", ... }],
"decisions": [{ "decisionId":"DEC-VCMACH-001", "decided_by_ids":[...], ... }],
"microDecisions": [{ "microId": "MIC-VCMACH-001", ... }],
"people": [{ "personId": "PER-VCMACH-001", ... }],
"businessRules": [{ "ruleId": "RULE-VCMACH-001", "category":"pricing", ... }],
"formulas": [{ "formulaId": "FRM-VCMACH-001", "expression":"...", ... }],
"permissions": [{ "permId": "PRM-VCMACH-001", ... }],
"workflows": [{ "workflowId":"WF-VCMACH-001", "steps":[...], ... }],
"actionItems": [{ "actionId": "ACT-VCMACH-001", "assignee_id":"PER-...", ... }],
"hiddenDependencies": [{ "depId": "DEP-VCMACH-001", "risk_level":"high", ... }],
"pendingClarity": [{ "pendId": "PND-VCMACH-001", "blocking_decisions":[...], ... }],
"conversationContexts":[{ "contextId": "CTX-VCMACH-001", ... }]
}
Cross-references use the domain ID columns (decided_by_ids, related_decision_ids, etc.). The full JSON Schema lives at shared/schema/meeting_master_schema_v4.json in the repo.
Code examples
End-to-end submit-and-fetch in two languages.
import os, time, requests
BASE = os.environ["MM_BASE_URL"] # e.g. https://your-host
KEY = os.environ["MM_API_KEY"] # mm_ingest_...
H = {"X-API-Key": KEY}
# 1) Submit
with open("meeting.pdf", "rb") as f:
r = requests.post(
f"{BASE}/api/upload/batch",
headers=H,
files={"files": f},
data={"project_code": "VCMACH"},
)
r.raise_for_status()
job_id = r.json()["jobs"][0]["job_id"]
# 2) Poll
while True:
r = requests.get(f"{BASE}/api/jobs/{job_id}", headers=H)
r.raise_for_status()
s = r.json()
if s["status"] in ("completed", "failed", "duplicate"):
break
time.sleep(3)
if s["status"] != "completed":
raise RuntimeError(s.get("error") or s["status"])
# 3) Fetch
data = requests.get(f"{BASE}/api/projects/{s['project_id']}/data", headers=H).json()
print(f"Decisions: {len(data['decisions'])}, Rules: {len(data['businessRules'])}")
import fs from 'node:fs';
const BASE = process.env.MM_BASE_URL;
const KEY = process.env.MM_API_KEY;
const H = { 'X-API-Key': KEY };
// 1) Submit
const fd = new FormData();
fd.append('files', new Blob([fs.readFileSync('meeting.pdf')]), 'meeting.pdf');
fd.append('project_code', 'VCMACH');
const submit = await (await fetch(`${BASE}/api/upload/batch`, {
method: 'POST', headers: H, body: fd,
})).json();
const jobId = submit.jobs[0].job_id;
// 2) Poll
let s;
while (true) {
s = await (await fetch(`${BASE}/api/jobs/${jobId}`, { headers: H })).json();
if (['completed','failed','duplicate'].includes(s.status)) break;
await new Promise(r => setTimeout(r, 3000));
}
if (s.status !== 'completed') throw new Error(s.error || s.status);
// 3) Fetch
const data = await (await fetch(`${BASE}/api/projects/${s.project_id}/data`, { headers: H })).json();
console.log(`Decisions: ${data.decisions.length}, Rules: ${data.businessRules.length}`);