Skip to main content

How to get the full transcript of a recording

The transcript is what Sally builds the summary from: who said what, and when. This guide shows how to check that a transcript exists, what the transcript response contains, how to keep the answer small, how to download it as a finished file or as subtitles, and ends with a script that turns a recording into a readable text file.

Every example works as it stands once SALLY_TOKEN, DIRECTORY_ID and RECORDING_ID are set, see the Quickstart. Pick your language once, every example on the API pages follows.

Before you start

Quick navigation

  1. Check the status first
  2. Fetch the transcript
  3. Keep the answer small
  4. Download as a file
  5. Complete script: export a transcript as text
  6. Endpoints at a glance

1. Check the status first​

A finished transcription run is not the same as an existing transcript. Look at the recording first. Field selection keeps that answer down to the three status fields:

curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID?fields=isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason" \
-H "Authorization: Bearer $SALLY_TOKEN"
200 OK
{
"isTranscriptionCompleted": true,
"isTranscriptionSucceeded": true,
"transcriptionCompletionReason": "succeeded"
}
FieldTypeMeaning
isTranscriptionCompletedbooleanThe run is over, whatever the outcome.
isTranscriptionSucceededbooleanThere is a usable transcript. This is the field to check.
transcriptionCompletionReasonstringWhy the run ended, see the next table.
transcriptionCompletionReasonMeaning
succeededA usable transcript exists.
noSpokenAudioNobody spoke in the recording.
fileEmptyThe file was empty.
languageNotDetectedThe language could not be detected.
quotaExceededThe transcription quota was used up.
errorThe run failed. The underlying error text is deliberately not exposed.
unknownThe run has not finished yet.
This means

A meeting in which nobody spoke produces a finished run without a transcript. That is not an error and does not need a retry, noSpokenAudio says so plainly.

2. Fetch the transcript​

Once isTranscriptionSucceeded is true, fetch the transcript:

curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription" \
-H "Authorization: Bearer $SALLY_TOKEN"
200 OK
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"speakers": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"speakerNumber": 1,
"name": "Anna Berger",
"emailAddress": "anna.berger@example.com"
},
{
"speakerId": "4f0e1c52-7a3b-4d8e-9c61-2b5d8e7f3a10",
"speakerNumber": 2,
"name": null,
"emailAddress": null
}
],
"segments": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"startTime": 12.4,
"endTime": 17.9,
"text": "We agreed on the rollout plan for October."
},
{
"speakerId": "4f0e1c52-7a3b-4d8e-9c61-2b5d8e7f3a10",
"startTime": 18.2,
"endTime": 21.0,
"text": "Then I will update the timeline today."
}
]
}

The answer holds two lists that belong together:

  • speakers has one entry per voice with speakerId, speakerNumber and, where Sally could resolve it, name and emailAddress.
  • segments holds the time-coded pieces of speech with startTime and endTime in seconds, the text and a speakerId from the speakers list. You do not need a second call to resolve the speaker of a segment.
FieldTypeDescription
recordingIdstringId of the recording.
speakers[].speakerIdstringId of the speaker, referenced by each segment.
speakers[].speakerNumbernumberSpeaker number.
speakers[].namestring | nullName of the speaker.
speakers[].emailAddressstring | nullEmail of the speaker.
segments[].speakerIdstringSpeaker of the segment, resolves to the speakers list.
segments[].startTimenumberStart time in seconds.
segments[].endTimenumberEnd time in seconds.
segments[].textstring | nullTranscribed text of the segment.

3. Keep the answer small​

Important

Transcripts are the largest responses in this API, and almost all of it is segments. If you only want to know who took part, ask for ?fields=speakers. The segments are then not sent at all. Field selection explains the rules.

curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription?fields=speakers" \
-H "Authorization: Bearer $SALLY_TOKEN"
200 OK
{
"speakers": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"speakerNumber": 1,
"name": "Anna Berger",
"emailAddress": "anna.berger@example.com"
}
]
}

Selectable fields on this endpoint are recordingId, speakers and segments. A nested list is returned whole or not at all, speakers.name is not supported.

4. Download as a file​

If you need the transcript as a document rather than as data, GET …/recordings/{recordingId}/transcription/document returns exactly the file a user downloads in the Sally web app: with branding, logos, header and footer, speakers, timestamps and the same file name. The file is built on request and comes back directly in the response, there is no job to poll. The formats vtt and srt are subtitle files without a header block, ready for a video player.

ParameterTypeRequiredDescription
formatstringyespdf, docx, xlsx, txt, md, vtt or srt.
languageCodestringnode-DE, en-US, es-ES, fr-FR, it-IT, nl-NL or pl-PL. Without it, the language of the token owner applies, as in the web app.
# -OJ saves the file under the name from Content-Disposition
curl -fOJ "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription/document?format=docx&languageCode=en-US" \
-H "Authorization: Bearer $SALLY_TOKEN"
Note

languageCode only changes the labels and the date format inside the document, not the transcribed text. The font always follows the language of the recording, so Chinese characters stay readable with en-US labels. This endpoint does not support ?fields=.

The answer is 200 with the file itself, not JSON. The header Content-Disposition carries the file name the web app uses for the same download, ASCII in filename and UTF-8 in filename*. The Content-Type follows the format:

formatContent-Type
pdfapplication/pdf
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
txttext/plain; charset=utf-8
mdtext/markdown; charset=utf-8
vtttext/vtt; charset=utf-8
srtapplication/x-subrip; charset=utf-8

Besides the usual errors, this endpoint answers with:

StatuscodeMeaning
400UNKNOWN_DOCUMENT_FORMAT or UNKNOWN_LANGUAGE_CODEformat is missing or unknown, or languageCode is unknown. The body lists the allowed values.
403TRANSCRIPT_DOWNLOAD_PREVENTEDThe company account has disabled this download for its members.
404The recording does not exist, belongs to another company account, or you cannot access it.
409TRANSCRIPT_NOT_AVAILABLEThe recording has no transcript yet. Check the status first.

Errors shows how to handle these codes in one place.

5. Complete script: export a transcript as text​

This script waits until the transcription run of a recording has finished, stops cleanly if there is no transcript and otherwise writes a readable text file with one line per segment: time stamp, speaker, text. Speakers without a resolved name appear as Speaker 1, Speaker 2 and so on. The polling interval of 30 seconds is a choice of the script, not a value of the API.

#!/usr/bin/env bash
# Needs curl and jq. curl --retry repeats a request after a 429 with growing waits.
set -euo pipefail
BASE="https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID"
CURL=(curl -sf --retry 5 -H "Authorization: Bearer $SALLY_TOKEN")

# 1. Wait until the run has finished
while : ; do
status=$("${CURL[@]}" "$BASE?fields=isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason")
[ "$(echo "$status" | jq '.isTranscriptionCompleted')" = "true" ] && break
sleep 30
done

# 2. Stop if there is no transcript
if [ "$(echo "$status" | jq '.isTranscriptionSucceeded')" != "true" ]; then
echo "No transcript: $(echo "$status" | jq -r '.transcriptionCompletionReason')"
exit 0
fi

# 3. Fetch it and write one line per segment
"${CURL[@]}" "$BASE/transcription" | jq -r '
(.speakers | map({key: .speakerId, value: (.name // "Speaker \(.speakerNumber)")}) | from_entries) as $names
| .segments[]
| (.startTime | floor) as $s
| "[\($s / 60 | floor):\($s % 60 | tostring | ("0" + .)[-2:])] \($names[.speakerId]): \(.text // "")"
' > "transcript-$RECORDING_ID.txt"

The result looks like this:

transcript-53e2e041-4b34-41c7-a285-634d80b3675d.txt
[0:12] Anna Berger: We agreed on the rollout plan for October.
[0:18] Speaker 2: Then I will update the timeline today.

6. Endpoints at a glance​

Each entry leads to the reference, where you can send the request with your own token.