Wie du eine Aufnahme zur Transkription hochlädst
Du kannst Sally eine Datei geben, die woanders aufgenommen wurde, und bekommst Transkript und Zusammenfassung zurück, genau wie bei einem aufgezeichneten Meeting. Es gibt zwei Wege, je nachdem, wo die Datei liegt: Sally holt sie von einer URL ab, oder du lädst sie selbst in drei Schritten hoch.
Jedes Beispiel funktioniert so, wie es dasteht, sobald SALLY_TOKEN und DIRECTORY_ID gesetzt sind, siehe Schnellstart. Wähl deine Sprache einmal, alle Beispiele im API-Bereich folgen ihr.
Lies zuerst den Schnellstart und Limits und Fair Usage.
Schnellnavigation
- Die Datei ist per URL erreichbar
- Du hast die Datei selbst
- Formate, Größe und Kontingent
- Vollständiges Skript: eine Datei komplett hochladen
- Endpunkte im Überblick
1. Die Datei ist per URL erreichbar
Ein Aufruf genügt. Sally holt die Datei serverseitig ab und startet Transkription und Zusammenfassung, genau wie nach einem abgeschlossenen Upload:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
url | string | ja | Öffentlich erreichbare https-URL der Audio- oder Videodatei selbst, keine Landingpage. |
name | string | null | nein | Name der Aufnahme. Ohne Angabe wird ein Name erzeugt, nie die URL. |
languageCode | string | null | nein | IETF-Sprachtag des Audios, z. B. de-DE. Weglassen für automatische Erkennung. |
speakerCount | number | null | nein | Anzahl der Sprecher, für die Sprechertrennung. |
- cURL
- JavaScript
- Python
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/from-url" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://files.example.com/weekly.mp4",
"name": "Wochenmeeting"
}'
const response = await fetch(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/from-url`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://files.example.com/weekly.mp4',
name: 'Wochenmeeting',
}),
},
);
const {recordingId} = await response.json();
import os
import requests
response = requests.post(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/from-url",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
json={
"url": "https://files.example.com/weekly.mp4",
"name": "Wochenmeeting",
},
)
recording_id = response.json()["recordingId"]
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d"
}
Der Aufruf kehrt zurück, sobald die Datei abgeholt ist und die Aufnahme existiert, die Transkription läuft danach im Hintergrund. Jede dieser Regeln wird geprüft, ein Verstoß ergibt 400:
- Nur
https, kein anderes Schema. - Keine Zugangsdaten in der URL, eine Form wie
user:pass@hostwird abgelehnt. - Nur öffentliche Hosts, Adressen in privaten oder reservierten Netzen werden abgelehnt.
- Keine Weiterleitungen. Gib die endgültige Adresse an, eine Weiterleitung wird abgelehnt statt verfolgt.
- Ein unterstütztes Medienformat, erkannt am
Content-Typeoder an der Dateiendung, siehe Formate, Größe und Kontingent. - Die Größengrenze, die der Upload-Endpunkt als
maxBytesliefert.
Vorsignierte Links von S3, Azure Blob oder Google Cloud Storage funktionieren, solange sie zum Zeitpunkt des Aufrufs gültig sind. Deshalb wird die URL auch nie als Name der Aufnahme verwendet: Solche Links tragen Zugangsdaten in ihrem Query-String.
2. Du hast die Datei selbst
Der Upload hat drei Schritte. Die Bytes der Datei laufen nicht über die Sally API, sie gehen direkt in den Speicher:
- Upload anfragen bei der Sally API.
- Datei hochladen, per
PUTan dieuploadUrl, die du bekommen hast. - Upload abschließen bei der Sally API. Erst jetzt entsteht die Aufnahme.
Schritt 1: Upload anfragen
Du bekommst eine uploadId, eine uploadUrl und die requiredHeaders, die dazugehören. Das Transkriptionskontingent wird schon hier geprüft, eine Ablehnung kostet dich also nichts.
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
fileName | string | ja | Ursprünglicher Dateiname mit Endung. |
mimeType | string | ja | MIME-Typ der Datei, z. B. video/mp4 oder audio/mpeg. |
sizeBytes | number | ja | Dateigröße in Bytes, größer als 0 und höchstens 5 GB. |
- cURL
- JavaScript
- Python
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/uploads" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fileName": "weekly.mp4",
"mimeType": "video/mp4",
"sizeBytes": 184320000
}'
const response = await fetch(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/uploads`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: 'weekly.mp4',
mimeType: 'video/mp4',
sizeBytes: 184320000,
}),
},
);
const {uploadId, uploadUrl, requiredHeaders} = await response.json();
import os
import requests
response = requests.post(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/uploads",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
json={
"fileName": "weekly.mp4",
"mimeType": "video/mp4",
"sizeBytes": 184320000,
},
)
upload = response.json()
{
"uploadId": "2054a871-e250-4b17-a44c-8dbda99714cd",
"uploadUrl": "https://example.blob.core.windows.net/uploads/2054a871-e250-4b17-a44c-8dbda99714cd?sv=...&sig=...",
"storageMode": "azure-blob",
"requiredHeaders": {
"x-ms-blob-type": "BlockBlob"
},
"maxBytes": 5368709120
}
| Feld | Typ | Beschreibung |
|---|---|---|
uploadId | string | Upload-Token, das du beim Abschließen mitgibst. |
uploadUrl | string | Absolute Azure-SAS-URL. An sie schickst du die Bytes der Datei per PUT. |
storageMode | string | Speichermodus, z. B. azure-blob. |
requiredHeaders | object | Header, die beim PUT mitgeschickt werden müssen. |
maxBytes | number | Höchste erlaubte Upload-Größe in Bytes. |
Schritt 2: Datei hochladen
Schick die Datei per PUT direkt an die uploadUrl, mit allen Headern aus requiredHeaders. Diese Anfrage läuft nicht über die Sally API, sie nimmt deshalb diese Header statt deines Sally-Tokens.
- cURL
- JavaScript
- Python
# UPLOAD_URL ist die uploadUrl aus Schritt 1, der Header stammt aus requiredHeaders
curl -X PUT "$UPLOAD_URL" \
-H "x-ms-blob-type: BlockBlob" \
--upload-file weekly.mp4
import {openAsBlob} from 'node:fs';
// Streamt die Datei von der Platte, statt sie in den Speicher zu laden
const file = await openAsBlob('weekly.mp4');
const put = await fetch(uploadUrl, {
method: 'PUT',
headers: requiredHeaders,
body: file,
});
if (!put.ok) throw new Error(`Upload fehlgeschlagen: ${put.status}`);
import requests
# Die geöffnete Datei wird gestreamt, statt sie in den Speicher zu laden
with open("weekly.mp4", "rb") as f:
put = requests.put(upload["uploadUrl"], headers=upload["requiredHeaders"], data=f)
put.raise_for_status()
Schritt 3: Upload abschließen
Erst jetzt entsteht die Aufnahme und die Transkription startet. Sally prüft die gespeicherte Datei, legt die Aufnahme an und gibt ihre recordingId zurück. Der Body ist optional:
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
languageCode | string | null | nein | IETF-Sprachtag des Audios, z. B. de-DE. Weglassen für automatische Erkennung. |
speakerCount | number | null | nein | Anzahl der Sprecher, für die Sprechertrennung. |
appointmentId | string | null | nein | Termin, mit dem die Aufnahme verknüpft wird. |
choice | copy | replace | null | nein | Nur nötig, wenn der verknüpfte Termin schon eine aktive Aufnahme hat, sonst wird es ignoriert. copy legt eine Kopie des Termins für dich an und hängt die Datei dort an, die bestehende Aufnahme bleibt am ursprünglichen Termin. replace hängt die Datei an den Termin und entfernt die bestehende Aufnahme, sobald die neue angelegt ist. |
Hat der Termin schon eine Aufnahme und du lässt choice weg, schlägt der Aufruf mit 409 und dem Code RECORDING_EXISTS_CHOICE_REQUIRED fehl (siehe Die Fehlercodes). Die hochgeladene Datei bleibt erhalten, du kannst denselben Aufruf also mit choice auf copy oder replace wiederholen, ohne neu hochzuladen.
- cURL
- JavaScript
- Python
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/uploads/$UPLOAD_ID/finalize" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"languageCode": "de-DE",
"speakerCount": 2
}'
const response = await fetch(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/uploads/${uploadId}/finalize`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({languageCode: 'de-DE', speakerCount: 2}),
},
);
const {recordingId} = await response.json();
import os
import requests
response = requests.post(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/uploads/{upload_id}/finalize",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
json={"languageCode": "de-DE", "speakerCount": 2},
)
recording_id = response.json()["recordingId"]
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d"
}
Schritt 3 zu überspringen ist der klassische Fehler. Die Datei liegt dann zwar da, aber es entsteht keine Aufnahme. Also taucht in Sally nichts auf und es wird nichts transkribiert.
3. Formate, Größe und Kontingent
Unterstützt werden Videodateien (mp4, mkv, avi, mov, webm) und Audiodateien (mp3, wav, flac, ogg, amr, m4a, opus, aac) bis jeweils 5 GB. Für die Transkription gilt außerdem eine Fair-Usage-Regel, gleichermaßen für den Upload und den URL-Import. Die letzte Zeile ist der eine Konflikt, auf den das Abschließen stoßen kann:
| Status | code | Kommt von | Was zu tun ist |
|---|---|---|---|
| 429 | FUP_LIMIT_EXCEEDED | Upload anfragen | Das Kontingent ist aufgebraucht. Ein baldiger neuer Versuch bringt dieselbe Antwort. |
| 429 | TOO_MANY_CONCURRENT_TRANSCRIPTIONS | Abschließen, URL-Import | Zu viele Transkriptionen deines Unternehmenskontos laufen gleichzeitig. Warte, bis laufende fertig sind. Beim URL-Import wird dann nichts abgeholt. |
| 429 | keiner | Jede Anfrage | Gewöhnliches Rate-Limit. Mach langsamer und versuch es später erneut. |
| 409 | RECORDING_EXISTS_CHOICE_REQUIRED | Abschließen mit appointmentId | Der Termin hat schon eine Aufnahme. Wiederhole den Aufruf mit choice auf copy oder replace, die hochgeladene Datei bleibt erhalten. |
Fehler nutzen den gemeinsamen Fehler-Body, die Fälle unterscheidest du also an code:
{
"statusCode": 429,
"message": "Transcription quota exhausted.",
"code": "FUP_LIMIT_EXCEEDED"
}
Behandle diese beiden Codes getrennt von einem gewöhnlichen Rate-Limit. Kurz zu warten hilft beim Rate-Limit und bei gleichzeitigen Transkriptionen, beim aufgebrauchten Kontingent aber nicht.
4. Vollständiges Skript: eine Datei komplett hochladen
Dieses Skript lädt eine lokale Datei hoch, schließt den Upload ab und wartet, bis der Transkriptionslauf beendet ist. Es reagiert auf die drei 429-Fälle aus der Tabelle oben: Bei FUP_LIMIT_EXCEEDED hört es auf, bei TOO_MANY_CONCURRENT_TRANSCRIPTIONS wartet es und schließt erneut ab, bei einem gewöhnlichen Rate-Limit wiederholt es mit wachsenden Pausen. Setz FILE und MIME_TYPE auf deine Datei. Die Wartezeiten von 60 und 30 Sekunden hat das Skript gewählt, sie sind keine Werte der API.
- cURL
- JavaScript
- Python
#!/usr/bin/env bash
# Braucht curl und jq.
set -euo pipefail
FILE="${FILE:-weekly.mp4}"
MIME_TYPE="${MIME_TYPE:-video/mp4}"
API="https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings"
AUTH="Authorization: Bearer $SALLY_TOKEN"
# Schickt ein POST, gibt den Body aus und hört bei FUP_LIMIT_EXCEEDED auf
post() {
local out status
out=$(mktemp)
while : ; do
status=$(curl -s -o "$out" -w '%{http_code}' -X POST "$1" \
-H "$AUTH" -H "Content-Type: application/json" -d "$2")
[ "$status" = "429" ] || break
case "$(jq -r '.code // empty' "$out")" in
FUP_LIMIT_EXCEEDED) echo "Transkriptionskontingent aufgebraucht." >&2; exit 1 ;;
TOO_MANY_CONCURRENT_TRANSCRIPTIONS) sleep 60 ;;
*) sleep 5 ;;
esac
done
[ "${status:0:1}" = "2" ] || { echo "HTTP $status: $(cat "$out")" >&2; exit 1; }
cat "$out"
}
# 1. Upload anfragen
size=$(wc -c < "$FILE" | tr -d ' ')
upload=$(post "$API/uploads" \
"{\"fileName\": \"$(basename "$FILE")\", \"mimeType\": \"$MIME_TYPE\", \"sizeBytes\": $size}")
# 2. Bytes per PUT hochladen, mit allen Headern aus requiredHeaders
headers=()
while IFS= read -r h; do headers+=(-H "$h"); done \
< <(echo "$upload" | jq -r '.requiredHeaders | to_entries[] | "\(.key): \(.value)"')
curl -sf -X PUT "$(echo "$upload" | jq -r '.uploadUrl')" "${headers[@]}" --upload-file "$FILE"
# 3. Abschließen
recording_id=$(post "$API/uploads/$(echo "$upload" | jq -r '.uploadId')/finalize" '{}' \
| jq -r '.recordingId')
echo "Aufnahme angelegt: $recording_id"
# 4. Warten, bis der Transkriptionslauf beendet ist
while : ; do
rec=$(curl -sf --retry 5 "$API/$recording_id?fields=isTranscriptionCompleted,transcriptionCompletionReason" -H "$AUTH")
[ "$(echo "$rec" | jq '.isTranscriptionCompleted')" = "true" ] && break
sleep 30
done
echo "Transkription beendet: $(echo "$rec" | jq -r '.transcriptionCompletionReason')"
import {openAsBlob} from 'node:fs';
import {basename} from 'node:path';
const FILE = process.env.FILE ?? 'weekly.mp4';
const MIME_TYPE = process.env.MIME_TYPE ?? 'video/mp4';
const API = `https://api.sally.io/v1.0/directories/${process.env.DIRECTORY_ID}/recordings`;
const AUTH = {Authorization: `Bearer ${process.env.SALLY_TOKEN}`};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Ruft die Sally API auf und behandelt die drei 429-Fälle
async function api(path, {method = 'GET', body} = {}) {
for (let wait = 1000; ; wait = Math.min(wait * 2, 60_000)) {
const response = await fetch(`${API}${path}`, {
method,
headers: body ? {...AUTH, 'Content-Type': 'application/json'} : AUTH,
body: body ? JSON.stringify(body) : undefined,
});
if (response.ok) return response.json();
const error = await response.json().catch(() => ({}));
if (response.status !== 429) throw new Error(`${response.status} ${error.message ?? ''}`);
if (error.code === 'FUP_LIMIT_EXCEEDED') throw new Error('Transkriptionskontingent aufgebraucht.');
await sleep(error.code === 'TOO_MANY_CONCURRENT_TRANSCRIPTIONS' ? 60_000 : wait);
}
}
const file = await openAsBlob(FILE);
// 1. Upload anfragen
const {uploadId, uploadUrl, requiredHeaders} = await api('/uploads', {
method: 'POST',
body: {fileName: basename(FILE), mimeType: MIME_TYPE, sizeBytes: file.size},
});
// 2. Bytes per PUT hochladen, mit den Headern aus requiredHeaders
const put = await fetch(uploadUrl, {method: 'PUT', headers: requiredHeaders, body: file});
if (!put.ok) throw new Error(`Upload fehlgeschlagen: ${put.status}`);
// 3. Abschließen
const {recordingId} = await api(`/uploads/${uploadId}/finalize`, {method: 'POST', body: {}});
console.log(`Aufnahme angelegt: ${recordingId}`);
// 4. Warten, bis der Transkriptionslauf beendet ist
let recording;
for (;;) {
recording = await api(
`/${recordingId}?fields=isTranscriptionCompleted,transcriptionCompletionReason`,
);
if (recording.isTranscriptionCompleted) break;
await sleep(30_000);
}
console.log(`Transkription beendet: ${recording.transcriptionCompletionReason}`);
import os
import time
import requests
FILE = os.environ.get("FILE", "weekly.mp4")
MIME_TYPE = os.environ.get("MIME_TYPE", "video/mp4")
API = f"https://api.sally.io/v1.0/directories/{os.environ['DIRECTORY_ID']}/recordings"
AUTH = {"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"}
def api(method, path, body=None):
"""Ruft die Sally API auf und behandelt die drei 429-Fälle."""
wait = 1
while True:
response = requests.request(method, f"{API}{path}", headers=AUTH, json=body)
if response.ok:
return response.json()
if response.status_code != 429:
response.raise_for_status()
code = response.json().get("code")
if code == "FUP_LIMIT_EXCEEDED":
raise SystemExit("Transkriptionskontingent aufgebraucht.")
if code == "TOO_MANY_CONCURRENT_TRANSCRIPTIONS":
time.sleep(60)
else:
time.sleep(wait)
wait = min(wait * 2, 60)
# 1. Upload anfragen
upload = api("POST", "/uploads", {
"fileName": os.path.basename(FILE),
"mimeType": MIME_TYPE,
"sizeBytes": os.path.getsize(FILE),
})
# 2. Bytes per PUT hochladen, mit den Headern aus requiredHeaders
with open(FILE, "rb") as f:
put = requests.put(upload["uploadUrl"], headers=upload["requiredHeaders"], data=f)
put.raise_for_status()
# 3. Abschließen
recording_id = api("POST", f"/uploads/{upload['uploadId']}/finalize", {})["recordingId"]
print(f"Aufnahme angelegt: {recording_id}")
# 4. Warten, bis der Transkriptionslauf beendet ist
while True:
recording = api("GET", f"/{recording_id}?fields=isTranscriptionCompleted,transcriptionCompletionReason")
if recording["isTranscriptionCompleted"]:
break
time.sleep(30)
print(f"Transkription beendet: {recording['transcriptionCompletionReason']}")
Für den Import von einer URL ersetzt du die Schritte 1 bis 3 durch den einzelnen from-url-Aufruf aus Abschnitt 1. Seine 429-Behandlung ist dieselbe wie beim Abschließen. Statt regelmäßig abzufragen, kannst du auch einen Webhook registrieren. Sobald das Transkript fertig ist, zeigen Transkript abrufen und Zusammenfassungen abrufen, wie du die Ergebnisse liest.
5. Endpunkte im Überblick
Jeder Eintrag führt in die Referenz, wo du die Anfrage mit deinem eigenen Token abschicken kannst.
- POST
…/recordings/from-urlErstellt eine Aufnahme aus einer Datei-URL (kein Upload nötig)Ausprobieren - POST
…/recordings/uploadsStartet einen Aufnahme-Upload (Schritt 1/2) und gibt eine SAS-PUT-URL zurückAusprobieren - POST
…/recordings/uploads/{uploadId}/finalizeSchließt einen Aufnahme-Upload ab (Schritt 2/2) und erstellt die AufnahmeAusprobieren - GET
…/recordings/{recordingId}Gibt eine einzelne Aufnahme zurückAusprobieren