Wie Paginierung und Suche funktionieren
Listen in der Sally API werden seitenweise ausgeliefert, und für die Suche hat jede Ressource ihren eigenen Endpunkt. Diese Seite zeigt die Parameter und die Hülle, die jede geblätterte Antwort trägt, eine kopierbare Schleife, die alle Seiten abholt, und wie die Such-Endpunkte ihre Begriffe entgegennehmen und abgleichen.
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.
Schnellnavigation
1. Paginierung
Geblätterte Listen-Endpunkte nehmen zwei Query-Parameter:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
page | integer | 1 | Seitennummer, beginnend bei 1. |
pageSize | integer | 25 | Einträge je Seite, höchstens 100. |
Diese Anfrage holt die zweite Seite mit zehn Aufnahmen:
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings?page=2&pageSize=10" \
-H "Authorization: Bearer $SALLY_TOKEN"
const url = new URL(`https://api.sally.io/v1.0/directories/${directoryId}/recordings`);
url.searchParams.set('page', '2');
url.searchParams.set('pageSize', '10');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
const {page, total, hasMore, items} = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings",
params={"page": 2, "pageSize": 10},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
data = response.json()
Die Antwort ist immer dieselbe Hülle um die Einträge:
{
"page": 2,
"pageSize": 10,
"total": 137,
"hasMore": true,
"items": [
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"name": "Weekly team meeting",
"durationInSeconds": 1843,
"languageCode": "de-DE",
"createdOn": "2026-09-24T09:30:00Z"
}
]
}
| Feld | Typ | Bedeutung |
|---|---|---|
page | integer | Die Seite, die du bekommen hast. |
pageSize | integer | Die angewendete Seitengröße. |
total | integer | Anzahl der Treffer über alle Seiten. |
hasMore | boolean | true, wenn noch eine Seite folgt. Nimm es als Bedingung deiner Schleife. |
items | array | Die Einträge dieser Seite. Ihre Felder stehen in der Referenz des jeweiligen Endpunkts. |
Ein paar kleine Listen werden nicht geblättert und liefern ein einfaches JSON-Array. Welche Art ein Endpunkt liefert, steht in der Referenz, und unter Endpunkte im Überblick sind alle geblätterten aufgelistet.
2. Alle Seiten abrufen
Frag so lange Seiten ab, bis hasMore auf false steht. Nimm die größte Seitengröße, die du verarbeiten kannst, dann brauchst du weniger Aufrufe. Stößt die Schleife an das Rate-Limit und bekommt ein 429, wartet sie jedes Mal etwas länger und fragt dieselbe Seite erneut ab.
- cURL
- JavaScript
- Python
page=1
while : ; do
body=$(curl -s "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings?page=$page&pageSize=100" \
-H "Authorization: Bearer $SALLY_TOKEN")
echo "$body" | jq -c '.items[]'
[ "$(echo "$body" | jq '.hasMore')" = "true" ] || break
page=$((page + 1))
done
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* allPages(path, pageSize = 100) {
let wait = 1000;
for (let page = 1; ; page++) {
const url = new URL(`https://api.sally.io${path}`);
url.searchParams.set('page', String(page));
url.searchParams.set('pageSize', String(pageSize));
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (response.status === 429) {
await sleep(wait);
wait = Math.min(wait * 2, 60_000);
page--; // dieselbe Seite noch einmal
continue;
}
wait = 1000;
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const body = await response.json();
yield* body.items;
if (!body.hasMore) return;
}
}
for await (const recording of allPages(`/v1.0/directories/${directoryId}/recordings`)) {
console.log(recording.recordingId, recording.name);
}
import os
import time
import requests
def all_pages(path, page_size=100):
page, wait = 1, 1
while True:
response = requests.get(
f"https://api.sally.io{path}",
params={"page": page, "pageSize": page_size},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
if response.status_code == 429:
time.sleep(wait)
wait = min(wait * 2, 60)
continue # dieselbe Seite noch einmal
wait = 1
response.raise_for_status()
body = response.json()
yield from body["items"]
if not body["hasMore"]:
return
page += 1
for recording in all_pages(f"/v1.0/directories/{directory_id}/recordings"):
print(recording["recordingId"], recording["name"])
Kombinier das Blättern mit der Feldauswahl: fields=recordingId,name,createdOn hält jede Seite klein. Die Felder der Hülle kommen immer mit, die Schleife funktioniert also weiter.
3. Such-Endpunkte
Jede durchsuchbare Ressource hat einen POST …/search-Endpunkt, der seine Parameter im Request-Body erwartet. Das ist Absicht: Ein Suchbegriff in einer URL läuft mit Leerzeichen, Umlauten und Prozentzeichen in Kodierungsprobleme, im Body nicht.
Ein Such-Body hat immer genau ein Freitextfeld namens search, das auf einmal über alle passenden Textfelder der Ressource sucht, dazu die strukturellen Filter der Ressource sowie page und pageSize. Diese Anfrage durchsucht Termine:
- cURL
- JavaScript
- Python
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/appointments/search" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"search": "Q4 Planung",
"from": "2026-09-01T00:00:00Z",
"page": 1,
"pageSize": 25
}'
const response = await fetch(
`https://api.sally.io/v1.0/directories/${directoryId}/appointments/search`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
search: 'Q4 Planung',
from: '2026-09-01T00:00:00Z',
page: 1,
pageSize: 25,
}),
},
);
const {total, items} = await response.json();
import os
import requests
response = requests.post(
f"https://api.sally.io/v1.0/directories/{directory_id}/appointments/search",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
json={
"search": "Q4 Planung",
"from": "2026-09-01T00:00:00Z",
"page": 1,
"pageSize": 25,
},
)
data = response.json()
Die Body-Felder der Terminsuche:
| Feld | Typ | Beschreibung |
|---|---|---|
search | string | Freitext, gesucht in Betreff, Beschreibung sowie Name oder E-Mail der Teilnehmenden. |
from | string | Nur Termine, die zu diesem UTC-Zeitpunkt oder danach beginnen. |
to | string | Nur Termine, die zu diesem UTC-Zeitpunkt oder davor beginnen. |
page | integer | Seitennummer, Standard 1. |
pageSize | integer | Einträge je Seite, Standard 25, höchstens 100. |
Die Antwort hat dieselbe Hülle wie der passende Listen-Endpunkt, die Schleife aus Alle Seiten abrufen funktioniert also auch hier: Schick denselben Body mit der nächsten page erneut.
Die einfachen GET-Listen bleiben reine Listen-Endpunkte. Der Such-Endpunkt kommt hinzu, er ersetzt sie nicht.
4. Wie gesucht wird
- Teilstring ohne Rücksicht auf Groß- und Kleinschreibung: Der Begriff passt irgendwo im Wert, nicht nur am Anfang.
- Wörtlich:
%,_und*sind gewöhnliche Zeichen, keine Platzhalter. - Bereinigt: Leerzeichen am Anfang und Ende werden entfernt, und ein leerer Begriff heißt kein Filter.
- Manche Endpunkte verlangen eine Mindestlänge, die die Referenz am Endpunkt nennt.
search | Findet Q4 Planung mit Vertrieb | Warum |
|---|---|---|
"planung" | ja | Teilstring irgendwo im Wert. |
"VERTRIEB" | ja | Groß- und Kleinschreibung spielen keine Rolle. |
" q4 " | ja | Leerzeichen außen herum werden entfernt. |
"plan*" | nein | * ist ein gewöhnliches Zeichen, kein Platzhalter. |
"" | ja | Ein leerer Begriff heißt kein Filter. |
5. Endpunkte im Überblick
Diese Listen kommen direkt aus der Spezifikation. Jeder Eintrag führt in die Referenz, wo du die Anfrage mit deinem eigenen Token abschicken kannst.
Geblätterte Endpunkte
- GET
…/membershipsListet die Mitgliedschaften eines Unternehmenskontos auf (paginiert, optional gefiltert)Ausprobieren - GET
…/memberships/usersListet die Nutzer eines Unternehmenskontos auf (paginiert, optional gefiltert)Ausprobieren - POST
…/memberships/users/searchDurchsucht die Nutzer eines Unternehmenskontos (Parameter im Body)Ausprobieren - GET
…/recordingsListet die Aufnahmen eines Unternehmenskontos auf (paginiert, optional nach Datum gefiltert)Ausprobieren - GET
…/appointmentsDurchsucht Termine (Betreff, Teilnehmer, Zeitraum), paginiertAusprobieren - POST
…/appointments/searchDurchsucht Termine (Parameter im Body)Ausprobieren - POST
…/summaries/searchDurchsucht Zusammenfassungen (Parameter im Body)Ausprobieren - GET
…/foldersListet die Aufnahmeordner auf, auf die der Aufrufer zugreifen kann (paginiert, optional gefiltert)Ausprobieren - POST
…/folders/searchDurchsucht Aufnahmeordner (Parameter im Body)Ausprobieren - GET
…/folders/{folderId}/recordingsListet die Aufnahmen in einem Ordner auf (paginiert)Ausprobieren - GET
…/tasksListet die Aufgaben eines Unternehmenskontos auf (paginiert, optional gefiltert)Ausprobieren - POST
…/tasks/searchDurchsucht Aufgaben (Parameter im Body)Ausprobieren - GET
…/meetingtemplatesListet die Meetingtemplates auf, die der Aufrufer in einem Unternehmenskonto verwenden kannAusprobieren
Such-Endpunkte
- POST
…/memberships/users/searchDurchsucht die Nutzer eines Unternehmenskontos (Parameter im Body)Ausprobieren - POST
…/appointments/searchDurchsucht Termine (Parameter im Body)Ausprobieren - POST
…/summaries/searchDurchsucht Zusammenfassungen (Parameter im Body)Ausprobieren - POST
…/folders/searchDurchsucht Aufnahmeordner (Parameter im Body)Ausprobieren - POST
…/tasks/searchDurchsucht Aufgaben (Parameter im Body)Ausprobieren