Wie du deinen ersten Aufruf an die Sally API machst
Dieser Schnellstart bringt dich von null zur Zusammenfassung eines echten Meetings, in vier Schritten. Jedes Beispiel funktioniert so, wie es dasteht, sobald dein eigenes Token eingesetzt ist, und zu jedem Schritt siehst du die Antwort, die zurückkommt. So weißt du, was du für den nächsten Aufruf daraus brauchst.
Du brauchst ein Sally-Konto mit Zugang zu mindestens einem Unternehmenskonto und ein Meeting, das Sally bereits zusammengefasst hat. Eine App-Registrierung oder Freigabe ist nicht nötig. Wähl deine Sprache einmal, alle Beispiele im API-Bereich folgen ihr.
Schnellnavigation
- Token anlegen
- Unternehmenskonto finden
- Aufnahmen auflisten
- Zusammenfassung lesen
- Bevor du darauf aufbaust
1. Token anlegen
Öffne in Sally die Einstellungen, wähle unter Konfiguration die Integrationen und wechsle in den Reiter Personal Access Token. Lege ein Token an und kopiere es sofort.
Das Token beginnt mit sally_user_api_. Es gehört dir und handelt mit deinen Rechten: Es erreicht genau die Unternehmenskonten, in denen du Mitglied bist, und sieht genau das, was du auch in der App siehst. Leg es in einer Umgebungsvariable ab, alle Beispiele lesen es von dort:
export SALLY_TOKEN="sally_user_api_..."
Jede Anfrage schickt es als Bearer-Token im Header Authorization mit. Die Details erklärt Authentifizierung.
2. Unternehmenskonto finden
Fast jeder Pfad braucht eine directoryId, die Id des Sally-Unternehmenskontos, zu dem die Daten gehören. Dieser Aufruf listet die Unternehmenskonten, die dein Token ansprechen darf:
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/me/directories/memberships" \
-H "Authorization: Bearer $SALLY_TOKEN"
const response = await fetch('https://api.sally.io/v1.0/me/directories/memberships', {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const memberships = await response.json();
for (const {directoryId, directoryName} of memberships) {
console.log(directoryId, directoryName);
}
import os
import requests
response = requests.get(
"https://api.sally.io/v1.0/me/directories/memberships",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
response.raise_for_status()
for membership in response.json():
print(membership["directoryId"], membership["directoryName"])
Die Antwort ist ein einfaches JSON-Array mit einem Eintrag je Unternehmenskonto:
[
{
"directoryId": "5daeda15-8fc7-4693-a1e1-13119cceb461",
"directoryName": "Example GmbH",
"roleId": "164531fc-9be4-4de4-a183-89cc85d6a98c",
"licenseId": "f5f084e9-b993-425f-ad36-bfd5dc3f0e65",
"isBlocked": false
}
]
| Feld | Typ | Beschreibung |
|---|---|---|
directoryId | string | Id des Unternehmenskontos. Die brauchen die Pfade. |
directoryName | string | Anzeigename des Unternehmenskontos. |
roleId | string | Deine Rolle in diesem Unternehmenskonto, aufzulösen über GET /v1.0/roles. |
licenseId | string | Deine Lizenz in diesem Unternehmenskonto, aufzulösen über GET /v1.0/licenses. |
isBlocked | boolean | Ob du in diesem Unternehmenskonto gesperrt bist. |
Bekommst du stattdessen ein 401, fehlt das Token oder es ist ungültig. Prüf, ob SALLY_TOKEN in der Shell gesetzt ist, in der du das Beispiel ausführst.
Kopiere die directoryId, mit der du arbeiten willst:
export DIRECTORY_ID="5daeda15-8fc7-4693-a1e1-13119cceb461"
Referenz: GET/v1.0/me/directories/memberships
3. Aufnahmen auflisten
Je aufgezeichnetem Meeting gibt es eine Aufnahme, die neueste zuerst. Frag nur die Felder ab, die du brauchst, dann bleibt die Antwort klein:
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings?pageSize=5&fields=recordingId,name,createdOn" \
-H "Authorization: Bearer $SALLY_TOKEN"
const directoryId = process.env.DIRECTORY_ID;
const url = new URL(`https://api.sally.io/v1.0/directories/${directoryId}/recordings`);
url.searchParams.set('pageSize', '5');
url.searchParams.set('fields', 'recordingId,name,createdOn');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const {items} = await response.json();
for (const {recordingId, name, createdOn} of items) {
console.log(recordingId, createdOn, name);
}
import os
import requests
directory_id = os.environ["DIRECTORY_ID"]
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings",
params={"pageSize": 5, "fields": "recordingId,name,createdOn"},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
response.raise_for_status()
for recording in response.json()["items"]:
print(recording["recordingId"], recording["createdOn"], recording["name"])
Die Liste wird geblättert, deshalb stehen die Aufnahmen in items innerhalb der Seitenhülle. Wegen fields trägt jeder Eintrag nur die drei Felder, die du genannt hast:
{
"page": 1,
"pageSize": 5,
"total": 137,
"hasMore": true,
"items": [
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"name": "Weekly team meeting",
"createdOn": "2026-09-24T09:30:00Z"
},
{
"recordingId": "8f0c2b7a-3d51-4e0f-9a6c-2b1e7d4c9f30",
"name": "Quarterly planning",
"createdOn": "2026-09-23T13:00:00Z"
}
]
}
Die Query-Parameter dieses Endpunkts:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
page | integer | 1 | Seitennummer, beginnend bei 1. |
pageSize | integer | 25 | Einträge je Seite, höchstens 100. |
createdAfter | string | Nur Aufnahmen, die zu oder nach diesem UTC-Zeitpunkt angelegt wurden (YYYY-MM-DDTHH:MM:SSZ). | |
createdBefore | string | Nur Aufnahmen, die zu oder vor diesem UTC-Zeitpunkt angelegt wurden. | |
fields | string | alle Felder | Kommagetrennte Liste der Felder, die zurückkommen sollen, siehe Feldauswahl. |
Wie du alle Seiten abholst, zeigt Paginierung und Suche. Kopiere die recordingId eines Meetings, das schon eine Zusammenfassung hat:
export RECORDING_ID="53e2e041-4b34-41c7-a285-634d80b3675d"
Referenz: GET/v1.0/directories/{directoryId}/recordings
4. Zusammenfassung lesen
Hänge includeDetails=true an, dann bekommst du auch die Abschnittseinträge deines Meetingtemplates. Ohne den Parameter ist sectionItems gleich null und es kommt nur der Zusammenfassungstext.
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/summaries?includeDetails=true" \
-H "Authorization: Bearer $SALLY_TOKEN"
const directoryId = process.env.DIRECTORY_ID;
const recordingId = process.env.RECORDING_ID;
const url = new URL(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/${recordingId}/summaries`,
);
url.searchParams.set('includeDetails', 'true');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const summaries = await response.json();
for (const summary of summaries) {
if (!summary.isSummarizationSucceeded) continue; // nichts zu lesen
console.log(`[${summary.languageCode}] ${summary.summary}`);
for (const item of summary.sectionItems ?? []) {
console.log(` ${item.sectionType}: ${item.title}`);
}
}
import os
import requests
directory_id = os.environ["DIRECTORY_ID"]
recording_id = os.environ["RECORDING_ID"]
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/{recording_id}/summaries",
params={"includeDetails": "true"},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
response.raise_for_status()
for summary in response.json():
if not summary["isSummarizationSucceeded"]:
continue # nichts zu lesen
print(f"[{summary['languageCode']}] {summary['summary']}")
for item in summary["sectionItems"] or []:
print(f" {item['sectionType']}: {item['title']}")
Die Antwort ist ein einfaches JSON-Array mit einer Zusammenfassung je Sprache:
[
{
"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 den Rollout-Plan für Oktober beschlossen und die Launch-Checkliste 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": "<p>Wir haben den Rollout-Plan für Oktober beschlossen.</p>",
"subject": null,
"description": null,
"responsibleUserName": null,
"responsibleUserEmail": null,
"dueDate": null,
"tableVariant": null,
"tableData": null,
"followUpMeetingStartTime": null,
"followUpMeetingEndTime": null,
"followUpMeetingParticipants": null,
"emailRecipients": null
},
{
"sectionItemId": "c2a7e4b1-6f38-4d0a-8e95-1b7c3f2d6a84",
"meetingTemplateSectionId": null,
"title": "Aufgaben",
"sectionType": "tasks",
"outputFormat": "html",
"sortOrder": 2,
"content": null,
"subject": "Launch-Checkliste vorbereiten",
"description": "Offene Punkte aller Teams vor dem Rollout sammeln.",
"responsibleUserName": "Anna Berger",
"responsibleUserEmail": "anna.berger@example.com",
"dueDate": "2026-10-01T00:00:00Z",
"tableVariant": null,
"tableData": null,
"followUpMeetingStartTime": null,
"followUpMeetingEndTime": null,
"followUpMeetingParticipants": null,
"emailRecipients": null
}
]
}
]
Die Felder, die du zuerst brauchst:
| Feld | Typ | Beschreibung |
|---|---|---|
recordingSummaryId | string | Id der Zusammenfassung. |
languageCode | string | null | Sprache der Zusammenfassung, zum Beispiel de-DE oder en-US. |
summary | string | null | Der Zusammenfassungstext. |
isSummarizationCompleted | boolean | true, sobald der Lauf der Zusammenfassung beendet ist, egal ob er etwas erzeugt hat. |
isSummarizationSucceeded | boolean | true, wenn es etwas zu lesen gibt: Zusammenfassungstext oder mindestens einen Abschnittseintrag. |
summarizationCompletionReason | string | Warum der Lauf endete: succeeded, noTranscript, emptyResult, error oder unknown, solange er noch läuft. noTranscript ist ein erwartbares Ergebnis, kein Fehler. |
sectionItems | array | null | Die strukturierten Ergebnisse je Abschnitt deines Meetingtemplates. null ohne includeDetails=true. |
Jeder Abschnittseintrag hat einen sectionType (summary, tasks, topics, decisions, customList, freeText, table, appointment, email oder unknown) und ein outputFormat (html, markdown oder unknown), das dir sagt, wie du content darstellst. Aufgabenartige Einträge tragen stattdessen subject, description, responsibleUserName, responsibleUserEmail und dueDate. Die Typen table, appointment und email legen ihr Ergebnis in eigenen Feldern ab und schreiben zusätzlich eine lesbare Markdown-Fassung in content. Wer nur content liest, sieht also trotzdem jeden Abschnitt.
Referenz: GET/v1.0/directories/{directoryId}/recordings/{recordingId}/summaries
5. Bevor du darauf aufbaust
Eine Aufnahme kann mehrere Zusammenfassungen haben, je Sprache eine. Identifiziere eine Zusammenfassung deshalb immer über die Aufnahme zusammen mit ihrem languageCode, nie über die Aufnahme allein. Warum das so ist und warum dasselbe Meeting mehrfach als Termin auftauchen kann, erklärt Termine, Aufnahmen und Zusammenfassungen.
Version 1.0 wird noch aktiv weiterentwickelt und kann sich ändern. Ignoriere Felder, die du nicht kennst, und rechne mit neuen Enum-Werten, dann läuft dein Code weiter, wenn etwas dazukommt. Details stehen unter Versionierung.
Die drei Endpunkte dieses Schnellstarts im Überblick:
- GET
/v1.0/me/directories/membershipsListet die Mitgliedschaften des authentifizierten Nutzers über alle Unternehmenskonten hinweg aufAusprobieren - GET
…/recordingsListet die Aufnahmen eines Unternehmenskontos auf (paginiert, optional nach Datum gefiltert)Ausprobieren - GET
…/recordings/{recordingId}/summariesGibt die Zusammenfassungen einer Aufnahme zurückAusprobieren
Von hier aus wählst du die Anleitung zu dem, was du baust: