Summaries
A summary belongs to one recording and exists once per language. Use these endpoints to search summaries, to fetch a single summary by its id and to download it as a finished file.
Every endpoint below shows its parameters and response fields in the middle. On the right you find request samples in cURL, JavaScript and Python, an example response and Try it, which sends the request for real with your own token.
Searches recording summaries (parameters in the body)
Searches recording summaries and returns, per hit, an excerpt around the first match.
Field overview from the specification
Body fields:
search(string, required, min. 3 characters): the search query. How it matches:- The query is split into words and filler words (e.g.
the,and,der,und) are dropped. A summary is a hit if it contains ANY of the remaining words (OR). - Words of four or more characters also match inside longer words and in their inflected forms:
FestgeldkontenfindsFestgeldkonto,KontofindsFestgeldkonto. - Words of up to three characters must stand as a whole word:
KIdoes not matchKinder. - Case is ignored, and umlaut spellings are interchangeable (
Müller=Mueller=Muller). - If only filler words remain, the whole query is matched as one case-insensitive substring.
- Searched are the summary text, the generated section items (title, content, subject, description) and the cells, person names and e-mail addresses and column names of table sections. A summary is a hit if ANY of them matches.
- The query is split into words and filler words (e.g.
page(number, optional, default1): 1-based page number.pageSize(number, optional, default25): page size, max.100.
Results are sorted by creation date, newest first. At most 5,000 matching summaries per source (summary text, sections, tables) are considered. The snippet shows the first match in the summary text, or in the section or table that matched when the summary text has none. The Sally MCP tool search_summaries uses exactly the same matching.
Path parameters
directoryIdstringrequiredId of the directory (the Sally company account) the resource belongs to. The directories a token can address are listed by
GET /v1.0/me/directories/memberships.
Query parameters
fieldsstringoptionalComma-separated list of the fields to return. Leave it out and you get every field — including fields added in the future, which is your call to make: name your fields if you want to be shielded from that.
Only first-level fields can be named. A nested list such as
attendees,tagsorsectionItemsis returned whole or not at all;attendees.nameis not supported.On a paged response the selection applies to the entries in
items;page,pageSize,totalandhasMoreare always returned. An unknown name is rejected with 400 rather than ignored.Selectable here:
appointmentId,createdOn,languageCode,recordingId,recordingSummaryId,snippet.Example:appointmentId,createdOn
Request body application/json
searchstringoptionalnullableSearch query, min. 3 characters. Split into words (filler words dropped), OR-combined; words of 4+ characters also match inside longer words and inflected forms, words of up to 3 characters only as a whole word. Case-insensitive. Matched against the summary text, the section items and table sections; see the endpoint description for details.
pagenumberoptionalPage (1-based, default 1).
pageSizenumberoptionalPage size (default 25, max 100).
Response: each entry in items
recordingSummaryIdstringrequiredId of the recording summary.
recordingIdstringrequiredAssociated recording id.
appointmentIdstringrequirednullableAssociated appointment id (if present).
languageCodestringrequirednullableLanguage code of the summary (IETF language tag, e.g.
de-DE,en-US).snippetstringrequiredExcerpt (max. 300 characters) around the FIRST match: from the summary text, or from the section or table that matched when the summary text has no match. Falls back to the beginning of the summary text only if no text contains the match.
createdOnstringrequiredCreation timestamp (ISO-8601).
Every page also carries page, pageSize, total, hasMore.
Status codes
- 200Success
- 400
Invalid request (validation error).
- 401
Missing or invalid bearer token.
- 403
Forbidden (insufficient permissions).
- 404
Resource not found or not accessible.
- 429
Rate limit exceeded (per token/IP). Transcription uploads may also return
codeFUP_LIMIT_EXCEEDEDorTOO_MANY_CONCURRENT_TRANSCRIPTIONS. - 500
Internal server error.
- cURL
- JavaScript
- Python
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": "pricing",
"page": 1,
"pageSize": 25
}'
const response = await fetch(`https://api.sally.io/v1.0/directories/${directoryId}/summaries/search`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"search": "pricing",
"page": 1,
"pageSize": 25
}),
});
const data = await response.json();
import os
import requests
response = requests.post(
f"https://api.sally.io/v1.0/directories/{directory_id}/summaries/search",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
json={
"search": "pricing",
"page": 1,
"pageSize": 25,
},
)
data = response.json()
{
"page": 1,
"pageSize": 25,
"total": 137,
"hasMore": true,
"items": [
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"snippet": "string",
"createdOn": "2026-09-24T09:30:00Z"
}
]
}
Returns a single recording summary by id
Returns the full recording summary for the given recordingSummaryId. 404 if the summary does not exist or the caller cannot access the underlying recording. Obtain valid ids from the summary search (POST .../summaries/search) or from GET .../recordings/{recordingId}/summaries.
Field overview from the specification
Path parameters:
directoryId(string): id of the directory (organization) the summary belongs to.recordingSummaryId(string): id of the recording summary to return.
Response fields:
recordingSummaryId(string): id of this summary. A recording can have several summaries — one per language.recordingId(string): id of the recording this summary was generated from.appointmentId(string | null): id of the caller’s own appointment linked to the recording; null if the recording is not linked to an appointment.languageCode(string | null): language of the summary text as an IETF language tag (e.g.de-DE,en-US); null if unknown.summary(string | null): the summary text (Markdown); null if no summary text was generated.createdOn(string): creation timestamp of the summary (UTC ISO-8601).sectionItems(array): the structured, per-section results of the meeting template that produced this summary (always included on this endpoint). Each item has:sectionItemId(string): id of the section item.meetingTemplateSectionId(string | null): id of the meeting-template section this item was generated from (resolve viaGET .../meetingtemplates/{meetingTemplateId}); null for derived items without a template section (e.g. task items).title(string): title of the section item.sectionType(string enum): the section type (summary,tasks,topics,decisions,customList,freeText,table,appointmentoremail).outputFormat(string enum): the format ofcontent(htmlormarkdown).sortOrder(number): order of the item within the summary.content(string | null): the free-text/Markdown content of the item. Fortable,appointmentandemailit carries a readable Markdown rendering of the dedicated fields below, so a client that only readscontentstill sees the section;outputFormatis thenmarkdown.tableVariant(string enum | null): only fortable.structuredmeans the columns were fixed in the template and the result is intableData;freemeans Sally chose them and the result is a Markdown table incontent.tableData(object | null): columns and rows of a structuredtablesection.followUpMeetingStartTime/followUpMeetingEndTime(string | null): the agreed wall-clock time of anappointmentsection, without a time zone.followUpMeetingParticipants(array | null): participants of anappointmentsection.emailRecipients(array | null): recipients of anemailsection,toandccin one list, told apart bykind.subject(string | null): task subject (for task-like section items).description(string | null): task description (for task-like section items).responsibleUserName(string | null): name of the responsible person (task items).responsibleUserEmail(string | null): email of the responsible person (task items).dueDate(string | null): due date of the task item (ISO-8601).
Path parameters
directoryIdstringrequiredId of the directory (the Sally company account) the resource belongs to. The directories a token can address are listed by
GET /v1.0/me/directories/memberships.recordingSummaryIdstringrequiredId of the recording summary.
Query parameters
fieldsstringoptionalComma-separated list of the fields to return. Leave it out and you get every field — including fields added in the future, which is your call to make: name your fields if you want to be shielded from that.
Only first-level fields can be named. A nested list such as
attendees,tagsorsectionItemsis returned whole or not at all;attendees.nameis not supported.On a paged response the selection applies to the entries in
items;page,pageSize,totalandhasMoreare always returned. An unknown name is rejected with 400 rather than ignored.Selectable here:
agendaResults,appointmentId,createdOn,isAgendaEvaluationOutdated,isSummarizationCompleted,isSummarizationSucceeded,languageCode,recordingId,recordingSummaryId,sectionItems,summarizationCompletionReason,summary.Example:agendaResults,appointmentId
Response
recordingSummaryIdstringrequiredId of the recording summary.
recordingIdstringrequiredAssociated recording id.
appointmentIdstringrequirednullableAssociated appointment id (if present).
languageCodestringrequirednullableLanguage code of the summary (IETF language tag, e.g.
de-DE,en-US).summarystringrequirednullableSummary text.
isSummarizationCompletedbooleanrequiredTrue once the summarization run has finished, whether or not it produced anything.
isSummarizationSucceededbooleanrequiredTrue when there is something to read: summary text or at least one section item. A run over a recording without a transcript finishes too, and then this stays false.
summarizationCompletionReasonstringrequiredWhy the summarization run ended.
noTranscriptmeans there was nothing to summarize, which is an expected outcome and not a failure.unknownwhile it has not finished. The underlying error text is deliberately not exposed.Allowed values:succeedednoTranscriptemptyResulterrorunknowncreatedOnstringrequiredCreation timestamp (ISO-8601).
sectionItemsobject[]requirednullableThe generated section items of the summary (the structured, per-section results of the meeting template). Null when not loaded (the recordings list endpoint only fills this with
?includeDetails=true); the single-summary endpoint always includes it.18 child fields
sectionItemIdstringrequiredId of the section item.
meetingTemplateSectionIdstringrequirednullableId of the meeting template section this item was generated from. Resolve it via
GET /v1.0/directories/{directoryId}/meetingtemplates/{meetingTemplateId}(matches a section’smeetingTemplateSectionId). Null for derived items without a template section (e.g. task items).titlestringrequiredTitle of the template section item.
sectionTypestringrequiredSection type of the item.
Allowed values:summarytaskstopicsdecisionscustomListfreeTexttableappointmentemailunknownoutputFormatstringrequiredOutput format of the item content.
Allowed values:htmlmarkdownunknownsortOrdernumberrequiredOrder within the summary.
contentstringrequirednullableFree-text/Markdown content of the item. For the section types that keep their result in dedicated fields (table, appointment, email), this carries a readable Markdown rendering of those fields, so a client that only reads this one field still sees the section. The dedicated fields are returned unchanged alongside it.
subjectstringrequirednullableTask subject (for task-like section items).
descriptionstringrequirednullableTask description.
responsibleUserNamestringrequirednullableResponsible person (name).
responsibleUserEmailstringrequirednullableResponsible person (email).
dueDatestringrequirednullableDue date (ISO-8601).
tableVariantstringrequirednullableOnly for
sectionType: table.structuredmeans the template fixed the columns and the result is intableData.freemeans Sally chose the columns herself and the result is a Markdown table incontent. Null for every other section type.Allowed values:freestructuredunknowntableDataanyrequirednullableThe table result, set only for
sectionType: tablewithtableVariant: structured. FortableVariant: freethis is null and the table is a Markdown table incontent.followUpMeetingStartTimestringrequirednullableStart of the proposed follow-up meeting, as a LOCAL date and time WITHOUT a time zone (
YYYY-MM-DDTHH:mm:ss, e.g.2026-10-16T09:00:00). Deliberately not a UTC instant: what the meeting agreed on is a wall-clock hour (Thursday at nine), and attaching a zone here would move that hour for every reader in a different one. Apply the time zone of the people involved. Only set forsectionType: appointment.followUpMeetingEndTimestringrequirednullableEnd of the proposed follow-up meeting, same local-time format as
followUpMeetingStartTime. Null when the meeting named a start but no end.followUpMeetingParticipantsobject[]requirednullableThe people the meeting named for the follow-up. Only set for
sectionType: appointment.4 child fields
userIdstringrequirednullableId of the matched user account, or null when no account was found for this person. Resolve it via
GET /v1.0/directories/{directoryId}/memberships/users/{userId}.namestringrequiredName of the person as recognized in the meeting.
emailstringrequirednullableEmail address as recognized in the meeting, lower-cased. Null when the meeting named the person without an address.
isExternalbooleanrequirednullableTrue when the person was recognized as not belonging to your own organization. Null when the meeting gave no indication either way.
emailRecipientsobject[]requirednullableRecipients of the proposed email, direct and on copy (see
kind). Only set forsectionType: email; its subject is insubjectand its body indescription.4 child fields
userIdstringrequirednullableId of the matched user account, or null when no account was found for this person. Resolve it via
GET /v1.0/directories/{directoryId}/memberships/users/{userId}.namestringrequiredName of the person as recognized in the meeting.
emailstringrequirednullableEmail address as recognized in the meeting, lower-cased. Null when the meeting named the person without an address.
kindstringrequiredWhether the person is a direct recipient (
to) or on copy (cc).Allowed values:tocc
agendaResultsobject[]requirednullableThe evaluated agenda of the appointment: one entry per agenda item that was evaluated against this transcript. A deliberately separate field, not another entry in
sectionItems, because the agenda is a separate run and not a section of the meeting template. Null when not loaded, empty when the appointment has no agenda. ResolveappointmentAgendaItemIdthroughGET /v1/directories/{directoryId}/appointments/{appointmentId}/agenda.6 child fields
appointmentAgendaItemIdstringrequiredId of the agenda item this result belongs to. Never a bare
id: the caller has to be able to tell which entity it points at.titlestringrequiredThe title of the agenda item at the time of the evaluation, recorded alongside. If the item is renamed later, the older summary stays readable.
agendaCoveragestringrequiredWhether the item was discussed. Exactly three values; "not evaluated yet" is NOT a fourth one, because an item without a result has no entry here at all.
Allowed values:discussedtouchednotDiscussedsummarystringrequirednullableTwo or three sentences on what this item produced in the meeting: the answer for a reader who does not work through
content. Plain prose, no Markdown and no places. Null fornotDiscussed, and also null for evaluations that ran before this field existed — in that casecontentstill carries the full result.contentstringrequirednullableWhat the meeting produced on this item, as a Markdown bullet list: one bullet per outcome, decision, number, answer or next step, each opening with a short bold label. A bullet that could be located in the recording also opens with its place or places in
[mm:ss]form, before the label, so that a reader can jump to the passage a statement comes from. Null fornotDiscussed, and that is not an error: the state is carried byagendaCoverage.timeStampsnumber[]requiredEvery point in the recording at which this item was discussed substantively, in seconds, ascending. An agenda item is often raised more than once, so this is a list and not a single start and end time. This is the union of the places carried by the individual bullets of
content, so that reading the places needs no Markdown parsing.
isAgendaEvaluationOutdatedbooleanrequiredTrue when the agenda changed after it was evaluated, so the entries in
agendaResultsno longer match the current agenda. Without this an integrator reads an agenda block that no longer fits and does not notice.
Status codes
- 200Success
- 400
Invalid request (validation error).
- 401
Missing or invalid bearer token.
- 404
Resource not found or not accessible.
- 429
Rate limit exceeded (per token/IP). Transcription uploads may also return
codeFUP_LIMIT_EXCEEDEDorTOO_MANY_CONCURRENT_TRANSCRIPTIONS. - 500
Internal server error.
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID" \
-H "Authorization: Bearer $SALLY_TOKEN"
const response = await fetch(`https://api.sally.io/v1.0/directories/${directoryId}/summaries/${recordingSummaryId}`, {
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
},
});
const data = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/summaries/{recording_summary_id}",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
data = response.json()
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"summary": "string",
"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": "string",
"sectionType": "summary",
"outputFormat": "html",
"sortOrder": 1,
"content": "We agreed on the rollout plan for October.",
"subject": "Quarterly planning",
"description": "string",
"responsibleUserName": "string",
"responsibleUserEmail": "anna.berger@example.com",
"dueDate": "2026-09-24T09:30:00Z",
"tableVariant": "free",
"tableData": null,
"followUpMeetingStartTime": "string",
"followUpMeetingEndTime": "string",
"followUpMeetingParticipants": [
{
"userId": "11f967df-7dd1-4719-a9aa-46530940c443",
"name": "Example",
"email": "anna.berger@example.com",
"isExternal": true
}
],
"emailRecipients": [
{
"userId": "11f967df-7dd1-4719-a9aa-46530940c443",
"name": "Example",
"email": "anna.berger@example.com",
"kind": "to"
}
]
}
],
"agendaResults": [
{
"appointmentAgendaItemId": "9b97e5a4-4127-477c-a6b6-a954a0350394",
"title": "string",
"agendaCoverage": "discussed",
"summary": "string",
"content": "We agreed on the rollout plan for October.",
"timeStamps": [
1
]
}
],
"isAgendaEvaluationOutdated": true
}
Returns a recording summary as a finished document file
Returns the same file the Sally web app downloads for this summary: branding, logos, header and footer, tables and the section order of the meeting template. The file is built on request and returned directly in the response; there is no job to poll.
Always the complete summary in the language version identified by recordingSummaryId (a recording has one summary per language, see GET .../recordings/{recordingId}/summaries). Custom insights and single sections are not part of this document.
Field overview from the specification
Path parameters:
directoryId(string): id of the directory the summary belongs to.recordingSummaryId(string): id of the recording summary.
Query parameters:
format(string enum, required): one ofpdf,docx,odt,txt,md.languageCode(string enum, optional): one ofde-DE,en-US,es-ES,fr-FR,it-IT,nl-NL,pl-PL; defaults to the language of the token owner.
This endpoint does not support ?fields=.
Path parameters
directoryIdstringrequiredId of the directory (the Sally company account) the resource belongs to. The directories a token can address are listed by
GET /v1.0/me/directories/memberships.recordingSummaryIdstringrequiredId of the recording summary.
Query parameters
formatstringrequiredFile format of the document.
Allowed values:pdfdocxodttxtmdlanguageCodestringoptionalIETF language tag for the labels and the date format inside the document, e.g.
de-DE. Defaults to the language of the token owner, exactly as in the Sally web app. It does NOT change the language of the content, and the font is always chosen by the language of the recording, so Chinese characters stay readable withen-USlabels.Allowed values:de-DEen-USes-ESfr-FRit-ITnl-NLpl-PL
Status codes
- 200The file itself. `Content-Disposition` carries the file name that the Sally web app uses for the same download (ASCII in `filename`, UTF-8 in `filename*`).
- 400
Unknown or missing
format, or unknownlanguageCode. The body lists the allowed values (code:UNKNOWN_DOCUMENT_FORMATorUNKNOWN_LANGUAGE_CODE). - 401
Missing or invalid bearer token.
- 403
The directory has disabled this download for its members (
code:SUMMARY_DOWNLOAD_PREVENTED). - 404
The source does not exist, belongs to another directory, or the caller cannot access the underlying recording.
- 409
The summary has no content that could be put into a document (it was generated before meeting templates existed, or its template has no sections) (
code:SUMMARY_HAS_NO_CONTENT). - 429
Rate limit exceeded (per token/IP). Transcription uploads may also return
codeFUP_LIMIT_EXCEEDEDorTOO_MANY_CONCURRENT_TRANSCRIPTIONS. - 500
Internal server error.
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID/document?format=pdf" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-OJ
const response = await fetch(`https://api.sally.io/v1.0/directories/${directoryId}/summaries/${recordingSummaryId}/document?format=pdf`, {
headers: {
Authorization: `Bearer ${process.env.SALLY_TOKEN}`,
},
});
const file = await response.blob();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/summaries/{recording_summary_id}/document?format=pdf",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
content = response.content
"string"