Zum Hauptinhalt springen

Wie du Meeting-Zusammenfassungen über die Sally API abrufst

Zusammenfassungen abzurufen ist der häufigste Grund, die Sally API zu nutzen: Du bringst das, was Sally vom Meeting verstanden hat, dorthin, wo dein Team ohnehin arbeitet. Diese Anleitung zeigt, wie eine Zusammenfassung aufgebaut ist, wie du sie abrufst oder durchsuchst, an welchen Statusfeldern du erkennst, ob es etwas zu lesen gibt, wie du sie als fertige PDF- oder Word-Datei herunterlädst, und endet mit einem vollständigen Skript, das alle neuen Zusammenfassungen seit dem letzten Lauf importiert.

Jedes Beispiel funktioniert so, wie es dasteht, sobald SALLY_TOKEN und DIRECTORY_ID (und wo eine Aufnahme gebraucht wird, RECORDING_ID) gesetzt sind, siehe Schnellstart. Wähl deine Sprache einmal, alle Beispiele im API-Bereich folgen ihr.

Schnellnavigation

  1. Was eine Zusammenfassung enthält
  2. Die Zusammenfassungen einer Aufnahme abrufen
  3. Eine einzelne Zusammenfassung abrufen
  4. Zusammenfassungen durchsuchen
  5. Prüfen, ob es etwas zu lesen gibt
  6. Als Datei herunterladen
  7. Vollständiges Skript: neue Zusammenfassungen importieren
  8. Endpunkte im Überblick

1. Was eine Zusammenfassung enthält​

Eine Zusammenfassung gehört zu genau einer Aufnahme. Eine Aufnahme kann mehrere Zusammenfassungen haben, eine je Sprache. Jede besteht aus einem Zusammenfassungstext (summary) und einer Liste von Abschnittseinträgen (sectionItems). Die Abschnitte stammen aus dem angewendeten Meetingtemplate, ein Vertriebsgespräch liefert also andere Abschnitte als ein technisches Review.

FeldTypBeschreibung
recordingSummaryIdstringId der Zusammenfassung.
recordingIdstringId der Aufnahme, aus der die Zusammenfassung erzeugt wurde.
appointmentIdstring | nullId des verknüpften Termins, null, wenn es keinen gibt.
languageCodestring | nullSprache der Zusammenfassung als IETF-Sprachtag, z. B. de-DE oder en-US.
summarystring | nullDer Zusammenfassungstext.
isSummarizationCompletedbooleantrue, sobald der Lauf beendet ist, egal mit welchem Ergebnis.
isSummarizationSucceededbooleantrue, wenn es etwas zu lesen gibt.
summarizationCompletionReasonstringWarum der Lauf endete, siehe Prüfen, ob es etwas zu lesen gibt.
createdOnstringZeitpunkt der Erstellung (ISO-8601).
sectionItemsarray | nullDie erzeugten Abschnittseinträge, null, wenn sie nicht mitgeladen wurden.

Jeder Abschnittseintrag trägt seinen sectionType und seinen Inhalt in content. Das Feld outputFormat sagt dir, ob dieser Inhalt html oder markdown ist. Aufgabenartige Einträge tragen zusätzlich subject, description, responsibleUserName, responsibleUserEmail und dueDate. Die Abschnittstypen table, appointment und email legen ihr Ergebnis in eigenen Feldern ab und schreiben zusätzlich eine lesbare Markdown-Fassung davon in content. So zeigt auch ein Client, der nur content liest, jeden Abschnitt an.

FeldTypBeschreibung
sectionItemIdstringId des Abschnittseintrags.
meetingTemplateSectionIdstring | nullId des Template-Abschnitts, aus dem der Eintrag entstand, null bei abgeleiteten Einträgen wie Aufgaben.
titlestringTitel des Abschnittseintrags.
sectionTypestringsummary, tasks, topics, decisions, customList, freeText, table, appointment, email oder unknown.
outputFormatstringFormat von content: html, markdown oder unknown.
sortOrdernumberReihenfolge innerhalb der Zusammenfassung.
contentstring | nullDer Inhalt des Eintrags. Bei table, appointment und email eine lesbare Markdown-Fassung der eigenen Felder, outputFormat ist dann markdown.
subjectstring | nullBetreff der Aufgabe, bei aufgabenartigen Einträgen.
descriptionstring | nullBeschreibung der Aufgabe.
responsibleUserNamestring | nullName der verantwortlichen Person.
responsibleUserEmailstring | nullE-Mail der verantwortlichen Person.
dueDatestring | nullFälligkeitsdatum (ISO-8601).
tableVariantstring | nullNur bei table: structured, wenn das Template die Spalten festlegt und das Ergebnis in tableData steht, free, wenn Sally die Spalten selbst gewählt hat und das Ergebnis eine Markdown-Tabelle in content ist.
tableDataobject | nullSpalten und Zeilen einer structured-Tabelle, sonst null.
followUpMeetingStartTimestring | nullNur bei appointment: Beginn des vorgeschlagenen Folgetermins als lokale Uhrzeit ohne Zeitzone, z. B. 2026-10-16T09:00:00. Wende die Zeitzone der Beteiligten an.
followUpMeetingEndTimestring | nullEnde im selben Format, null, wenn im Meeting nur ein Beginn genannt wurde.
followUpMeetingParticipantsarray | nullNur bei appointment: die für den Folgetermin genannten Personen, jeweils mit userId, name, email und isExternal.
emailRecipientsarray | nullNur bei email: die Empfänger mit userId, name, email und kind (to oder cc). Betreff und Text der E-Mail stehen in subject und description.

2. Die Zusammenfassungen einer Aufnahme abrufen​

Wenn du die Aufnahme kennst, frag sie nach ihren Zusammenfassungen. Hänge includeDetails=true an, sonst ist sectionItems gleich null:

ParameterTypStandardBeschreibung
includeDetailsbooleanfalseBei true trägt jede Zusammenfassung zusätzlich ihre sectionItems.
fieldsstringalle FelderKommagetrennte Liste der gewünschten Felder, siehe Feldauswahl.
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/summaries?includeDetails=true" \
-H "Authorization: Bearer $SALLY_TOKEN"

Die Antwort ist ein einfaches JSON-Array, nicht geblättert, mit einem Eintrag je Sprache:

200 OK
[
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"summary": "Das Team hat sich auf den Rollout-Plan für Oktober geeinigt und die offenen Aufgaben verteilt.",
"isSummarizationCompleted": true,
"isSummarizationSucceeded": true,
"summarizationCompletionReason": "succeeded",
"createdOn": "2026-09-24T09:30:00Z",
"sectionItems": [
{
"sectionItemId": "39ec96ee-9578-4082-a103-aa168fcd18f6",
"meetingTemplateSectionId": "0ba1ea56-516d-485a-a738-e65e51578bbe",
"title": "Entscheidungen",
"sectionType": "decisions",
"outputFormat": "html",
"sortOrder": 1,
"content": "Wir haben uns auf den Rollout-Plan für Oktober geeinigt.",
"subject": null,
"description": null,
"responsibleUserName": null,
"responsibleUserEmail": null,
"dueDate": null,
"tableVariant": null,
"tableData": null,
"followUpMeetingStartTime": null,
"followUpMeetingEndTime": null,
"followUpMeetingParticipants": null,
"emailRecipients": null
}
]
}
]
Empfehlung

Abschnittseinträge machen den Großteil der Antwort aus. Wenn du nur den Zusammenfassungstext brauchst, lass includeDetails weg. Wie du Antworten noch weiter verkleinerst, zeigt Feldauswahl.

3. Eine einzelne Zusammenfassung abrufen​

Wenn du schon eine recordingSummaryId hast, etwa aus einem Suchtreffer, rufst du diese Zusammenfassung direkt ab. Dieser Endpunkt liefert sectionItems immer mit, ohne Parameter. Er antwortet mit 404, wenn die Zusammenfassung nicht existiert oder du keinen Zugriff auf die zugehörige Aufnahme hast.

curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID" \
-H "Authorization: Bearer $SALLY_TOKEN"

Die Antwort ist ein einzelnes Objekt mit denselben Feldern wie ein Eintrag aus Abschnitt 2.

4. Zusammenfassungen durchsuchen​

Die Suche deckt den Zusammenfassungstext, die erzeugten Abschnittseinträge (Titel, Inhalt, Betreff und Beschreibung) und die Zellen strukturierter Tabellen in einem Rutsch ab. Eine Aufnahme ist ein Treffer, wenn irgendeines davon passt. Der Suchbegriff braucht mindestens 3 Zeichen und steht im Request-Body:

FeldTypStandardBeschreibung
searchstringTeilstring ohne Rücksicht auf Groß- und Kleinschreibung, mindestens 3 Zeichen.
pagenumber1Seitennummer, beginnend bei 1.
pageSizenumber25Einträge je Seite, höchstens 100.
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/search" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"search": "Preisgestaltung",
"page": 1,
"pageSize": 25
}'

Jeder Treffer liefert einen kurzen snippet und die Ids, mit denen du die vollständige Zusammenfassung abrufst:

200 OK
{
"page": 1,
"pageSize": 25,
"total": 3,
"hasMore": false,
"items": [
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"snippet": "Das Team hat die neue Preisgestaltung besprochen und will sie mit zwei Kunden testen.",
"createdOn": "2026-09-24T09:30:00Z"
}
]
}
FeldTypBeschreibung
recordingSummaryIdstringId der Zusammenfassung. Gib sie an Eine einzelne Zusammenfassung abrufen weiter.
recordingIdstringId der Aufnahme.
appointmentIdstring | nullId des verknüpften Termins, falls vorhanden.
languageCodestring | nullSprache der Zusammenfassung.
snippetstringGekürzter Auszug aus dem Zusammenfassungstext, höchstens 300 Zeichen.
createdOnstringZeitpunkt der Erstellung (ISO-8601).

Die Antwort ist geblättert wie jede Liste. Warum die Suche ihre Parameter im Body erwartet, erklärt Paginierung und Suche.

5. Prüfen, ob es etwas zu lesen gibt​

  • isSummarizationCompleted sagt, dass der Lauf beendet ist, auch wenn er nichts ergeben hat.
  • isSummarizationSucceeded sagt, dass es etwas zu lesen gibt: einen Zusammenfassungstext oder mindestens einen Abschnittseintrag. Dieses Feld solltest du prüfen.
  • summarizationCompletionReason sagt, warum der Lauf endete.
summarizationCompletionReasonBedeutung
succeededEs gibt einen Zusammenfassungstext oder mindestens einen Abschnittseintrag.
noTranscriptEs gab nichts zusammenzufassen. Ein erwartetes Ergebnis, kein Fehler.
emptyResultDer Lauf endete ohne Ergebnis.
errorDer Lauf ist fehlgeschlagen. Der eigentliche Fehlertext wird bewusst nicht herausgegeben.
unknownDer Lauf ist noch nicht beendet.
Beispiel

Ein nächtlicher Import listet mit createdAfter die seit dem letzten Lauf entstandenen Aufnahmen, holt ihre Zusammenfassungen und überspringt jeden Eintrag, bei dem isSummarizationSucceeded gleich false ist. Als Schlüssel speichert er die recordingId zusammen mit dem languageCode, nie den Termin. Genau das zeigt der nächste Abschnitt.

6. Als Datei herunterladen​

Manchmal brauchst du nicht die Daten, sondern das fertige Dokument, etwa um es an einen CRM-Eintrag zu hängen oder in einem Dokumentenmanagementsystem abzulegen. GET …/summaries/{recordingSummaryId}/document liefert genau die Datei, die ein Nutzer in der Sally-Weboberfläche herunterlädt: mit Branding, Logos, Kopf- und Fußzeile, Tabellen, der Abschnittsreihenfolge des Meetingtemplates und demselben Dateinamen. Die Datei wird bei der Anfrage erzeugt und kommt direkt in der Antwort zurück, es gibt keinen Auftrag, den du abfragen musst.

Das Dokument enthält immer die vollständige Zusammenfassung in der Sprachfassung dieser recordingSummaryId. Eine Aufnahme hat eine Zusammenfassung pro Sprache, such dir also zuerst die passende Id heraus, zum Beispiel über Abschnitt 2. Eigene Einblicke und einzelne Abschnitte sind nicht Teil des Dokuments.

ParameterTypPflichtBeschreibung
formatstringjapdf, docx, odt, txt oder md.
languageCodestringneinde-DE, en-US, es-ES, fr-FR, it-IT, nl-NL oder pl-PL. Ohne Angabe gilt die Sprache des Token-Inhabers, wie in der Weboberfläche.
# -OJ speichert die Datei unter dem Namen aus Content-Disposition
curl -fOJ "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID/document?format=pdf&languageCode=de-DE" \
-H "Authorization: Bearer $SALLY_TOKEN"
Hinweis

languageCode ändert nur die Beschriftungen und das Datumsformat im Dokument, nicht den Inhalt. Die Schrift richtet sich immer nach der Sprache der Aufnahme, chinesische Schriftzeichen bleiben also auch mit en-US-Beschriftungen lesbar. ?fields= wird bei diesem Endpunkt nicht unterstützt.

Die Antwort ist 200 mit der Datei selbst, kein JSON. Der Header Content-Disposition enthält den Dateinamen, den die Weboberfläche für denselben Download verwendet, in ASCII in filename und in UTF-8 in filename*. Der Content-Type richtet sich nach dem Format:

formatContent-Type
pdfapplication/pdf
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
odtapplication/vnd.oasis.opendocument.text
txttext/plain; charset=utf-8
mdtext/markdown; charset=utf-8

Neben den üblichen Fehlern antwortet dieser Endpunkt mit:

StatuscodeBedeutung
400UNKNOWN_DOCUMENT_FORMAT oder UNKNOWN_LANGUAGE_CODEformat fehlt oder ist unbekannt, oder languageCode ist unbekannt. Der Body nennt die erlaubten Werte.
403SUMMARY_DOWNLOAD_PREVENTEDDas Unternehmenskonto hat diesen Download für seine Mitglieder gesperrt.
404Die Zusammenfassung gibt es nicht, sie gehört zu einem anderen Unternehmenskonto, oder du hast keinen Zugriff auf die zugrunde liegende Aufnahme.
409SUMMARY_HAS_NO_CONTENTDie Zusammenfassung hat keinen Inhalt für ein Dokument, weil sie entstand, bevor es Meetingtemplates gab, oder ihr Template keine Abschnitte hat.

Unter Fehler siehst du, wie du diese Codes an einer Stelle behandelst.

7. Vollständiges Skript: neue Zusammenfassungen importieren​

Dieses Skript importiert alle Zusammenfassungen, die seit seinem letzten Lauf entstanden sind. Zwischen zwei Läufen merkt es sich zwei Dinge: wann es zuletzt lief und welche Aufnahmen noch in Bearbeitung waren. Diese prüft es beim nächsten Mal erneut, eine Zusammenfassung, die noch nicht fertig war, geht also nicht verloren.

Für jede Aufnahme entscheidet es:

  • Transkription läuft noch: für den nächsten Lauf vormerken.
  • Transkription beendet, aber ohne Transkript: überspringen, es wird keine Zusammenfassung geben.
  • Zusammenfassungen noch nicht fertig: für den nächsten Lauf vormerken.
  • Sonst: jede Zusammenfassung mit isSummarizationSucceeded gleich true importieren, mit recordingId plus languageCode als Schlüssel.

Das Skript schreibt die Zusammenfassungen in eine lokale Datei. Ersetze diesen Teil durch den Aufruf deines CRM, Wikis oder Ticketsystems.

#!/usr/bin/env bash
# Braucht curl und jq. curl --retry wiederholt eine Anfrage nach einem 429 mit wachsenden Pausen.
set -euo pipefail
API="https://api.sally.io/v1.0/directories/$DIRECTORY_ID"
AUTH="Authorization: Bearer $SALLY_TOKEN"
CURL=(curl -sf --retry 5 -H "$AUTH")

since=$(cat .sally-last-run 2>/dev/null || echo "2026-01-01T00:00:00Z")
started=$(date -u +%Y-%m-%dT%H:%M:%SZ)
touch .sally-pending

# Neue Aufnahmen seit dem letzten Lauf, dazu die noch vorgemerkten
ids=$(cat .sally-pending)
page=1
while : ; do
body=$("${CURL[@]}" -G "$API/recordings" \
--data-urlencode "createdAfter=$since" \
-d "page=$page" -d "pageSize=100" -d "fields=recordingId")
ids="$ids"$'\n'"$(echo "$body" | jq -r '.items[].recordingId')"
[ "$(echo "$body" | jq '.hasMore')" = "true" ] || break
page=$((page + 1))
done

: > .sally-pending.next
for id in $(echo "$ids" | grep -v '^$' | sort -u); do
rec=$("${CURL[@]}" "$API/recordings/$id?fields=isTranscriptionCompleted,isTranscriptionSucceeded")
if [ "$(echo "$rec" | jq '.isTranscriptionCompleted')" != "true" ]; then
echo "$id" >> .sally-pending.next; continue
fi
[ "$(echo "$rec" | jq '.isTranscriptionSucceeded')" = "true" ] || continue

summaries=$("${CURL[@]}" "$API/recordings/$id/summaries?includeDetails=true")
if [ "$(echo "$summaries" | jq 'length > 0 and all(.[]; .isSummarizationCompleted)')" != "true" ]; then
echo "$id" >> .sally-pending.next; continue
fi
# Diese Zeile durch den Aufruf deines Zielsystems ersetzen
echo "$summaries" | jq -c '.[] | select(.isSummarizationSucceeded)' >> summaries.jsonl
done

mv .sally-pending.next .sally-pending
echo "$started" > .sally-last-run
Empfehlung

Lass das Skript regelmäßig laufen, zum Beispiel jede Nacht. Willst du von jeder Zusammenfassung sofort erfahren, registrier einen Webhook für summary.ready. Seine Nutzdaten haben dieselbe Form wie die Zusammenfassungen oben, ein Parser reicht also für beide Wege.

8. Endpunkte im Überblick​

Jeder Eintrag führt in die Referenz, wo du die Anfrage mit deinem eigenen Token abschicken kannst.