Back up meetings over the API
List every meeting and export each as Markdown to a local folder, in Python, Node, or Rust.
A common workflow: pull every meeting out of Meetily and keep a local, offline copy. The script below lists your meetings, then exports each one as Markdown into a backup folder next to the script. It is pure HTTP against the local Agent API. No SDK, no install.
Set your token first:
export MEETILY_PRO_TOKEN=your-token-hereGet a token from meetily-pro whoami, or from the in-app Integrations panel for a scoped one. On the same machine you can also read the loopback token file directly; see the authentication page for its path. Any token with Read scope works here, since this workflow only lists and exports.
Script
Standard library only, no pip installs required.
import json
import os
import pathlib
import urllib.request
BASE_URL = "http://127.0.0.1:8420"
TOKEN = os.environ["MEETILY_PRO_TOKEN"]
OUT_DIR = pathlib.Path("backup")
def api_get(path):
req = urllib.request.Request(
f"{BASE_URL}{path}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
with urllib.request.urlopen(req) as resp:
return resp.read()
def list_all_meetings():
meetings = []
offset = 0
limit = 200
while True:
data = json.loads(api_get(f"/v1/meetings?limit={limit}&offset={offset}"))
batch = data["meetings"]
meetings.extend(batch)
if len(batch) < limit:
break
offset += limit
return meetings
def main():
OUT_DIR.mkdir(exist_ok=True)
meetings = list_all_meetings()
print(f"Found {len(meetings)} meetings")
for m in meetings:
meeting_id = m["id"]
md = api_get(f"/v1/meetings/{meeting_id}/export?format=md").decode("utf-8")
(OUT_DIR / f"{meeting_id}.md").write_text(md, encoding="utf-8")
print(f"Exported {meeting_id}")
if __name__ == "__main__":
main()Run it with python backup.py.
Uses the built-in fetch (Node 18+), no dependencies.
const fs = require("fs");
const path = require("path");
const BASE_URL = "http://127.0.0.1:8420";
const TOKEN = process.env.MEETILY_PRO_TOKEN;
async function apiGet(pathname) {
const res = await fetch(`${BASE_URL}${pathname}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
if (!res.ok) throw new Error(`${pathname} failed: ${res.status}`);
return res;
}
async function listAllMeetings() {
const meetings = [];
let offset = 0;
const limit = 200;
while (true) {
const res = await apiGet(`/v1/meetings?limit=${limit}&offset=${offset}`);
const data = await res.json();
meetings.push(...data.meetings);
if (data.meetings.length < limit) break;
offset += limit;
}
return meetings;
}
async function main() {
fs.mkdirSync("backup", { recursive: true });
const meetings = await listAllMeetings();
console.log(`Found ${meetings.length} meetings`);
for (const m of meetings) {
const res = await apiGet(`/v1/meetings/${m.id}/export?format=md`);
const md = await res.text();
fs.writeFileSync(path.join("backup", `${m.id}.md`), md);
console.log(`Exported ${m.id}`);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});Run it with node backup.js.
Uses the ureq crate for HTTP and serde_json for parsing.
[dependencies]
ureq = { version = "2", features = ["json"] }
serde_json = "1"use std::env;
use std::fs;
const BASE_URL: &str = "http://127.0.0.1:8420";
fn api_get(token: &str, path: &str) -> ureq::Response {
ureq::get(&format!("{BASE_URL}{path}"))
.set("Authorization", &format!("Bearer {token}"))
.call()
.expect("request failed")
}
fn main() {
let token = env::var("MEETILY_PRO_TOKEN").expect("MEETILY_PRO_TOKEN not set");
fs::create_dir_all("backup").expect("failed to create backup dir");
let mut meetings: Vec<serde_json::Value> = Vec::new();
let mut offset = 0;
let limit = 200;
loop {
let path = format!("/v1/meetings?limit={limit}&offset={offset}");
let body: serde_json::Value = api_get(&token, &path).into_json().expect("bad json");
let batch = body["meetings"].as_array().cloned().unwrap_or_default();
let batch_len = batch.len();
meetings.extend(batch);
if batch_len < limit {
break;
}
offset += limit;
}
println!("Found {} meetings", meetings.len());
for m in &meetings {
let id = m["id"].as_str().expect("missing id");
let path = format!("/v1/meetings/{id}/export?format=md");
let md = api_get(&token, &path).into_string().expect("bad body");
fs::write(format!("backup/{id}.md"), md).expect("failed to write file");
println!("Exported {id}");
}
}Run it with cargo run.
All three scripts only call GET endpoints. They read your meetings and export them; they never rename, delete, or otherwise modify anything. A Read-scoped token is enough.
Last updated on
Sample workflows
Copy-paste recipes with runnable Python, Node, and Rust scripts, one for each way to drive Meetily.
Drive a recording from a script
Start a recording, sleep through the meeting, stop it, then list meetings and export the newest summary, all from a script that shells out to the meetily-pro CLI.
