Developer Guide

How to Transcribe Audio with Timestamps

A transcript without timing is a wall of text. A transcript with timing is a way to navigate the recording — jump to a quote, build chapter markers, or make every line clickable. Here is what timing our API gives you and what to build with it.

Segment-level, not word-level

Being precise about this up front saves disappointment. Our API returns segment-level timestamps: a start and end time for each contiguous chunk of speech, which usually corresponds to a sentence or a speaker turn. It does not return a timestamp for every individual word.

That distinction matters for exactly one use case: word-by-word highlighting as audio plays, karaoke style. If that is what you are building, you need a service with word-level output and we are not it.

For everything else — jumping to a passage, generating captions, building chapters, citing a quote with a timecode, measuring speaking time per participant — segment-level timing is what you actually want, and word-level would only add noise.

Getting timestamps

There is nothing to enable. Every completed job returns two fields: transcription, which is plain text with no timing, and diarization, which carries the timing and the speaker labels.

The diarization field
SPEAKER_00 [00:00:01.000 - 00:00:05.240]: Thanks for coming in today.
SPEAKER_01 [00:00:05.900 - 00:00:11.480]: No problem at all, happy to help.
SPEAKER_00 [00:00:12.010 - 00:00:19.750]: Let's start with how you first heard about us.

Times are HH:MM:SS.mmm from the start of the recording, with millisecond precision. Speaker labels are generic — SPEAKER_00, SPEAKER_01 — and map to real people in your own code.

Parsing into something usable

Python — segments as objects, with seconds
import re

LINE = re.compile(
    r"^(?P<speaker>SPEAKER_\d+) "
    r"\[(?P<start>[\d:.]+) - (?P<end>[\d:.]+)\]: "
    r"(?P<text>.*)$"
)


def to_seconds(timecode: str) -> float:
    hours, minutes, seconds = timecode.split(":")
    return int(hours) * 3600 + int(minutes) * 60 + float(seconds)


def parse(diarization: str) -> list[dict]:
    segments = []
    for line in diarization.splitlines():
        match = LINE.match(line.strip())
        if not match:
            continue
        seg = match.groupdict()
        seg["start_seconds"] = to_seconds(seg["start"])
        seg["end_seconds"] = to_seconds(seg["end"])
        seg["duration"] = seg["end_seconds"] - seg["start_seconds"]
        segments.append(seg)
    return segments

Converting to seconds immediately is worth doing, because almost everything downstream — media player seeking, arithmetic, sorting — wants a number rather than a string.

Clickable transcripts

The highest-value thing to build with timestamps is a transcript where clicking a line seeks the audio to that moment. It takes very little code and transforms how usable a long recording is.

Rendering a seekable transcript
def render_html(segments: list[dict], audio_url: str) -> str:
    rows = "\n".join(
        f'<p data-start="{s["start_seconds"]:.3f}" class="cue">'
        f'<span class="time">{s["start"][:8]}</span> '
        f'<strong>{s["speaker"]}</strong>: {s["text"]}</p>'
        for s in segments
    )
    return f"""
<audio id="player" src="{audio_url}" controls></audio>
<div id="transcript">{rows}</div>
"""
The three lines of JavaScript that make it work
const player = document.getElementById("player");

document.getElementById("transcript").addEventListener("click", (event) => {
  const cue = event.target.closest(".cue");
  if (cue) {
    player.currentTime = Number(cue.dataset.start);
    player.play();
  }
});

// Optional: highlight the line currently being spoken.
const cues = [...document.querySelectorAll(".cue")];
player.addEventListener("timeupdate", () => {
  const now = player.currentTime;
  let active = null;
  for (const cue of cues) {
    if (Number(cue.dataset.start) <= now) active = cue;
    else break;
  }
  cues.forEach((c) => c.classList.toggle("active", c === active));
});

Why this matters more than it sounds

The objection to transcripts is usually that nobody reads a 12,000-word document. Nobody does. But people scan a clickable transcript to find the two minutes they need, which is a completely different behaviour and the reason podcast and research platforms all build this.

Chapters and speaking time

Two more things fall out of segment timing almost for free.

Speaking time per participant
from collections import defaultdict

def speaking_time(segments: list[dict]) -> dict[str, float]:
    totals = defaultdict(float)
    for seg in segments:
        totals[seg["speaker"]] += seg["duration"]
    return dict(totals)


# Useful for interview QA: did the interviewer talk too much?
totals = speaking_time(segments)
total = sum(totals.values())
for speaker, seconds in sorted(totals.items()):
    print(f"{speaker}: {seconds / 60:.1f} min ({seconds / total:.0%})")

Researchers and UX teams use this as a quality check on interview technique. A moderator taking 60% of the airtime in a discovery interview is a finding in itself.

Naive chapter markers from long pauses
def chapters(segments: list[dict], gap_seconds: float = 3.0) -> list[dict]:
    """Start a new chapter after a noticeable silence."""
    marks, current = [], None
    for i, seg in enumerate(segments):
        gap = seg["start_seconds"] - segments[i - 1]["end_seconds"] if i else 0
        if current is None or gap >= gap_seconds:
            current = {"start": seg["start_seconds"], "title": seg["text"][:60]}
            marks.append(current)
    return marks

Pause-based chaptering is crude but surprisingly effective on interviews and presentations, where topic changes genuinely do follow a beat of silence. Tune the gap threshold to your material.

Accuracy of the timing itself

Segment boundaries are derived from the diarization model's view of where speech starts and stops, so they are accurate to a fraction of a second in clean audio and less precise in messy audio.

Where they drift is crosstalk. When two people speak over each other, the boundary between their segments is a judgement the model makes, and it can place a word on the wrong side. For navigation this is immaterial — you land within a second of the right place. For captions it can produce a slightly early or late cue.

If exact boundaries matter, offset your seek target back by half a second. Landing slightly early is unnoticeable; landing slightly late means missing the first word of the quote you were looking for.

Frequently asked questions

Does the API return word-level timestamps?

No. Timing is segment-level — a start and end time for each contiguous chunk of speech, usually a sentence or speaker turn. Word-level timing is only required for karaoke-style word-by-word highlighting. For navigation, captions, chapters and citation, segment-level timing is what you want.

How do I get timestamps in a transcript?

They come automatically. Every completed job returns a diarization field containing speaker-labelled lines in the format SPEAKER_00 [00:00:01.000 - 00:00:05.240]: text. There is nothing to enable and no extra charge — the plain transcription field is the same content without timing.

What format are the timestamps in?

HH:MM:SS.mmm measured from the start of the recording, with millisecond precision. Converting to seconds as soon as you parse is usually worth doing, since media player seeking and any arithmetic want a number rather than a string.

How do I make a transcript where clicking a line jumps to that point in the audio?

Render each segment with its start time in a data attribute, then set the audio element's currentTime to that value on click. It is about ten lines of JavaScript and it is the single most useful thing to build with timestamps, because people scan a clickable transcript for the part they need rather than reading it through.

How accurate are the segment timestamps?

Accurate to a fraction of a second on clean audio. They degrade with crosstalk, where the boundary between two overlapping speakers is a judgement the model makes and a word can land on the wrong side. For navigation this is immaterial; if exact boundaries matter, offset your seek target back by half a second.

Can I work out how long each person spoke for?

Yes — sum the duration of each speaker's segments. This is a common quality check in research and UX interviews, where a moderator taking most of the airtime is a finding in itself.

Get a timestamped transcript in minutes

90 minutes free, no credit card required. Speaker labels and segment timing included at no extra cost.