Skip to main content

How to get meeting summaries from the Sally API

Getting summaries is the most common reason to use the Sally API: take what Sally understood about a meeting and put it where your team already works. This guide shows how a summary is structured, how to fetch or search it, which status fields tell you whether there is something to read, how to download it as a finished PDF or Word file, and ends with a complete script that imports every new summary since its last run.

Every example works as it stands once SALLY_TOKEN and DIRECTORY_ID (plus RECORDING_ID where a recording is needed) are set, see the Quickstart. Pick your language once, every example on the API pages follows.

Before you start

Quick navigation

  1. What a summary contains
  2. Fetch the summaries of a recording
  3. Fetch a single summary
  4. Search summaries
  5. Check that there is something to read
  6. Download as a file
  7. Complete script: import new summaries
  8. Endpoints at a glance

1. What a summary contains​

A summary belongs to exactly one recording. A recording can have several summaries, one per language. Each summary consists of a summary text (summary) and a list of section items (sectionItems). The sections come from the meeting template that was applied, so a sales call and a technical review produce different sections.

FieldTypeDescription
recordingSummaryIdstringId of the summary.
recordingIdstringId of the recording the summary was generated from.
appointmentIdstring | nullId of the linked appointment, null if there is none.
languageCodestring | nullLanguage of the summary as an IETF language tag, e.g. de-DE or en-US.
summarystring | nullThe summary text.
isSummarizationCompletedbooleantrue once the summarization run has finished, whatever the outcome.
isSummarizationSucceededbooleantrue when there is something to read.
summarizationCompletionReasonstringWhy the run ended, see Check that there is something to read.
createdOnstringCreation timestamp (ISO-8601).
sectionItemsarray | nullThe generated section items, null unless loaded.

Each section item carries its sectionType and its content. The field outputFormat tells you whether that content is html or markdown. Task-like items additionally carry subject, description, responsibleUserName, responsibleUserEmail and dueDate. The section types table, appointment and email keep their result in dedicated fields and also put a readable Markdown version of it into content, so a client that only reads content still shows every section.

FieldTypeDescription
sectionItemIdstringId of the section item.
meetingTemplateSectionIdstring | nullId of the meeting template section the item was generated from, null for derived items such as task items.
titlestringTitle of the section item.
sectionTypestringsummary, tasks, topics, decisions, customList, freeText, table, appointment, email or unknown.
outputFormatstringFormat of content: html, markdown or unknown.
sortOrdernumberOrder within the summary.
contentstring | nullThe content of the item. For table, appointment and email a readable Markdown version of the dedicated fields, outputFormat is then markdown.
subjectstring | nullTask subject, for task-like items.
descriptionstring | nullTask description.
responsibleUserNamestring | nullName of the responsible person.
responsibleUserEmailstring | nullEmail of the responsible person.
dueDatestring | nullDue date (ISO-8601).
tableVariantstring | nullOnly for table: structured when the template fixed the columns and the result is in tableData, free when Sally chose the columns and the result is a Markdown table in content.
tableDataobject | nullColumns and rows of a structured table, otherwise null.
followUpMeetingStartTimestring | nullOnly for appointment: start of the proposed follow-up meeting as a local time without time zone, e.g. 2026-10-16T09:00:00. Apply the time zone of the people involved.
followUpMeetingEndTimestring | nullEnd in the same format, null when the meeting named only a start.
followUpMeetingParticipantsarray | nullOnly for appointment: the people named for the follow-up, each with userId, name, email and isExternal.
emailRecipientsarray | nullOnly for email: the recipients with userId, name, email and kind (to or cc). Subject and body of the email are in subject and description.

2. Fetch the summaries of a recording​

When you know the recording, ask it for its summaries. Add includeDetails=true, otherwise sectionItems is null:

ParameterTypeDefaultDescription
includeDetailsbooleanfalseWhen true, every summary also carries its sectionItems.
fieldsstringall fieldsComma-separated list of the fields to return, see Field selection.
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/summaries?includeDetails=true" \
-H "Authorization: Bearer $SALLY_TOKEN"

The answer is a plain JSON array, not paged, with one entry per language:

200 OK
[
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"summary": "The team agreed on the rollout plan for October and assigned the open tasks.",
"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": "Decisions",
"sectionType": "decisions",
"outputFormat": "html",
"sortOrder": 1,
"content": "We agreed on the rollout plan for October.",
"subject": null,
"description": null,
"responsibleUserName": null,
"responsibleUserEmail": null,
"dueDate": null,
"tableVariant": null,
"tableData": null,
"followUpMeetingStartTime": null,
"followUpMeetingEndTime": null,
"followUpMeetingParticipants": null,
"emailRecipients": null
}
]
}
]
Recommendation

Section items make up most of the payload. If you only need the summary text, leave out includeDetails. Field selection shows how to trim responses further.

3. Fetch a single summary​

When you already have a recordingSummaryId, for example from a search hit, fetch that summary directly. This endpoint always includes sectionItems, no parameter needed. It answers 404 if the summary does not exist or you cannot access the underlying recording.

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

The answer is a single object with the same fields as an entry in section 2.

4. Search summaries​

The search covers the summary text, the generated section items (title, content, subject and description) and the cells of structured tables in one go. A recording is a hit if any of them matches. The search term needs at least 3 characters and goes into the request body:

FieldTypeDefaultDescription
searchstringCase-insensitive substring, at least 3 characters.
pagenumber1Page number, starting at 1.
pageSizenumber25Entries per page, at most 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": "pricing",
"page": 1,
"pageSize": 25
}'

Each hit returns a short snippet plus the ids you need to fetch the full summary:

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": "The team discussed the new pricing model and agreed to test it with two customers.",
"createdOn": "2026-09-24T09:30:00Z"
}
]
}
FieldTypeDescription
recordingSummaryIdstringId of the summary. Pass it to Fetch a single summary.
recordingIdstringId of the recording.
appointmentIdstring | nullId of the linked appointment, if there is one.
languageCodestring | nullLanguage of the summary.
snippetstringTruncated excerpt of the summary text, at most 300 characters.
createdOnstringCreation timestamp (ISO-8601).

The response is paged like every list, and Pagination & search explains why search takes its parameters in the body.

5. Check that there is something to read​

  • isSummarizationCompleted says the run has ended, also when it produced nothing.
  • isSummarizationSucceeded says there is something to read: a summary text or at least one section item. This is the field to check.
  • summarizationCompletionReason says why the run ended.
summarizationCompletionReasonMeaning
succeededThere is a summary text or at least one section item.
noTranscriptThere was nothing to summarize. An expected outcome, not a failure.
emptyResultThe run finished without a result.
errorThe run failed. The underlying error text is deliberately not exposed.
unknownThe run has not finished yet.
Example

A nightly import lists the recordings created since its last run with createdAfter, fetches their summaries and skips every entry where isSummarizationSucceeded is false. It stores the recordingId together with the languageCode as its key, never the appointment. The next section shows exactly that.

6. Download as a file​

Sometimes you do not need the data but the finished document, for example to attach it to a CRM entry or file it in a document management system. GET …/summaries/{recordingSummaryId}/document returns exactly the file a user downloads in the Sally web app: with branding, logos, header and footer, tables, the section order of the meeting template and the same file name. The file is built on request and comes back directly in the response, there is no job to poll.

The document always contains the complete summary in the language version of this recordingSummaryId. A recording has one summary per language, so pick the right id first, for example from section 2. Custom insights and single sections are not part of the document.

ParameterTypeRequiredDescription
formatstringyespdf, docx, odt, txt or md.
languageCodestringnode-DE, en-US, es-ES, fr-FR, it-IT, nl-NL or pl-PL. Without it, the language of the token owner applies, as in the web app.
# -OJ saves the file under the name from Content-Disposition
curl -fOJ "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID/document?format=pdf&languageCode=en-US" \
-H "Authorization: Bearer $SALLY_TOKEN"
Note

languageCode only changes the labels and the date format inside the document, not the content. The font always follows the language of the recording, so Chinese characters stay readable with en-US labels. This endpoint does not support ?fields=.

The answer is 200 with the file itself, not JSON. The header Content-Disposition carries the file name the web app uses for the same download, ASCII in filename and UTF-8 in filename*. The Content-Type follows the 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

Besides the usual errors, this endpoint answers with:

StatuscodeMeaning
400UNKNOWN_DOCUMENT_FORMAT or UNKNOWN_LANGUAGE_CODEformat is missing or unknown, or languageCode is unknown. The body lists the allowed values.
403SUMMARY_DOWNLOAD_PREVENTEDThe company account has disabled this download for its members.
404The summary does not exist, belongs to another company account, or you cannot access the underlying recording.
409SUMMARY_HAS_NO_CONTENTThe summary has no content for a document, because it was generated before meeting templates existed or its template has no sections.

Errors shows how to handle these codes in one place.

7. Complete script: import new summaries​

This script imports every summary created since its last run. It remembers two things between runs: when it last ran, and which recordings were still being processed. Those are checked again next time, so a summary that was not ready yet is not lost.

For every recording it decides:

  • Transcription still running: keep it for the next run.
  • Transcription finished without a transcript: skip it, there will be no summary.
  • Summaries not finished: keep it for the next run.
  • Otherwise: import every summary where isSummarizationSucceeded is true, keyed by recordingId plus languageCode.

The script writes the summaries to a local file. Replace that part with a call to your CRM, wiki or ticket system.

#!/usr/bin/env bash
# Needs curl and jq. curl --retry repeats a request after a 429 with growing waits.
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

# New recordings since the last run, plus the ones still pending
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
# Replace this line with the call to your target system
echo "$summaries" | jq -c '.[] | select(.isSummarizationSucceeded)' >> summaries.jsonl
done

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

Run the script on a schedule, for example every night. If you would rather hear about each summary right away, register a webhook for summary.ready. Its payload has the same shape as the summaries above, so one parser serves both paths.

8. Endpoints at a glance​

Each entry leads to the reference, where you can send the request with your own token.