Docs

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.
  • MeetingCreated comes first, with schema: 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. at is milliseconds since 1970. startMs and endMs are milliseconds from the start of the recording. The meeting's length comes from the at of RecordingStarted and MeetingEnded, so set those apart by the length of the recording.
  • words may be an empty list. Chitmonk fills it with word timings when it transcribes; nothing needs it on import.
  • language is a short code. Chitmonk's own are en, de, fr, es, it, nl, pl; on a meeting it may also be auto.
  • Text is capped at 20,000 characters for any one title, line or note.
  • No voice data. A key named embedding, embeddings, voiceprint, prototype, pcm or audio anywhere 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 ActionSuggested and stop. ActionAccepted means 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.