Write a bundle
To put a meeting into Chitmonk, write a .chit and have the person import it: the library drawer, then Import. There is no other way in, and no way to do it without them.
The smallest valid bundle
Two files in a ZIP.
manifest.json lists one meeting, with the SHA-256 of its file:
{
"format": "chitmonk-bundle",
"version": 1,
"exportedAt": "2026-10-09T16:00:00.000Z",
"app": "my-tool 1.0",
"meetings": [{ "id": "<meeting-id>", "path": "meetings/<meeting-id>.json", "schema": 1, "events": 3, "sha256": "<sha-256 of that file>" }]
}
meetings/<meeting-id>.json holds the events. A meeting needs only its first one, MeetingCreated, to be valid. A useful one also has people, lines and an end:
MeetingCreated the title, the language, what made the transcript
ParticipantAdded one per person
RecordingStarted
SegmentCommitted one per line
MeetingEnded
A writer in Python
No packages to install. Change TITLE, PEOPLE and LINES to what you have.
"""Write a .chit bundle Chitmonk will import, from a transcript you already have.
Standard library only. python3 write_chit.py planning-call.chit
"""
import datetime
import hashlib
import json
import sys
import time
import uuid
import zipfile
TITLE = "Planning call"
PEOPLE = ["Ana", "Ben"]
# start and end in seconds from the start of the recording, who your tool thinks spoke (or None), the words
LINES = [
(0.0, 4.2, "Ana", "Shall we ship the import on Thursday?"),
(4.8, 7.5, "Ben", "Yes. I will test it on the shared laptop first."),
(8.0, 9.4, None, "Sounds like a plan."),
]
def new_id():
return str(uuid.uuid4()) # any lowercase UUID will do
meeting = new_id()
started = int(time.time() * 1000) # when the recording began, in milliseconds since 1970
events = []
def add(kind, at, **fields):
events.append({"id": new_id(), "meetingId": meeting, "at": at, "type": kind, **fields})
# The first event must be MeetingCreated. `engine` is printed on the receipt as "Transcribed by",
# so say what really made the transcript.
add("MeetingCreated", started, schema=1, title=TITLE, language="en", privacyMode="transcript-only",
engine={"stt": "My transcriber", "sttVersion": "1.0", "runtime": "Python", "vad": "none", "speaker": None},
vocabulary=[])
people = {name: new_id() for name in PEOPLE}
for name, pid in people.items():
add("ParticipantAdded", started, participantId=pid, name=name)
add("RecordingStarted", started)
for start, end, who, text in LINES:
# suggestedSpeakerId is a guess: Chitmonk shows it dashed, with a "?", until a person confirms it.
# If a person really did say who spoke, follow the line with a SpeakerAssigned event instead.
add("SegmentCommitted", started + round(end * 1000), lineId=new_id(), text=text, words=[],
startMs=round(start * 1000), endMs=round(end * 1000), language="en", turnId=new_id(),
suggestedSpeakerId=people.get(who))
add("MeetingEnded", started + round(LINES[-1][1] * 1000))
# One file per meeting, and a manifest that lists it with the SHA-256 of its exact bytes.
body = json.dumps({"schema": 1, "events": events}).encode()
path = f"meetings/{meeting}.json"
manifest = {
"format": "chitmonk-bundle", "version": 1,
"exportedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"app": "write_chit.py 1.0",
"meetings": [{"id": meeting, "path": path, "schema": 1, "events": len(events), "sha256": hashlib.sha256(body).hexdigest()}],
}
# Only these two files. No folders as entries of their own, nothing else.
with zipfile.ZipFile(sys.argv[1], "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("manifest.json", json.dumps(manifest, indent=2))
z.writestr(path, body)
print(f"wrote {sys.argv[1]}: {len(events)} events")
Rules that trip people up
- Ids are lowercase UUIDs, hyphenated. Uppercase is refused. Give every event, line, person and item its own.
- Every event carries the same
meetingId, and it is the id in the manifest and in the file's name. MeetingCreatedcomes first, withschema: 1.- The checksum is of the exact bytes in the ZIP. Hash the bytes you write, not a re-serialised copy.
- Only the listed files. No folder entries, no
__MACOSX, no readme. Zip the files, not the folder they are in. - Times.
atis milliseconds since 1970.startMsandendMsare milliseconds from the start of the recording. The meeting's length comes from theatofRecordingStartedandMeetingEnded, so set those apart by the length of the recording. wordsmay be an empty list. Chitmonk fills it with word timings when it transcribes; nothing needs it on import.languageis a short code. Chitmonk's own areen,de,fr,es,it,nl,pl; on a meeting it may also beauto.- Text is capped at 20,000 characters for any one title, line or note.
- No voice data. A key named
embedding,embeddings,voiceprint,prototype,pcmoraudioanywhere in an event makes the whole bundle fail.
The JSON Schema says all of this in a form a validator can check.
Do not confirm what a person did not
This is the one rule that is about honesty and not about format.
- If your tool worked out who spoke, put it in
suggestedSpeakerId. Chitmonk shows it dashed with a question mark, and the person confirms it with one key. - If a person said who spoke, follow the line with
SpeakerAssigned. - If your tool found an action item, write
ActionSuggestedand stop.ActionAcceptedmeans a person agreed to it.
Chitmonk never turns a guess into a fact by itself, and a bundle should not do it on its behalf.
What happens on import
The bundle is checked before anything is saved: what import checks. If the person already has a meeting with the same id and different events, yours is added as a separate copy; it never overwrites theirs. Use a new id for a new meeting.