Skip to main content

How appointments, recordings and summaries relate

If you read only one concept page, read this one. Appointments, recordings and summaries look interchangeable, but they are scoped differently, and that difference is where most integrations go wrong.

This page shows how each of the three is scoped, which key identifies it, what the responses look like and a copyable import loop that stores every summary exactly once. Every example works as it stands once SALLY_TOKEN and DIRECTORY_ID are set, see the Quickstart.

Quick navigation

  1. An appointment exists per person
  2. A recording exists once
  3. A summary exists per language
  4. What that means in practice
  5. Import summaries without duplicates
  6. All entities at a glance

1. An appointment exists per person​

Every attendee of the same real-world meeting has their own appointment record. A meeting with six people can therefore appear up to six times inside one company account, with the same subject and the same time.

An appointment carries no recordingId. This is one entry from GET …/appointments, shortened to the fields that matter here:

200 OK
{
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"subject": "Quarterly planning",
"startTime": "2026-09-24T09:30:00Z",
"endTime": "2026-09-24T10:00:00Z",
"icalId": "e32cc9bd-3639-442b-a946-3e99bc12da69",
"attendees": [
{
"name": "Anna Berger",
"email": "anna.berger@example.com",
"invitationStatus": "organizer",
"attendanceStatus": "attended",
"isRequired": true
}
]
}
Important

Never treat subject plus time as an identity. Counting appointments does not count meetings, and grouping by subject merges things that are not the same. The icalId is no identity either: it is not unique within a company account, because there is one copy per synced calendar. If you need the meeting, go through the recording.

2. A recording exists once​

There is exactly one recording per recorded meeting within a company account, no matter how many people attended. That makes the recording the stable anchor: it is what you store, deduplicate and join on.

curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID" \
-H "Authorization: Bearer $SALLY_TOKEN"
200 OK
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"name": "Weekly team meeting",
"durationInSeconds": 1843,
"isManualUpload": false,
"isTranscriptionSucceeded": true,
"isTranscriptionCompleted": true,
"transcriptionCompletionReason": "succeeded",
"languageCode": "de-DE",
"createdOn": "2026-09-24T09:30:00Z",
"tags": [
{
"tagId": "18063a7e-a82b-4972-a850-f866b8b4a046",
"name": "Customer",
"colorCode": "indigo"
}
]
}

3. A summary exists per language​

A summary belongs to exactly one recording, and a recording can have several summaries, one per language. A summary is unique per recording and languageCode. To pick a specific one, use both.

All summaries of a recording come from one call:

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

The answer is a plain array with one entry per language. Both entries share the recordingId:

200 OK
[
{
"recordingSummaryId": "731180f9-257a-46cf-a7e3-8ca508fb9c35",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "de-DE",
"summary": "Wir haben den Rollout-Plan für Oktober beschlossen.",
"isSummarizationCompleted": true,
"isSummarizationSucceeded": true,
"summarizationCompletionReason": "succeeded",
"createdOn": "2026-09-24T09:30:00Z",
"sectionItems": null
},
{
"recordingSummaryId": "0f5d2a51-6c1e-4b8e-9d7a-2e4c3b1a9f60",
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"languageCode": "en-US",
"summary": "We agreed on the rollout plan for October.",
"isSummarizationCompleted": true,
"isSummarizationSucceeded": true,
"summarizationCompletionReason": "succeeded",
"createdOn": "2026-09-24T09:30:00Z",
"sectionItems": null
}
]
FieldMeaning
recordingSummaryIdId of this one summary, for GET …/summaries/{recordingSummaryId}.
recordingIdThe recording the summary was generated from. Your join key.
languageCodeLanguage of the summary as an IETF tag, such as de-DE or en-US.
appointmentIdYour own appointment linked to the recording, or null if the recording is not linked to one.
sectionItemsnull here. Add ?includeDetails=true to get the structured section items.

4. What that means in practice​

EntityExistsUnique key
AppointmentOnce per attendeeappointmentId
RecordingOnce per recorded meeting in a company accountrecordingId
SummaryOnce per recording and languagerecordingId + languageCode, or recordingSummaryId
  • Store the recordingId as your foreign key, not the appointment.
  • When you show a meeting to a user, use the appointment of that user, because it carries their view of the meeting.
  • When you import summaries, iterate over recordings and pick the language you want, instead of iterating over appointments and importing the same meeting several times.
ApproachSix colleagues, summaries in German and EnglishResult
Keyed on the appointment6 appointmentsSix entries for one meeting
Keyed on the recording1 recordingOne entry per meeting
Keyed on recording + language2 summariesOne entry per meeting and language
Example

Six colleagues attend the same weekly meeting. The company account holds six appointments, one recording and, if summaries exist in German and English, two summaries. An importer keyed on the appointment would create six entries for one meeting.

5. Import summaries without duplicates​

This loop walks the recordings, fetches the summaries of each one and stores the German summary under the key recordingId + languageCode. Running it twice updates the same entries instead of adding new ones. How the paging works is explained in Pagination & search.

# German summary of one recording, keyed on recordingId + languageCode
curl -s "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/summaries" \
-H "Authorization: Bearer $SALLY_TOKEN" \
| jq '.[] | select(.languageCode == "de-DE") | {key: "\(.recordingId):\(.languageCode)", summary}'
Recommendation

Check isSummarizationSucceeded before you store a summary. A run over a recording without a transcript finishes too, and then summarizationCompletionReason is noTranscript, which is an expected outcome, not a failure.

6. All entities at a glance​

Most resources live under a company account: /v1.0/directories/{directoryId}/….

EntityWhat it is
DirectoryA Sally company account and the top-level tenant. Every other resource belongs to exactly one directory.
User / MembershipA person, and the link between a user and a company account with a role and a license.
Role / LicenseLookups that a membership refers to.
TeamA named group of users inside a company account.
AppointmentA calendar meeting with its attendees, stored once per user.
RecordingOne audio or video recording of a meeting, plus its transcript.
SummaryThe AI summary of a recording, one per language, made of section items.
Meeting templateDefines the sections a summary is generated into (read-only).
Folder / TagOrganize recordings in a tree and label appointments and recordings.
TaskAn action item, often derived from a meeting.
VocabularyTerms that improve transcription.
WebhookAn outgoing subscription that sends events to your URL.
FileA binary asset such as a profile picture, fetched by id.

The endpoints for the three entities on this page: