{"openapi":"3.0.0","paths":{"/health":{"get":{"description":"Lightweight liveness/health probe for monitoring and uptime checks (e.g. a load balancer or status page). Requires NO authentication (no Bearer token) and reads no tenant data and no database — it only confirms that the API process is up and answering, and reports which service/environment/version responded.\n\nResponse fields:\n- **`status`** (string): health status; always `ok` when the service answers.\n- **`service`** (string): name of the service that responded.\n- **`environment`** (string): environment the service runs in (e.g. `dev`, `prod`).\n- **`version`** (string): version of the running service.\n- **`timestamp`** (string): time the response was produced (UTC ISO-8601).\n\nPossible outcomes:\n- **200**: the service is running (body as above). This endpoint itself only ever returns `200` when reached.\n- **no response / 502 / 503 / 504**: returned by the gateway/infrastructure, NOT by this endpoint, when the service is down, still starting, or unreachable — a missing or non-2xx answer is exactly the \"unhealthy\" signal a liveness probe checks for.","operationId":"HealthController_Check","parameters":[],"responses":{"200":{"description":"Service is running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiHealth"}}}}},"summary":"Liveness check of the Public API.","tags":["meta"]}},"/v1.0/me":{"get":{"description":"Response fields:\n- **`userId`** (string): unique user id.\n- **`email`** (string): email address of the user.\n- **`firstName`** (string): first name.\n- **`lastName`** (string): last name.\n- **`timeZone`** (string | null): IANA time zone identifier of the user (e.g. \"Europe/Berlin\"), suitable for Intl.DateTimeFormat and date math. Null if the user has no time zone set.","operationId":"MeController_Me_v1.0","parameters":[{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `timeZone`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiMe"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the basic info of the authenticated user.","tags":["me"],"security":[{"SallyBearer":[]}]}},"/v1.0/me/directories/memberships":{"get":{"description":"Discovery of \"which directories + my role/license in each\". Resolve `roleId`/`licenseId` via `GET /v1.0/roles` / `GET /v1.0/licenses`.\n\nEach item has:\n- **`directoryId`** (string): id of the directory.\n- **`directoryName`** (string): display name of the directory.\n- **`roleId`** (string): role id of the caller in this directory; resolve via `GET /v1.0/roles`.\n- **`licenseId`** (string): license id of the caller in this directory; resolve via `GET /v1.0/licenses`.\n- **`isBlocked`** (boolean): whether the caller is blocked in this directory.","operationId":"MeController_Memberships_v1.0","parameters":[{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"directoryId,directoryName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `directoryId`, `directoryName`, `isBlocked`, `licenseId`, `roleId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiMyMembership"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the memberships of the authenticated user across all directories.","tags":["me"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories":{"get":{"description":"Discovery endpoint: returns all directories of the token user (active, non-blocked memberships). The `directoryId` is used in the subsequent paths. Each entry carries the same fields as the single-directory endpoint.\n\nResponse fields (per directory):\n- **`directoryId`** (string): id of the directory; use it in all `/v1.0/directories/{directoryId}/...` paths.\n- **`name`** (string): display name of the directory.\n- **`pictureFileUrl`** (string | null): absolute URL of the directory logo (requires the same Bearer token as the API); null if unset.\n- **`languageCode`** (string | null): default language of the directory (IETF language tag, e.g. `de-DE`, `en`); null if unset.\n- **`maxApiRequestsPerMinute`** (number): maximum number of API requests per minute allowed for this directory.\n- **`maxApiRequestsPerDay`** (number): maximum number of API requests per day allowed for this directory.","operationId":"DirectoriesController_List_v1.0","parameters":[{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"directoryId,languageCode","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `directoryId`, `languageCode`, `maxApiRequestsPerDay`, `maxApiRequestsPerMinute`, `name`, `pictureFileUrl`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiDirectoryDetail"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the directories the authenticated user is a member of.","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}":{"get":{"description":"Returns one directory by id (the caller must be a member, otherwise 404). Same shape as the discovery list.\n\nResponse fields:\n- **`directoryId`** (string): id of the directory; use it in all `/v1.0/directories/{directoryId}/...` paths.\n- **`name`** (string): display name of the directory.\n- **`pictureFileUrl`** (string | null): absolute URL of the directory logo (requires the same Bearer token as the API); null if unset.\n- **`languageCode`** (string | null): default language of the directory (IETF language tag, e.g. `de-DE`, `en`); null if unset.\n- **`maxApiRequestsPerMinute`** (number): maximum number of API requests per minute allowed for this directory.\n- **`maxApiRequestsPerDay`** (number): maximum number of API requests per day allowed for this directory.","operationId":"DirectoriesController_GetOne_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"directoryId,languageCode","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `directoryId`, `languageCode`, `maxApiRequestsPerDay`, `maxApiRequestsPerMinute`, `name`, `pictureFileUrl`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiDirectoryDetail"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single directory (if the user is a member).","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/memberships":{"get":{"description":"Pagination via `?page` + `?pageSize` (max. 100). An optional `?search` substring matches first/last name and email of the member (case-insensitive). Returns membership scalars only (`userId`, `roleId`, `licenseId`, `isBlocked`); fetch user details via `GET .../memberships/users/{userId}`. Resolve `roleId`/`licenseId` via `GET /v1.0/roles` / `GET /v1.0/licenses`.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the memberships on this page. Each item has:\n  - **`directoryId`** (string): id of the directory this membership belongs to.\n  - **`userId`** (string): id of the user this membership belongs to.\n  - **`roleId`** (string): role id of the user in this directory; resolve via `GET /v1.0/roles`.\n  - **`licenseId`** (string): license id of the user in this directory; resolve via `GET /v1.0/licenses`.\n  - **`isBlocked`** (boolean): whether the user is blocked in this directory.","operationId":"MembershipsController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on first/last name and email. The term may appear at the start, middle, or end.","schema":{"type":"string"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"directoryId,isBlocked","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `directoryId`, `isBlocked`, `licenseId`, `roleId`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiDirectoryMembershipListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the memberships of a directory (paginated, optionally filtered).","tags":["directories"],"security":[{"SallyBearer":[]}]},"post":{"description":"Creates a membership by inviting a user (by email) with a role + license. Only admins/owners may invite; only an owner may invite another owner. Fails with 400 if no free license seat is available. Body fields:\n- **`email`** (string, required): email address to invite.\n- **`roleId`** (string, required): role id to assign; resolve valid ids via `GET /v1.0/roles`.\n- **`licenseId`** (string, required): license id to assign; resolve valid ids via `GET /v1.0/licenses`.\n- **`description`** (string, optional): free-text note.\n- **`languageCode`** (string, optional): invitation language (IETF language tag, e.g. `de-DE`).","operationId":"MembershipsController_Invite_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,inviteeEmail","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `inviteeEmail`, `licenseId`, `roleId`, `userId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiInviteMemberBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiInvite"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Invites/creates a user in the directory.","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/memberships/users":{"get":{"description":"Basic user info of all members of the directory. Pagination via `?page` + `?pageSize` (max. 100). An optional `?search` substring matches first/last name and email (case-insensitive). The caller must be a member of the directory.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the users on this page. Each item has:\n  - **`userId`** (string): id of the user.\n  - **`email`** (string): email address of the user.\n  - **`firstName`** (string): first name.\n  - **`lastName`** (string): last name.\n  - **`pictureFileUrl`** (string | null): absolute URL of the profile picture, or null. Requires the same Bearer token as the API.","operationId":"MembershipsController_Users_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on first/last name and email. The term may appear at the start, middle, or end.","schema":{"type":"string"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `pictureFileUrl`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUserListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the users of a directory (paginated, optionally filtered).","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/memberships/users/search":{"post":{"description":"The caller must be a member of the directory. Body fields:\n- **`search`** (string, optional): case-insensitive substring on first/last name and email; the term may appear at the start, middle, or end.\n- **`page`** (number, optional, default `1`): 1-based page number.\n- **`pageSize`** (number, optional, default `25`): page size (max. 100).","operationId":"MembershipsController_SearchUsers_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `pictureFileUrl`, `userId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSearchUsersBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUserListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches the users of a directory (parameters in the body).","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/memberships/users/{userId}":{"get":{"description":"Basic user info by id. The caller must be a member of the directory, and the requested user must be a member of the same directory — otherwise 404 (no existence leak).\n\nResponse fields:\n- **`userId`** (string): id of the user.\n- **`email`** (string): email address of the user.\n- **`firstName`** (string): first name.\n- **`lastName`** (string): last name.\n- **`pictureFileUrl`** (string | null): absolute URL of the profile picture, or null. Requires the same Bearer token as the API.","operationId":"MembershipsController_User_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"userId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the user."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `pictureFileUrl`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUser"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single user of a directory.","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/memberships/{userId}":{"delete":{"description":"Revokes the membership of the user. Only admins/owners may remove members; the last owner cannot be removed. If it was the user’s last directory, the user is deleted entirely (GDPR).","operationId":"MembershipsController_Revoke_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"userId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the user."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"userHardDeleted","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `userHardDeleted`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiRevokeResult"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Removes a user from the directory.","tags":["directories"],"security":[{"SallyBearer":[]}]}},"/v1.0/roles":{"get":{"description":"Each item has:\n- **`roleId`** (string): id of the role.\n- **`name`** (string): role name; one of `Owner`, `Admin`, `Member`.","operationId":"RolesController_Roles_v1.0","parameters":[{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"name,roleId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `name`, `roleId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiRole"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the roles a membership can have (resolves `roleId`).","tags":["roles"],"security":[{"SallyBearer":[]}]}},"/v1.0/licenses":{"get":{"description":"Each item has:\n- **`licenseId`** (string): id of the license.\n- **`name`** (string): license name (tier).","operationId":"LicensesController_Licenses_v1.0","parameters":[{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"licenseId,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `licenseId`, `name`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiLicense"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the licenses (resolves `licenseId`).","tags":["licenses"],"security":[{"SallyBearer":[]}]}},"/v1.0/files/{fileId}":{"get":{"description":"Returns the raw file bytes with the stored content type. Referenced by `pictureFileUrl`. Requires the same Bearer token as the rest of the API. 404 if the file does not exist.","operationId":"FilesController_Download_v1.0","parameters":[{"name":"fileId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the file."}],"responses":{"200":{"description":"Raw file bytes."},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Streams a file (e.g. a profile picture) by id.","tags":["files"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings":{"get":{"description":"Pagination via `?page` + `?pageSize` (max. 100). Optional `?createdAfter` / `?createdBefore` are UTC ISO-8601 (`YYYY-MM-DDTHH:MM:SSZ`, see „Dates & times\" above). Sorted by creation date descending.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the recordings on this page. Each item has:\n  - **`recordingId`** (string): id of the recording.\n  - **`name`** (string | null): display name of the recording.\n  - **`durationInSeconds`** (number | null): duration in seconds.\n  - **`isManualUpload`** (boolean): true if manually uploaded (no meeting bot).\n  - **`isTranscriptionSucceeded`** (boolean): true when a usable transcript exists. A run that finished without producing one (no spoken audio, empty file, language not detected) leaves this false.\n  - **`isTranscriptionCompleted`** (boolean): true once the transcription run has finished, whether or not it produced a transcript. Tells \"still running\" apart from \"finished without a result\".\n  - **`transcriptionCompletionReason`** (string enum): why the run ended. One of `succeeded`, `noSpokenAudio`, `fileEmpty`, `languageNotDetected`, `error`, `quotaExceeded`, `unknown`. The underlying error text is deliberately not exposed.\n  - **`languageCode`** (string | null): language code of the recording (IETF language tag, e.g. `de-DE`, `en-US`).\n  - **`createdOn`** (string): creation timestamp (ISO-8601).\n  - **`tags`** (array): effective tags (own + inherited from linked appointments and their recurring series master). Each tag has:\n    - **`tagId`** (string): id of the tag.\n    - **`name`** (string): tag display name.\n    - **`colorCode`** (string | null): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); null if unset.","operationId":"RecordingsController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"createdAfter","required":false,"in":"query","description":"Only recordings created at/after this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).","schema":{"type":"string"}},{"name":"createdBefore","required":false,"in":"query","description":"Only recordings created at/before this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).","schema":{"type":"string"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,durationInSeconds","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `durationInSeconds`, `isManualUpload`, `isTranscriptionCompleted`, `isTranscriptionSucceeded`, `languageCode`, `name`, `recordingId`, `tags`, `transcriptionCompletionReason`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiRecordingListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists recordings of a directory (paginated, optionally filtered by date).","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/{recordingId}":{"get":{"description":"Response fields:\n- **`recordingId`** (string): id of the recording.\n- **`name`** (string | null): display name of the recording.\n- **`durationInSeconds`** (number | null): duration in seconds.\n- **`isManualUpload`** (boolean): true if manually uploaded (no meeting bot).\n- **`isTranscriptionSucceeded`** (boolean): true when a usable transcript exists. A run that finished without producing one (no spoken audio, empty file, language not detected) leaves this false.\n- **`isTranscriptionCompleted`** (boolean): true once the transcription run has finished, whether or not it produced a transcript. Tells \"still running\" apart from \"finished without a result\".\n- **`transcriptionCompletionReason`** (string enum): why the run ended. One of `succeeded`, `noSpokenAudio`, `fileEmpty`, `languageNotDetected`, `error`, `quotaExceeded`, `unknown`. The underlying error text is deliberately not exposed.\n- **`languageCode`** (string | null): language code of the recording (IETF language tag, e.g. `de-DE`, `en-US`).\n- **`createdOn`** (string): creation timestamp (ISO-8601).\n- **`tags`** (array): effective tags (own + inherited from linked appointments and their recurring series master). Each tag has:\n  - **`tagId`** (string): id of the tag.\n  - **`name`** (string): tag display name.\n  - **`colorCode`** (string | null): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); null if unset.","operationId":"RecordingsController_GetOne_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,durationInSeconds","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `durationInSeconds`, `isManualUpload`, `isTranscriptionCompleted`, `isTranscriptionSucceeded`, `languageCode`, `name`, `recordingId`, `tags`, `transcriptionCompletionReason`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiRecording"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single recording.","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/{recordingId}/transcription":{"get":{"description":"Response fields:\n- **`recordingId`** (string): associated recording id.\n- **`speakers`** (array): speakers of the transcript. Each item has:\n  - **`speakerId`** (string): id of the transcript speaker (referenced by each segment).\n  - **`speakerNumber`** (number): speaker number.\n  - **`name`** (string | null): name of the speaker.\n  - **`emailAddress`** (string | null): email of the speaker.\n- **`segments`** (array): time-coded segments. Each item has:\n  - **`speakerId`** (string): speaker id of the segment (resolves to the `speakers` list above).\n  - **`startTime`** (number): start time in seconds.\n  - **`endTime`** (number): end time in seconds.\n  - **`text`** (string | null): transcribed text of the segment.","operationId":"RecordingsController_Transcription_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."},{"name":"fields","required":false,"in":"query","schema":{"type":"string"},"example":"recordingId,segments","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `recordingId`, `segments`, `speakers`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTranscription"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the transcript of a recording (time-coded segments + speakers).","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/{recordingId}/transcription/document":{"get":{"description":"Returns the same file the Sally web app downloads for this transcript: branding, logos, header and footer, speakers and timestamps. The file is built on request and returned directly in the response; there is no job to poll.\n\nPath parameters:\n- **`directoryId`** (string): id of the directory the recording belongs to.\n- **`recordingId`** (string): id of the recording.\n\nQuery parameters:\n- **`format`** (string enum, required): one of `pdf`, `docx`, `xlsx`, `txt`, `md`, `vtt`, `srt`. `vtt` and `srt` are subtitle files without a header block.\n- **`languageCode`** (string enum, optional): one of `de-DE`, `en-US`, `es-ES`, `fr-FR`, `it-IT`, `nl-NL`, `pl-PL`; defaults to the language of the token owner.\n\nThis endpoint does not support `?fields=`.","operationId":"RecordingsController_TranscriptionDocument_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."},{"name":"format","required":true,"in":"query","description":"File format of the document.","schema":{"enum":["pdf","docx","xlsx","txt","md","vtt","srt"],"type":"string"}},{"name":"languageCode","required":false,"in":"query","description":"IETF 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 with `en-US` labels.","schema":{"enum":["de-DE","en-US","es-ES","fr-FR","it-IT","nl-NL","pl-PL"],"type":"string"}}],"responses":{"200":{"description":"The 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*`).","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{"schema":{"type":"string","format":"binary"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"text/plain; charset=utf-8":{"schema":{"type":"string","format":"binary"}},"text/markdown; charset=utf-8":{"schema":{"type":"string","format":"binary"}},"text/vtt; charset=utf-8":{"schema":{"type":"string","format":"binary"}},"application/x-subrip; charset=utf-8":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unknown or missing `format`, or unknown `languageCode`. The body lists the allowed values (`code`: `UNKNOWN_DOCUMENT_FORMAT` or `UNKNOWN_LANGUAGE_CODE`)."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"The directory has disabled this download for its members (`code`: `TRANSCRIPT_DOWNLOAD_PREVENTED`)."},"404":{"description":"The source does not exist, belongs to another directory, or the caller cannot access the underlying recording."},"409":{"description":"The recording has no transcript yet (`code`: `TRANSCRIPT_NOT_AVAILABLE`)."},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the transcript of a recording as a finished document file.","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/{recordingId}/summaries":{"get":{"description":"With `?includeDetails=true`, each summary additionally embeds its section items (the structured per-section results of the meeting template). Without the parameter, only the summary texts. To fetch a single summary by id, use `GET .../summaries/{recordingSummaryId}`.\n\nEach item has:\n- **`recordingSummaryId`** (string): id of the recording summary.\n- **`recordingId`** (string): associated recording id.\n- **`appointmentId`** (string | null): associated appointment id (if present).\n- **`languageCode`** (string | null): language code of the summary (IETF language tag, e.g. `de-DE`, `en-US`).\n- **`summary`** (string | null): summary text.\n- **`isSummarizationCompleted`** (boolean): true once the summarization run has finished, whether or not it produced anything.\n- **`isSummarizationSucceeded`** (boolean): true 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.\n- **`summarizationCompletionReason`** (string enum): why the run ended. One of `succeeded`, `noTranscript`, `emptyResult`, `error`, `unknown`. `noTranscript` is an expected outcome, not a failure. The underlying error text is deliberately not exposed.\n- **`createdOn`** (string): creation timestamp (ISO-8601).\n- **`sectionItems`** (array | null): the generated section items of the summary (structured per-section results of the meeting template). Null unless loaded (only with `?includeDetails=true`). Each section item has:\n  - **`sectionItemId`** (string): id of the section item.\n  - **`meetingTemplateSectionId`** (string | null): id of the meeting template section this item was generated from (resolve via `GET /v1.0/directories/{directoryId}/meetingtemplates/{meetingTemplateId}`); null for derived items (e.g. task items).\n  - **`title`** (string): title of the template section item.\n  - **`sectionType`** (string enum): section type of the item. One of `summary`, `tasks`, `topics`, `decisions`, `customList`, `freeText`, `unknown`.\n  - **`outputFormat`** (string enum): output format of the item content. One of `html`, `markdown`, `unknown`.\n  - **`sortOrder`** (number): order within the summary.\n  - **`content`** (string | null): free-text/Markdown content of the item.\n  - **`subject`** (string | null): task subject (for task-like section items).\n  - **`description`** (string | null): task description.\n  - **`responsibleUserName`** (string | null): responsible person (name).\n  - **`responsibleUserEmail`** (string | null): responsible person (email).\n  - **`dueDate`** (string | null): due date (ISO-8601).","operationId":"RecordingsController_Summaries_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."},{"name":"includeDetails","required":false,"in":"query","description":"When `true`, every summary additionally carries its `sectionItems`. When omitted or `false`, `sectionItems` is `null` and only the summary text is returned.","schema":{"type":"boolean"}},{"name":"fields","required":false,"in":"query","schema":{"type":"string"},"example":"agendaResults,appointmentId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `agendaResults`, `appointmentId`, `createdOn`, `isAgendaEvaluationOutdated`, `isSummarizationCompleted`, `isSummarizationSucceeded`, `languageCode`, `recordingId`, `recordingSummaryId`, `sectionItems`, `summarizationCompletionReason`, `summary`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiSummary"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the summaries of a recording.","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/uploads":{"post":{"description":"The client then uploads the file bytes via `PUT` directly to the returned `uploadUrl` (with the `requiredHeaders`) and afterwards calls `POST .../uploads/{uploadId}/finalize`. The transcription quota (fair usage policy) is already checked here — if the quota is exhausted, **429** with code `FUP_LIMIT_EXCEEDED` is returned. Body fields:\n- **`fileName`** (string, required): original file name (incl. extension, for MIME/extension derivation).\n- **`mimeType`** (string, required): MIME type of the file (whitelist: video mp4/mkv/avi/mov/webm, audio mp3/wav/flac/ogg/amr/m4a/opus/aac).\n- **`sizeBytes`** (number, required): file size in bytes (> 0, max. 5 GB).","operationId":"RecordingsController_RequestUpload_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"maxBytes,requiredHeaders","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `maxBytes`, `requiredHeaders`, `storageMode`, `uploadId`, `uploadUrl`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiRequestUploadBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUploadTicket"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Starts a recording upload (step 1/2) — returns a SAS PUT URL.","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/uploads/{uploadId}/finalize":{"post":{"description":"Call after a successful `PUT` of the file bytes. Verifies the blob, creates the recording row and triggers the transcription pipeline. Returns the `recordingId`. If too many transcriptions of the directory are running concurrently, **429** with code `TOO_MANY_CONCURRENT_TRANSCRIPTIONS` is returned. Body fields (all optional):\n- **`languageCode`** (string, optional): language code of the audio (IETF language tag, e.g. `de-DE`, `en-US`). Omit or null to auto-detect.\n- **`speakerCount`** (number, optional): number of speakers (for diarization).\n- **`appointmentId`** (string, optional): optionally linked appointment (GUID).\n- **`choice`** (`copy` | `replace`, optional): only when the linked appointment already has an active recording. Without it such an upload fails with **409** and code `RECORDING_EXISTS_CHOICE_REQUIRED`; the uploaded file stays and the call can be repeated with a choice.","operationId":"RecordingsController_FinalizeUpload_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"uploadId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the upload."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"recordingId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `recordingId`."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFinalizeUploadBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUploadedRecording"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Completes a recording upload (step 2/2) — creates the recording.","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/from-url":{"post":{"description":"Sally fetches the file from the URL server-side, stores it and triggers the transcription and summarization pipeline, exactly like a finished upload. Use this instead of the two-step upload when the file is already reachable over https. The call returns once the file has been fetched and the recording exists; transcription then runs in the background, so poll `GET .../recordings/{recordingId}` or use a webhook for the result.\n\n**What the URL must satisfy** (each of these is enforced, and a violation returns 400):\n- **`https` only.** No `http`, no other scheme.\n- **No credentials in the URL.** A `user:pass@host` form is rejected.\n- **Public targets only.** The host is resolved and rejected if it points into a private or reserved network (loopback, private ranges, link-local including cloud metadata, CGNAT, multicast). The verified address is then pinned for the connection, so a DNS answer that changes between check and fetch cannot redirect us inside.\n- **No redirects.** Pass the final address; a redirect is rejected rather than followed.\n- **A supported media type**, either by `Content-Type` or by the file extension: `mp4`, `mkv`, `avi`, `mov`, `webm`, `mp3`, `wav`, `flac`, `ogg`, `amr`, `m4a`, `opus`, `aac`.\n- **Size limit** as returned by the upload endpoint (`maxBytes`). Both the declared `Content-Length` and the actual bytes are checked.\n\nThe same licence, trial and fair-usage rules apply as for an upload. If too many transcriptions of the directory are running concurrently, **429** with code `TOO_MANY_CONCURRENT_TRANSCRIPTIONS` is returned and nothing is fetched.\n\nBody fields:\n- **`url`** (string): the https URL of the file.\n- **`name`** (string, optional): name of the recording. The URL is never used as the name, because presigned links carry credentials in their query string.\n- **`languageCode`** (string, optional): IETF language tag of the audio, e.g. `de-DE`. Omit to auto-detect.\n- **`speakerCount`** (number, optional): number of speakers, for diarization.","operationId":"RecordingsController_IngestFromUrl_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"recordingId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `recordingId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiIngestFromUrlBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUploadedRecording"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Creates a recording from a file URL (no upload needed).","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/recordings/{recordingId}/tags":{"put":{"description":"Replace-set of the recording’s DIRECT tags; tags inherited from linked appointments/series stay dynamic. Max. 20 direct tags per recording. Body fields:\n- **`tagIds`** (array of strings): existing tag ids to assign; the given set replaces the current one. Assigning only takes tag ids, so create the tag first via `POST /v1.0/directories/{directoryId}/tags` and assign it here.","operationId":"RecordingsController_SetTags_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`, `name`, `tagId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSetTagsBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTag"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Replaces the direct tags of a recording (assign/unassign).","tags":["recordings"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/appointments":{"get":{"description":"Case-insensitive substring search (see „Text search\" above): `?subject` matches any part of the subject (start, middle or end), `?attendee` any part of an attendee name/email. `?from`/`?to` filter on StartTime and are UTC ISO-8601 (`YYYY-MM-DDTHH:MM:SSZ`, see „Dates & times\" above). Pagination via `?page` + `?pageSize` (max. 100). Sorted by StartTime descending.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the appointments on this page. Each item has:\n  - **`appointmentId`** (string): id of the appointment.\n  - **`subject`** (string): subject of the appointment.\n  - **`description`** (string | null): body of the appointment as delivered by the calendar; usually HTML for Outlook/Google appointments, plain text for ones created in Sally. Returned verbatim.\n  - **`startTime`** (string | null): start time (ISO-8601).\n  - **`endTime`** (string | null): end time (ISO-8601).\n  - **`durationInMinutes`** (number | null): duration in minutes.\n  - **`isAllDay`** (boolean): true if the appointment spans whole days.\n  - **`location`** (string | null): location of the appointment.\n  - **`isPrivate`** (boolean): true if the appointment is marked as private.\n  - **`isCanceled`** (boolean): true if the appointment was canceled.\n  - **`isRecurring`** (boolean): true if the appointment is part of a recurring series.\n  - **`icalId`** (string | null): the iCalendar UID from the source calendar, for matching against your own copy. Not unique within a directory (one copy per synced calendar).\n  - **`attendees`** (array): the attendees. Each has:\n    - **`name`** (string | null): name of the attendee.\n    - **`email`** (string | null): email/address entry of the attendee.\n    - **`isRequired`** (boolean): true for a required participant, false for an optional one.\n    - **`invitationStatus`** (string enum): invitation status; one of `declined`, `accepted`, `tentative`, `notResponded`, `organizer`, `unknown`.\n    - **`attendanceStatus`** (string enum): attendance status; one of `notAttended`, `attended`, `unknown`.\n  - **`tags`** (array): effective tags (own + inherited from the recurring series master). Each has:\n    - **`tagId`** (string): id of the tag.\n    - **`name`** (string): tag display name.\n    - **`colorCode`** (string | null): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); null if unset.","operationId":"AppointmentsController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"subject","required":false,"in":"query","description":"Case-insensitive substring match on the subject. The term may appear at the start, middle, or end (e.g. \"meet\" matches \"Weekly Meeting\").","schema":{"type":"string"}},{"name":"attendee","required":false,"in":"query","description":"Case-insensitive substring match on an attendee name or email. The term may appear at the start, middle, or end (e.g. \"ann\" matches \"Joanna\").","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"Only appointments with StartTime >= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"Only appointments with StartTime <= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).","schema":{"type":"string"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,attendees","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `attendees`, `description`, `durationInMinutes`, `endTime`, `icalId`, `isAllDay`, `isCanceled`, `isPrivate`, `isRecurring`, `location`, `startTime`, `subject`, `tags`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiAppointmentListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches appointments (subject, attendee, time range), paginated.","tags":["appointments"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/appointments/search":{"post":{"description":"Searches appointments, sorted by StartTime descending. Body fields:\n- **`search`** (string, optional): case-insensitive substring over subject, description, and attendee name/email; a match in any of them counts.\n- **`from`** (string, optional): only appointments with StartTime >= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).\n- **`to`** (string, optional): only appointments with StartTime <= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`).\n- **`page`** (number, optional, default `1`): 1-based page number.\n- **`pageSize`** (number, optional, default `25`): page size, max. `100`.","operationId":"AppointmentsController_Search_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,attendees","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `attendees`, `description`, `durationInMinutes`, `endTime`, `icalId`, `isAllDay`, `isCanceled`, `isPrivate`, `isRecurring`, `location`, `startTime`, `subject`, `tags`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSearchAppointmentsBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiAppointmentListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches appointments (parameters in the body).","tags":["appointments"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/appointments/{appointmentId}/tags":{"put":{"description":"Replace-set of the tags on an appointment. Series tags live on the recurring master and are inherited, not set here. Max. 20 tags per appointment. Body fields:\n- **`tagIds`** (string array): existing tag ids to assign; the given set replaces the current one. Assigning only takes tag ids, so create the tag first via `POST /v1.0/directories/{directoryId}/tags` and assign it here.","operationId":"AppointmentsController_SetTags_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"appointmentId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the appointment."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`, `name`, `tagId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSetTagsBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTag"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Replaces the tags of an appointment (assign/unassign).","tags":["appointments"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/appointments/{appointmentId}/agenda":{"get":{"description":"The agenda belongs to the ONLINE MEETING, not to the single participant: the same meeting has one appointment row per user, and all of them share one agenda. This endpoint therefore reads across the sibling appointments of the same online meeting, which is what the participants see in the app as well.\n\nThis is the resolution path for `appointmentAgendaItemId` in the `agendaResults` of a summary. What the meeting made of each item is NOT here: that is the result of a separate evaluation run and belongs to the summary.","operationId":"AppointmentsController_GetAgenda_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"appointmentId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the appointment."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentAgendaItemId,note","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentAgendaItemId`, `note`, `sortOrder`, `title`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiAgendaItem"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the planned agenda of an appointment.","tags":["appointments"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/summaries/search":{"post":{"description":"Searches recording summaries and returns, per hit, an excerpt around the first match. Body fields:\n- **`search`** (string, required, min. 3 characters): the search query. How it matches:\n  - 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).\n  - Words of four or more characters also match inside longer words and in their inflected forms: `Festgeldkonten` finds `Festgeldkonto`, `Konto` finds `Festgeldkonto`.\n  - Words of up to three characters must stand as a whole word: `KI` does not match `Kinder`.\n  - Case is ignored, and umlaut spellings are interchangeable (`Müller` = `Mueller` = `Muller`).\n  - If only filler words remain, the whole query is matched as one case-insensitive substring.\n  - 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.\n- **`page`** (number, optional, default `1`): 1-based page number.\n- **`pageSize`** (number, optional, default `25`): page size, max. `100`.\n\nResults 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.","operationId":"SummariesController_Search_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `createdOn`, `languageCode`, `recordingId`, `recordingSummaryId`, `snippet`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSearchSummariesBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSummarySearchResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches recording summaries (parameters in the body).","tags":["recordingSummaries"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/summaries/{recordingSummaryId}":{"get":{"description":"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`.\n\nPath parameters:\n- **`directoryId`** (string): id of the directory (organization) the summary belongs to.\n- **`recordingSummaryId`** (string): id of the recording summary to return.\n\nResponse fields:\n- **`recordingSummaryId`** (string): id of this summary. A recording can have several summaries — one per language.\n- **`recordingId`** (string): id of the recording this summary was generated from.\n- **`appointmentId`** (string | null): id of the caller’s own appointment linked to the recording; null if the recording is not linked to an appointment.\n- **`languageCode`** (string | null): language of the summary text as an IETF language tag (e.g. `de-DE`, `en-US`); null if unknown.\n- **`summary`** (string | null): the summary text (Markdown); null if no summary text was generated.\n- **`createdOn`** (string): creation timestamp of the summary (UTC ISO-8601).\n- **`sectionItems`** (array): the structured, per-section results of the meeting template that produced this summary (always included on this endpoint). Each item has:\n  - **`sectionItemId`** (string): id of the section item.\n  - **`meetingTemplateSectionId`** (string | null): id of the meeting-template section this item was generated from (resolve via `GET .../meetingtemplates/{meetingTemplateId}`); null for derived items without a template section (e.g. task items).\n  - **`title`** (string): title of the section item.\n  - **`sectionType`** (string enum): the section type (`summary`, `tasks`, `topics`, `decisions`, `customList`, `freeText`, `table`, `appointment` or `email`).\n  - **`outputFormat`** (string enum): the format of `content` (`html` or `markdown`).\n  - **`sortOrder`** (number): order of the item within the summary.\n  - **`content`** (string | null): the free-text/Markdown content of the item. For `table`, `appointment` and `email` it carries a readable Markdown rendering of the dedicated fields below, so a client that only reads `content` still sees the section; `outputFormat` is then `markdown`.\n  - **`tableVariant`** (string enum | null): only for `table`. `structured` means the columns were fixed in the template and the result is in `tableData`; `free` means Sally chose them and the result is a Markdown table in `content`.\n  - **`tableData`** (object | null): columns and rows of a structured `table` section.\n  - **`followUpMeetingStartTime`** / **`followUpMeetingEndTime`** (string | null): the agreed wall-clock time of an `appointment` section, without a time zone.\n  - **`followUpMeetingParticipants`** (array | null): participants of an `appointment` section.\n  - **`emailRecipients`** (array | null): recipients of an `email` section, `to` and `cc` in one list, told apart by `kind`.\n  - **`subject`** (string | null): task subject (for task-like section items).\n  - **`description`** (string | null): task description (for task-like section items).\n  - **`responsibleUserName`** (string | null): name of the responsible person (task items).\n  - **`responsibleUserEmail`** (string | null): email of the responsible person (task items).\n  - **`dueDate`** (string | null): due date of the task item (ISO-8601).","operationId":"SummariesController_GetOne_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingSummaryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording summary."},{"name":"fields","required":false,"in":"query","schema":{"type":"string"},"example":"agendaResults,appointmentId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `agendaResults`, `appointmentId`, `createdOn`, `isAgendaEvaluationOutdated`, `isSummarizationCompleted`, `isSummarizationSucceeded`, `languageCode`, `recordingId`, `recordingSummaryId`, `sectionItems`, `summarizationCompletionReason`, `summary`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSummary"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single recording summary by id.","tags":["recordingSummaries"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/summaries/{recordingSummaryId}/document":{"get":{"description":"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.\n\nAlways 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.\n\nPath parameters:\n- **`directoryId`** (string): id of the directory the summary belongs to.\n- **`recordingSummaryId`** (string): id of the recording summary.\n\nQuery parameters:\n- **`format`** (string enum, required): one of `pdf`, `docx`, `odt`, `txt`, `md`.\n- **`languageCode`** (string enum, optional): one of `de-DE`, `en-US`, `es-ES`, `fr-FR`, `it-IT`, `nl-NL`, `pl-PL`; defaults to the language of the token owner.\n\nThis endpoint does not support `?fields=`.","operationId":"SummariesController_Document_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"recordingSummaryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording summary."},{"name":"format","required":true,"in":"query","description":"File format of the document.","schema":{"enum":["pdf","docx","odt","txt","md"],"type":"string"}},{"name":"languageCode","required":false,"in":"query","description":"IETF 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 with `en-US` labels.","schema":{"enum":["de-DE","en-US","es-ES","fr-FR","it-IT","nl-NL","pl-PL"],"type":"string"}}],"responses":{"200":{"description":"The 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*`).","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{"schema":{"type":"string","format":"binary"}},"application/vnd.oasis.opendocument.text":{"schema":{"type":"string","format":"binary"}},"text/plain; charset=utf-8":{"schema":{"type":"string","format":"binary"}},"text/markdown; charset=utf-8":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unknown or missing `format`, or unknown `languageCode`. The body lists the allowed values (`code`: `UNKNOWN_DOCUMENT_FORMAT` or `UNKNOWN_LANGUAGE_CODE`)."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"The directory has disabled this download for its members (`code`: `SUMMARY_DOWNLOAD_PREVENTED`)."},"404":{"description":"The source does not exist, belongs to another directory, or the caller cannot access the underlying recording."},"409":{"description":"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":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a recording summary as a finished document file.","tags":["recordingSummaries"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/webhooks":{"get":{"description":"Admins/owners see all webhooks of the directory (both personal and directory-wide); a regular user only sees their own (personal) webhooks.\n\nEach item has:\n- **`webhookId`** (string): id of the webhook.\n- **`name`** (string): display name of the webhook.\n- **`event`** (string enum): when the webhook fires. `summary.ready` after every summary, `manual` never on its own (it is started from the meeting or recording). Webhooks created before the current trigger model may still report `recording.summarized` or `meeting.summarized`; those two cannot be set any more.\n- **`url`** (string): target URL that Sally POSTs the event to.\n- **`isActive`** (boolean): whether the webhook is actively firing.\n- **`directoryWide`** (boolean): whether the webhook fires directory-wide (for recordings of ALL users, admin/owner only) or personally (only for the creator's own recordings).\n- **`createdOn`** (string): creation timestamp (ISO-8601).","operationId":"WebhooksController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,directoryWide","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `directoryWide`, `event`, `isActive`, `name`, `url`, `webhookId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiWebhook"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the registered webhooks.","tags":["webhooks"],"security":[{"SallyBearer":[]}]},"post":{"description":"Sally POSTs to the given URL when the event fires (payload: summary + custom insights + section items). Body fields:\n- **`url`** (string, required): the endpoint Sally sends the POST request to.\n- **`event`** (string, optional, default `summary.ready`): `summary.ready` fires after every summary, `manual` never fires on its own and is started from the meeting or recording. The event that triggers the webhook.\n- **`name`** (string, optional): a label to recognize the webhook in the list.\n- **`directoryWide`** (boolean, optional, default `false`): `false` = personal webhook, fires only for the creator's own recordings; `true` = fires for the recordings of ALL users (reserved for admins/owners).\n\nWebhooks registered here send the payload in version **3.0**. Version 3.1 adds `sectionTitle`, `meetingTemplateSectionId` and `responsibleUserId` to every entry in `sections`; a webhook is switched to it in Sally (Integrations, Webhooks tab, edit the webhook, Payload version). The payload fields are described in the webhook guide in the help center.","operationId":"WebhooksController_Create_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,directoryWide","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `directoryWide`, `event`, `isActive`, `name`, `url`, `webhookId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","example":"https://example.com/sally-webhook"},"event":{"type":"string","enum":["summary.ready","manual"],"example":"summary.ready"},"name":{"type":"string","example":"Mein CRM-Sync"},"directoryWide":{"type":"boolean","example":false}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiWebhook"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Registers a webhook (fires after the summary).","tags":["webhooks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/webhooks/{webhookId}":{"patch":{"description":"Partial update — only the fields present in the body change; the others stay as they are. Activating/deactivating is NOT done here but via the separate activate/deactivate endpoints. Same permissions as delete: a regular user can only change their own (personal) webhooks; admins/owners can change any webhook of the directory. Body fields:\n- **`url`** (string, optional): new HTTPS endpoint Sally POSTs to.\n- **`event`** (string, optional): new event that triggers the webhook.\n- **`name`** (string, optional): new label to recognize the webhook in the list.","operationId":"WebhooksController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"webhookId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the webhook."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,directoryWide","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `directoryWide`, `event`, `isActive`, `name`, `url`, `webhookId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","example":"https://example.com/sally-webhook"},"event":{"type":"string","enum":["summary.ready","manual"],"example":"summary.ready"},"name":{"type":"string","example":"Mein CRM-Sync"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiWebhook"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Updates a webhook (url, event, name).","tags":["webhooks"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Soft delete (active=false) per Sally convention. A regular user can only delete their own (personal) webhooks; admins/owners can delete any webhook of the directory.","operationId":"WebhooksController_Delete_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"webhookId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the webhook."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Deletes (deactivates) a webhook.","tags":["webhooks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/webhooks/{webhookId}/activate":{"post":{"description":"Resumes a paused webhook so it fires again. No request body. Same permissions as delete: a regular user can only change their own (personal) webhooks; admins/owners can change any webhook of the directory. Returns the updated webhook.","operationId":"WebhooksController_Activate_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"webhookId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the webhook."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,directoryWide","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `directoryWide`, `event`, `isActive`, `name`, `url`, `webhookId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiWebhook"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Activates a webhook.","tags":["webhooks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/webhooks/{webhookId}/deactivate":{"post":{"description":"Pauses a webhook without deleting it — it stays in the list but does not fire until it is reactivated. No request body. Same permissions as delete: a regular user can only change their own (personal) webhooks; admins/owners can change any webhook of the directory. Returns the updated webhook.","operationId":"WebhooksController_Deactivate_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"webhookId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the webhook."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,directoryWide","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `directoryWide`, `event`, `isActive`, `name`, `url`, `webhookId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiWebhook"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Deactivates a webhook.","tags":["webhooks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tags":{"get":{"description":"Returns all active tags of the directory, most-used first.\n\nResponse fields:\n- **`items`** (array): the tags in the catalog. Each item has:\n  - **`tagId`** (string): id of the tag.\n  - **`name`** (string): tag display name.\n  - **`colorCode`** (string | null): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); null if unset.","operationId":"TagsController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`, `name`, `tagId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiCatalogTagListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the tag catalog of a directory (most-used first).","tags":["tags"],"security":[{"SallyBearer":[]}]},"post":{"description":"Names are deduplicated case-insensitively. Body fields:\n- **`name`** (string, required): tag display name (max. 100 characters).\n- **`colorCode`** (string, optional): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); `null` to clear.","operationId":"TagsController_Create_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`, `name`, `tagId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTagUpsertBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiCatalogTag"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Creates a catalog tag with an optional colour (admin/owner only).","tags":["tags"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tags/colors":{"get":{"description":"Returns the fixed set of colour keys a tag's `colorCode` can take — a colour NAME, not a raw hex value. The client maps each key to the actual (theme-aware) colour; an unknown key or `null` renders in the neutral default colour.\n\nEach item has:\n- **`colorCode`** (string): a valid tag colour key (e.g. `blue`, `red`, `green`).","operationId":"TagsController_Colors_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTagColor"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the available tag colours.","tags":["tags"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tags/{tagId}":{"patch":{"description":"Send `name` to rename and/or `colorCode` to recolour; at least one is required. Body fields:\n- **`name`** (string, optional): new tag display name (max. 100 characters).\n- **`colorCode`** (string, optional): a colour key from the Sally tag palette (a colour name like `blue`/`red`/`green`, not a raw hex value; full list in the schema); `null` clears the colour.","operationId":"TagsController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"tagId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the tag."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"colorCode,name","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `colorCode`, `name`, `tagId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTagUpsertBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiCatalogTag"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Renames a catalog tag and/or changes its colour (admin/owner only).","tags":["tags"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Soft-deletes the tag and removes it from all appointments and recordings.","operationId":"TagsController_Delete_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"tagId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the tag."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Deletes a catalog tag (admin/owner only).","tags":["tags"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders":{"get":{"description":"Merges personal, shared and team folders into one list. Pagination via `?page` + `?pageSize` (max. 100, applied in-memory). An optional `?search` substring matches the folder name (case-insensitive). Pass `?parentRecordingFolderId` to list only the direct children of a folder; omit it to list all folders.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the folders on this page. Each item has:\n  - **`folderId`** (string): id of the folder.\n  - **`name`** (string): folder display name.\n  - **`description`** (string | null): folder description; null if unset.\n  - **`ownerUserId`** (string): id of the user who owns the folder.\n  - **`parentFolderId`** (string | null): id of the parent folder; null if this is a top-level folder.\n  - **`isOwner`** (boolean): true if the caller is the owner of the folder.\n  - **`isFavorite`** (boolean): true if the caller marked the folder as favorite.\n  - **`accessLevel`** (string, one of `none`/`read`/`readWrite`): the caller's effective access level on the folder.\n  - **`recordingsCount`** (number): number of recordings in the folder.\n  - **`createdOn`** (string): creation timestamp (ISO 8601).\n  - **`modifiedOn`** (string): last modification timestamp (ISO 8601).","operationId":"FoldersController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on the folder name. The term may appear at the start, middle, or end.","schema":{"type":"string"}},{"name":"parentRecordingFolderId","required":false,"in":"query","schema":{"type":"string"},"description":"Id of the parent recording folder."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"accessLevel,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `accessLevel`, `createdOn`, `description`, `folderId`, `isFavorite`, `isOwner`, `modifiedOn`, `name`, `ownerUserId`, `parentFolderId`, `recordingsCount`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolderListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the recording folders the caller can access (paginated, optionally filtered).","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders/search":{"post":{"description":"Searches recording folders. Merges personal, shared and team folders into one list and paginates in-memory. Body fields:\n- **`search`** (string, optional): case-insensitive substring on the folder name.\n- **`parentRecordingFolderId`** (string, optional): list only the direct children of that folder; omit for all folders.\n- **`page`** (number, optional, default `1`): 1-based page number.\n- **`pageSize`** (number, optional, default `25`): page size (max. 100).","operationId":"FoldersController_Search_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"accessLevel,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `accessLevel`, `createdOn`, `description`, `folderId`, `isFavorite`, `isOwner`, `modifiedOn`, `name`, `ownerUserId`, `parentFolderId`, `recordingsCount`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSearchFoldersBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolderListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches recording folders (parameters in the body).","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders/{folderId}":{"get":{"description":"The caller must be a member of the directory and have at least read access to the folder — otherwise 404/403. `accessLevel` reflects the effective access of the caller.\n\nResponse fields:\n- **`folderId`** (string): id of the folder.\n- **`name`** (string): folder display name.\n- **`description`** (string | null): folder description; null if unset.\n- **`ownerUserId`** (string): id of the user who owns the folder.\n- **`parentFolderId`** (string | null): id of the parent folder; null if this is a top-level folder.\n- **`isOwner`** (boolean): true if the caller is the owner of the folder.\n- **`isFavorite`** (boolean): true if the caller marked the folder as favorite.\n- **`accessLevel`** (string, one of `none`/`read`/`readWrite`): the caller's effective access level on the folder.\n- **`recordingsCount`** (number): number of recordings in the folder.\n- **`createdOn`** (string): creation timestamp (ISO 8601).\n- **`modifiedOn`** (string): last modification timestamp (ISO 8601).","operationId":"FoldersController_GetOne_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"accessLevel,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `accessLevel`, `createdOn`, `description`, `folderId`, `isFavorite`, `isOwner`, `modifiedOn`, `name`, `ownerUserId`, `parentFolderId`, `recordingsCount`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolder"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single recording folder.","tags":["folders"],"security":[{"SallyBearer":[]}]},"patch":{"description":"Updates the folder metadata. Only the provided fields are changed (partial); the shares are left untouched (use `PUT .../folders/{folderId}/shares` for those). Requires read-write access to the folder. Returns the updated folder. Body fields:\n- **`name`** (string, optional): new folder name; omit to leave unchanged.\n- **`description`** (string, optional): new description; `null` clears it, omit to leave unchanged.\n- **`parentFolderId`** (string, optional): id of the new parent folder to move this folder under; `null` makes it a top-level folder, omit to leave unchanged.","operationId":"FoldersController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"accessLevel,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `accessLevel`, `createdOn`, `description`, `folderId`, `isFavorite`, `isOwner`, `modifiedOn`, `name`, `ownerUserId`, `parentFolderId`, `recordingsCount`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUpdateFolderBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolder"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Updates a recording folder (name, description, parent).","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders/{folderId}/shares":{"get":{"description":"Direct user shares, team shares and inherited shares (from parent folders). Each share carries only ids + access level; resolve user/team details via the membership/team endpoints.\n\nResponse fields:\n- **`userShares`** (array): direct user shares of the folder. Each item has:\n  - **`userId`** (string): id of the user this folder is shared with.\n  - **`accessLevel`** (string, one of `none`/`read`/`readWrite`): access level granted to the user.\n- **`teamShares`** (array): team shares of the folder. Each item has:\n  - **`teamId`** (string): id of the team this folder is shared with.\n  - **`accessLevel`** (string, one of `none`/`read`/`readWrite`): access level granted to the team.\n- **`inheritedShares`** (array): shares inherited from parent folders (read-only). Each item has:\n  - **`userId`** (string): id of the user who inherits access.\n  - **`accessLevel`** (string, one of `none`/`read`/`readWrite`): inherited access level.\n  - **`inheritedFromFolderId`** (string): id of the parent folder the access is inherited from.","operationId":"FoldersController_GetShares_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"inheritedShares,teamShares","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `inheritedShares`, `teamShares`, `userShares`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolderShares"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns the shares of a recording folder.","tags":["folders"],"security":[{"SallyBearer":[]}]},"put":{"description":"Replaces the direct user shares and team shares of the folder with the provided sets (inherited shares are read-only and cannot be set). Only the folder owner may change shares. Returns the resulting shares. Body fields:\n- **`userShares`** (array, required): direct user shares to apply, each `{ userId (string), accessLevel (`none`/`read`/`readWrite`) }`. The resulting set replaces the current one.\n- **`teamShares`** (array, required): team shares to apply, each `{ teamId (string), accessLevel (`none`/`read`/`readWrite`) }`. The resulting set replaces the current one.","operationId":"FoldersController_SetShares_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"inheritedShares,teamShares","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `inheritedShares`, `teamShares`, `userShares`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSetFolderSharesBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolderShares"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Sets the shares of a recording folder (replace-set).","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders/{folderId}/recordings":{"get":{"description":"Pass `?includeSubfolders=true` to also include the recordings of all accessible subfolders (recursive, deduplicated). Sorted by creation date descending. Pagination via `?page` + `?pageSize` (max. 100, applied in-memory). Each item references the recording by `recordingId`; fetch full details via `GET .../recordings/{recordingId}`.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the recordings on this page. Each item has:\n  - **`recordingId`** (string): id of the recording.\n  - **`durationInSeconds`** (number | null): duration in seconds; null if unknown.\n  - **`isManualUpload`** (boolean): true if manually uploaded (no meeting bot).\n  - **`appointmentId`** (string | null): id of the caller's own appointment linked to the recording; null if none.\n  - **`createdOn`** (string): creation timestamp (ISO 8601, UTC).","operationId":"FoldersController_ListRecordings_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"includeSubfolders","required":false,"in":"query","description":"When `true`, recordings of all nested subfolders are included as well. When omitted or `false`, only the recordings placed directly in this folder are returned.","schema":{"type":"boolean"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,createdOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `createdOn`, `durationInSeconds`, `isManualUpload`, `recordingId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiFolderRecordingListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the recordings in a folder (paginated).","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/folders/{folderId}/recordings/{recordingId}":{"put":{"description":"Idempotent. Requires read-write access to the folder. 404 if the recording does not exist in the directory.","operationId":"FoldersController_AddRecording_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Adds a recording to a folder.","tags":["folders"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Requires read-write access to the folder.","operationId":"FoldersController_RemoveRecording_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"folderId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the folder."},{"name":"recordingId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the recording."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Removes a recording from a folder.","tags":["folders"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tasks":{"get":{"description":"The `?filter` selects a task view (default `myTasks`); the `all*` variants are admin/owner only. An optional `?search` substring matches the task subject. Pagination via `?page` + `?pageSize` (max. 100). Resolve `responsibleUserId`/`ownerId` via `GET /v1.0/directories/{directoryId}/memberships/users/{userId}`.\n\nResponse fields:\n- **`page`** / **`pageSize`** / **`total`** / **`hasMore`**: pagination envelope (current page, page size, total count, whether more pages follow).\n- **`items`** (array): the tasks on this page. Each item has:\n  - **`taskId`** (string): id of the task.\n  - **`subject`** (string): task subject.\n  - **`description`** (string | null): task description; null if unset.\n  - **`ownerId`** (string): id of the user who owns (created) the task.\n  - **`responsibleUserId`** (string | null): id of the user responsible for the task; null if unassigned.\n  - **`taskCategoryId`** (string | null): id of the task category; null if none. Resolve via `GET /v1.0/directories/{directoryId}/task-categories`.\n  - **`dueDate`** (string | null): due date (ISO 8601); null if no due date is set.\n  - **`isDone`** (boolean): whether the task is marked as done.\n  - **`doneOn`** (string | null): timestamp when the task was completed (ISO 8601); null if not done.\n  - **`isCanceled`** (boolean): whether the task is canceled.\n  - **`canceledOn`** (string | null): timestamp when the task was canceled (ISO 8601); null if not canceled.\n  - **`recordingSummaryId`** (string | null): id of the recording summary this task originated from; null if none.\n  - **`appointmentId`** (string | null): id of the appointment this task is linked to; null if none.\n  - **`createdOn`** (string): creation timestamp (ISO 8601).\n  - **`modifiedOn`** (string): last modification timestamp (ISO 8601).","operationId":"TasksController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"filter","required":false,"in":"query","description":"Named preset that combines scope and state. `my…` covers every task the calling user is involved in, which means they are the responsible person OR the owner; `all…` every task of the directory the caller may see. The state part is `Tasks` (any), `Open`, `Overdue`, `Completed` or `Forwarded`. `Forwarded` is the one exception to the scope above: it lists only what the caller handed to someone else (owner is the caller, responsible is not). Omitted means no preset filter.","schema":{"enum":["myTasks","myOpen","myOverdue","myCompleted","myForwarded","allTasks","allOpen","allOverdue","allCompleted","allForwarded"],"type":"string"}},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on the task subject. The term may appear at the start, middle, or end.","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTaskListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the tasks of a directory (paginated, optionally filtered).","tags":["tasks"],"security":[{"SallyBearer":[]}]},"post":{"description":"Creates a task. The caller becomes the owner. Body fields:\n- **`subject`** (string, required): task subject (1..1000 characters).\n- **`description`** (string, optional): task description (max. 4000 characters); null for none.\n- **`responsibleUserId`** (string, optional): id of the responsible user; null or omitted leaves the task unassigned.\n- **`taskCategoryId`** (string, optional): id of the task category; null or omitted for none. Pick one from `GET /v1.0/directories/{directoryId}/task-categories`.\n- **`dueDate`** (string, optional): due date as an ISO 8601 string; null for no due date.","operationId":"TasksController_Create_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiCreateTaskBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTask"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Creates a task.","tags":["tasks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tasks/search":{"post":{"description":"Searches tasks. Body fields:\n- **`search`** (string, optional): case-insensitive substring over the task subject AND description.\n- **`responsible`** (string, optional): filter by responsible user id — your own is always allowed, another user or omitting it (= all responsibles) requires admin/owner.\n- **`status`** (string, optional): one of `open` (not done/canceled), `completed`, `overdue` (open + past due), `canceled`; omit for any status.\n- **`page`** (number, optional, default `1`): 1-based page number.\n- **`pageSize`** (number, optional, default `25`): page size (max. 100).\nResults are sorted by due date ascending.","operationId":"TasksController_Search_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSearchTasksBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTaskListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Searches tasks (parameters in the body).","tags":["tasks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tasks/{taskId}":{"get":{"description":"The caller must be allowed to view the task (owner, responsible, or admin/owner).\n\nResponse fields:\n- **`taskId`** (string): id of the task.\n- **`subject`** (string): task subject.\n- **`description`** (string | null): task description; null if unset.\n- **`ownerId`** (string): id of the user who owns (created) the task.\n- **`responsibleUserId`** (string | null): id of the user responsible for the task; null if unassigned.\n- **`dueDate`** (string | null): due date (ISO 8601); null if no due date is set.\n- **`isDone`** (boolean): whether the task is marked as done.\n- **`doneOn`** (string | null): timestamp when the task was completed (ISO 8601); null if not done.\n- **`isCanceled`** (boolean): whether the task is canceled.\n- **`canceledOn`** (string | null): timestamp when the task was canceled (ISO 8601); null if not canceled.\n- **`recordingSummaryId`** (string | null): id of the recording summary this task originated from; null if none.\n- **`appointmentId`** (string | null): id of the appointment this task is linked to; null if none.\n- **`createdOn`** (string): creation timestamp (ISO 8601).\n- **`modifiedOn`** (string): last modification timestamp (ISO 8601).","operationId":"TasksController_GetOne_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"taskId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the task."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTask"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single task by id.","tags":["tasks"],"security":[{"SallyBearer":[]}]},"patch":{"description":"Updates a task. Only the provided fields are changed; omitted fields keep their current value, a null value explicitly clears a field. The caller must be allowed to edit the task. Body fields:\n- **`subject`** (string, optional): new task subject (1..1000 characters).\n- **`description`** (string, optional): new task description (max. 4000 characters); null to clear.\n- **`responsibleUserId`** (string, optional): new id of the responsible user; null to unassign.\n- **`taskCategoryId`** (string, optional): new id of the task category; null to clear. Pick one from `GET /v1.0/directories/{directoryId}/task-categories`.\n- **`dueDate`** (string, optional): new due date as an ISO 8601 string; null to clear.","operationId":"TasksController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"taskId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the task."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUpdateTaskBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTask"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Updates a task (partial update).","tags":["tasks"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Only the owner or an admin/owner may delete a task.","operationId":"TasksController_Delete_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"taskId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the task."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Deletes a task (soft-delete).","tags":["tasks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/tasks/{taskId}/done":{"post":{"description":"Toggles the done status of a task. Body fields:\n- **`isDone`** (boolean, required): `true` to mark the task as done, `false` to reopen it.","operationId":"TasksController_ToggleDone_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"taskId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the task."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"appointmentId,canceledOn","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `appointmentId`, `canceledOn`, `createdOn`, `description`, `doneOn`, `dueDate`, `isCanceled`, `isDone`, `modifiedOn`, `ownerId`, `recordingSummaryId`, `responsibleUserId`, `subject`, `taskCategoryId`, `taskId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiToggleDoneBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTask"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Toggles the done status of a task.","tags":["tasks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/task-categories":{"get":{"description":"Returns the categories a task can carry: the ones the directory owns plus the global Sally defaults, ordered by name.\n\nDeactivated categories are included with `active: false` so a task that still carries one stays resolvable; they are no longer offered for new tasks.\n\nResponse fields:\n- **`items`** (array): the task categories. Each item has:\n  - **`taskCategoryId`** (string): id of the category.\n  - **`name`** (string): category name.\n  - **`description`** (string | null): what the category covers; this text is what Sally is given when it picks a category for a recognized task. Null if unset.\n  - **`active`** (boolean): whether the category is still offered for new tasks.\n  - **`isGlobal`** (boolean): true for a global Sally default, false for one the directory owns.","operationId":"TaskCategoriesController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"active,description","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `active`, `description`, `isGlobal`, `name`, `taskCategoryId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTaskCategoryListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the task categories of a directory (resolves `taskCategoryId`).","tags":["tasks"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/vocabulary":{"get":{"description":"Returns all directory-wide entries plus the caller's own personal entries, sorted by phrase.\n\nEach item has:\n- **`vocabularyId`** (string): id of the vocabulary entry.\n- **`phrase`** (string): the term/phrase Sally should recognize preferentially.\n- **`manualReplacePhrases`** (string | null): alternative spellings/misrecognitions that get replaced with `phrase` in the transcript (comma-separated); null if none.\n- **`description`** (string | null): free-text note describing the entry (not used for recognition); null if unset.\n- **`languageCode`** (string | null): language the entry applies to, as an IETF language tag (e.g. `de-DE`, `en-US`); null = applies to all languages.\n- **`isDirectoryWide`** (boolean): scope — `true` = directory-wide (recognized for every member of the organization), `false` = personal (only the caller's own recordings).\n- **`createdOn`** (string): when the entry was created (UTC ISO-8601).\n- **`modifiedOn`** (string): when the entry was last changed (UTC ISO-8601).","operationId":"VocabularyController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,description","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `description`, `isDirectoryWide`, `languageCode`, `manualReplacePhrases`, `modifiedOn`, `phrase`, `vocabularyId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiVocabulary"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the vocabulary entries of a directory.","tags":["vocabulary"],"security":[{"SallyBearer":[]}]},"post":{"description":"Personal entries are allowed for any member; directory-wide entries require admin/owner rights. Body fields:\n- **`phrase`** (string, required): the term/phrase to recognize (max. 400 characters).\n- **`manualReplacePhrases`** (string, optional): manual alternative spellings (max. 4000 characters); `null` to clear.\n- **`description`** (string, optional): free-text description/note (max. 4000 characters); `null` to clear.\n- **`languageCode`** (string, optional): language as an IETF language tag (e.g. `de-DE`); `null`/omitted for language-agnostic.\n- **`isDirectoryWide`** (boolean, optional, default `false`): `false` = personal to the caller, `true` = directory-wide (requires admin/owner rights).","operationId":"VocabularyController_Create_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,description","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `description`, `isDirectoryWide`, `languageCode`, `manualReplacePhrases`, `modifiedOn`, `phrase`, `vocabularyId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUpsertVocabularyBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiVocabulary"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Creates a vocabulary entry.","tags":["vocabulary"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/vocabulary/{vocabularyId}":{"put":{"description":"Personal entries are allowed for any member; directory-wide entries require admin/owner rights. Body fields:\n- **`phrase`** (string, required): the term/phrase to recognize (max. 400 characters).\n- **`manualReplacePhrases`** (string, optional): manual alternative spellings (max. 4000 characters); `null` to clear.\n- **`description`** (string, optional): free-text description/note (max. 4000 characters); `null` to clear.\n- **`languageCode`** (string, optional): language as an IETF language tag (e.g. `de-DE`); `null`/omitted for language-agnostic.\n- **`isDirectoryWide`** (boolean, optional, default `false`): `false` = personal to the caller, `true` = directory-wide (requires admin/owner rights).","operationId":"VocabularyController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"vocabularyId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the vocabulary."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"createdOn,description","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `createdOn`, `description`, `isDirectoryWide`, `languageCode`, `manualReplacePhrases`, `modifiedOn`, `phrase`, `vocabularyId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUpsertVocabularyBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiVocabulary"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Updates a vocabulary entry.","tags":["vocabulary"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Soft-deletes the entry. Deleting a directory-wide entry requires admin/owner rights.","operationId":"VocabularyController_Delete_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"vocabularyId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the vocabulary."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Deletes a vocabulary entry.","tags":["vocabulary"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/teams":{"get":{"description":"Each item has:\n- **`teamId`** (string): id of the team.\n- **`name`** (string): team name.\n- **`code`** (string | null): optional team code.\n- **`isScimGroup`** (boolean): whether the team is synced from a SCIM identity provider.\n- **`memberCount`** (number): number of active members.","operationId":"TeamsController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"code,isScimGroup","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `code`, `isScimGroup`, `memberCount`, `name`, `teamId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTeam"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the teams of a directory.","tags":["teams"],"security":[{"SallyBearer":[]}]},"post":{"description":"Only admins/owners may create teams. Body fields:\n- **`name`** (string, required): team name.\n- **`code`** (string, optional): optional team code.","operationId":"TeamsController_Create_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"code,isScimGroup","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `code`, `isScimGroup`, `memberCount`, `name`, `teamId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiCreateTeamBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTeam"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Creates a team.","tags":["teams"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/teams/{teamId}":{"patch":{"description":"Only admins/owners. Body fields:\n- **`name`** (string, required): new team name.\n- **`code`** (string, optional): new team code; `null` clears it, omit to leave unchanged.","operationId":"TeamsController_Update_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"code,isScimGroup","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `code`, `isScimGroup`, `memberCount`, `name`, `teamId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUpdateTeamBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiTeam"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Updates a team (rename + optional code).","tags":["teams"],"security":[{"SallyBearer":[]}]},"delete":{"description":"Soft-delete (archive) of the team and all its memberships. Only admins/owners.","operationId":"TeamsController_Archive_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."}],"responses":{"204":{"description":""},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Removes (archives) a team.","tags":["teams"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/teams/{teamId}/memberships":{"get":{"description":"Membership scalars only (`userId`, `isMainTeam`); fetch user details via `GET .../teams/{teamId}/memberships/users/{userId}`.\n\nEach item has:\n- **`teamId`** (string): id of the team.\n- **`userId`** (string): id of the member user.\n- **`isMainTeam`** (boolean): whether this team is the user’s main team.","operationId":"TeamsController_Memberships_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"isMainTeam,teamId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `isMainTeam`, `teamId`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTeamMembership"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the memberships of a team.","tags":["teams"],"security":[{"SallyBearer":[]}]},"put":{"description":"Only admins/owners. Body fields:\n- **`userIds`** (array of strings, required): full replace-set of member user ids. Users not in the list are removed.","operationId":"TeamsController_SetMembers_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"isMainTeam,teamId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `isMainTeam`, `teamId`, `userId`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiSetTeamMembersBody"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiTeamMembership"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"403":{"description":"Forbidden (insufficient permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Sets the members of a team (full replace-set).","tags":["teams"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/teams/{teamId}/memberships/users":{"get":{"description":"Each item has:\n- **`userId`** (string): id of the user.\n- **`email`** (string): email address of the user.\n- **`firstName`** (string): first name.\n- **`lastName`** (string): last name.\n- **`pictureFileUrl`** (string | null): absolute URL of the profile picture, or null. Requires the same Bearer token as the API.","operationId":"TeamsController_Users_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `pictureFileUrl`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiUser"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the users (members) of a team.","tags":["teams"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/teams/{teamId}/memberships/users/{userId}":{"get":{"description":"The user must be a member of the team, otherwise 404.\n\nResponse fields:\n- **`userId`** (string): id of the user.\n- **`email`** (string): email address of the user.\n- **`firstName`** (string): first name.\n- **`lastName`** (string): last name.\n- **`pictureFileUrl`** (string | null): absolute URL of the profile picture, or null. Requires the same Bearer token as the API.","operationId":"TeamsController_User_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"teamId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the team."},{"name":"userId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the user."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"email,firstName","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `email`, `firstName`, `lastName`, `pictureFileUrl`, `userId`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiUser"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Returns a single user (member) of a team.","tags":["teams"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/meetingtemplates":{"get":{"description":"Returns the published templates the authenticated user can use in this directory: their own personal templates, the templates of their teams and the directory-wide templates. Paginated via `?page` + `?pageSize` (max. 100). Use the detail endpoint (or `.../{meetingTemplateId}/sections`) to read a template’s sections.\n\nEach item has:\n- **`meetingTemplateId`** (string): id of the template (use it for the detail/sections endpoints).\n- **`title`** (string): template title.\n- **`description`** (string | null): template description; null if unset.","operationId":"MeetingTemplatesController_List_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"page","required":false,"in":"query","schema":{"type":"number"},"description":"Page number, 1-based. Defaults to 1."},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"},"description":"Items per page. Defaults to 25, capped at 100."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"description,meetingTemplateId","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `description`, `meetingTemplateId`, `title`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiMeetingTemplateListResponse"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists the meeting templates the caller can use in a directory.","tags":["meetingTemplates"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/meetingtemplates/{meetingTemplateId}":{"get":{"description":"Returns the template and its published sections in sort order. Each section carries its `meetingTemplateSectionId` — the same id referenced by a summary section item, so a summary section item can be mapped back to its template section. To fetch only the sections, use `GET .../meetingtemplates/{meetingTemplateId}/sections`.\n\nResponse fields:\n- **`meetingTemplateId`** (string): id of the template.\n- **`title`** (string): template title.\n- **`description`** (string | null): template description; null if unset.\n- **`scope`** (string enum): who the template belongs to — `global` (a built-in Sally template), `directory` (the organization’s own), `team` or `personal`.\n- **`sections`** (array): the published sections in sort order. Each section has:\n  - **`meetingTemplateSectionId`** (string): id of the section; referenced by a summary section item’s `meetingTemplateSectionId`.\n  - **`title`** (string): section title.\n  - **`description`** (string | null): section description; null if unset.\n  - **`sectionType`** (string enum): the section type (`summary`, `tasks`, `topics`, `decisions`, `customList`, `freeText`, `table`, `appointment` or `email`).\n  - **`outputFormat`** (string enum): the format the section content is generated in (`html` or `markdown`).\n  - **`detailLevel`** (string enum): the requested level of detail for the section (`short`, `medium` or `long`).\n  - **`sortOrder`** (number): order of the section within the template.\n  - **`additionalInstructions`** (string | null): extra generation instructions for the section; null if none.\n  - **`isTimestampInclusionEnabled`** (boolean): whether transcript timestamps are included in the section.\n  - **`isResponsibleDetectionEnabled`** (boolean): whether a responsible person is detected for the section.\n  - **`isDueDateInferenceEnabled`** (boolean): whether a due date is inferred for the section.\n  - **`smallTalk`** and **`privateTopics`** (string enum): whether small talk and private topics may appear in THIS section (`inherit`, `include` or `exclude`). `inherit` follows the template, whose own setting is `isSmallTalkInclusionEnabled` / `isPrivateTopicsInclusionEnabled` on this response.","operationId":"MeetingTemplatesController_Get_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"meetingTemplateId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the meeting template."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"description,isPrivateTopicsInclusionEnabled","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `description`, `isPrivateTopicsInclusionEnabled`, `isSmallTalkInclusionEnabled`, `meetingTemplateId`, `scope`, `sections`, `title`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiMeetingTemplateDetail"}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Views one meeting template including its published sections.","tags":["meetingTemplates"],"security":[{"SallyBearer":[]}]}},"/v1.0/directories/{directoryId}/meetingtemplates/{meetingTemplateId}/sections":{"get":{"description":"Returns just the published sections of the template in sort order — the same sections as the detail endpoint returns, but without the template meta. Each section carries its `meetingTemplateSectionId`, the same id referenced by a summary section item (so a summary section item can be mapped back to its template section).\n\nSection fields:\n- **`meetingTemplateSectionId`** (string): id of the section; referenced by a summary section item’s `meetingTemplateSectionId`.\n- **`title`** (string): section title.\n- **`description`** (string | null): section description; null if unset.\n- **`sectionType`** (string enum): the section type (`summary`, `tasks`, `topics`, `decisions`, `customList`, `freeText`, `table`, `appointment` or `email`).\n- **`outputFormat`** (string enum): the format the section content is generated in (`html` or `markdown`).\n- **`detailLevel`** (string enum): the requested level of detail for the section (`short`, `medium` or `long`).\n- **`sortOrder`** (number): order of the section within the template.\n- **`additionalInstructions`** (string | null): extra generation instructions for the section; null if none.\n- **`isTimestampInclusionEnabled`** (boolean): whether transcript timestamps are included in the section.\n- **`isResponsibleDetectionEnabled`** (boolean): whether a responsible person is detected for the section.\n- **`isDueDateInferenceEnabled`** (boolean): whether a due date is inferred for the section.\n- **`smallTalk`** and **`privateTopics`** (string enum): whether small talk and private topics may appear in THIS section (`inherit`, `include` or `exclude`). `inherit` follows the template; its own two switches are on the template detail endpoint.","operationId":"MeetingTemplatesController_Sections_v1.0","parameters":[{"name":"directoryId","required":true,"in":"path","schema":{"type":"string"},"description":"Id 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`."},{"name":"meetingTemplateId","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the meeting template."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"example":"additionalInstructions,description","description":"Comma-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.\n\nOnly first-level fields can be named. A nested list such as `attendees`, `tags` or `sectionItems` is returned whole or not at all; `attendees.name` is not supported.\n\nOn a paged response the selection applies to the entries in `items`; `page`, `pageSize`, `total` and `hasMore` are always returned. An unknown name is rejected with 400 rather than ignored.\n\nSelectable here: `additionalInstructions`, `description`, `detailLevel`, `isDueDateInferenceEnabled`, `isResponsibleDetectionEnabled`, `isTimestampInclusionEnabled`, `meetingTemplateSectionId`, `outputFormat`, `privateTopics`, `sectionType`, `smallTalk`, `sortOrder`, `tableColumns`, `tableVariant`, `title`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiMeetingTemplateSection"}}}}},"400":{"description":"Invalid request (validation error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"404":{"description":"Resource not found or not accessible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"429":{"description":"Rate limit exceeded (per token/IP). Transcription uploads may also return `code` `FUP_LIMIT_EXCEEDED` or `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiError"}}}}},"summary":"Lists only the sections of a meeting template.","tags":["meetingTemplates"],"security":[{"SallyBearer":[]}]}}},"info":{"title":"Sally Public API","description":"External REST API for Sally customers and integration partners. All endpoints live under the version prefix `/v1.0`.\n\n### Versioning\nThe version is the first path segment: `/v1.0`.\n\n**Version 1.0 is under active development and is not frozen yet.** It can still change, including in ways that would break a client, so it is not a stable contract. We will announce when it is frozen; build against it with that in mind.\n\nOnce a version is frozen it keeps answering exactly as it did at that moment and is never changed again. Work then continues on the next version (`/v1.1`), and a client that wants the older behaviour simply stays on its path. A redesign too large to carry that way becomes a new major (`/v2`).\n\nA new version is created only for a change that could break a client, never for a purely additive one: a new endpoint, a new field or a new enum value appears in the version you are already on. A client must therefore ignore unknown fields and tolerate unknown enum values.\n\n`GET /health` is deliberately unversioned: a liveness probe says nothing about the contract, and a monitor should not have to follow a version change.\n\nNo version is frozen yet.\n\n### Base URL & conventions\nAll paths shown here are relative to the server URL above and are prefixed with `/v1.0`. Requests and responses are JSON (`Content-Type: application/json`). All ids are UUIDs (v4, lower-case). Fields are `camelCase`. Booleans are real JSON booleans, not strings. A field that can be absent is returned as `null` (never omitted).\n\n### Entities\nThe API exposes these entities (most live under a directory, i.e. `/v1.0/directories/{directoryId}/...`):\n- **Directory**: a Sally company account and the top-level tenant (path segment `directories`, id `directoryId`). Every other resource belongs to exactly one directory. A token can address the directories its user is a member of (`GET /v1.0/me/directories/memberships`).\n- **User**: a person. Basic identity only (name, email, picture).\n- **Membership**: ties a user to a directory with a role and a license (the DB entity `UserHasAccessToDirectory`). Identity = (`directoryId`, `userId`).\n- **Role** / **License**: lookups that a membership references (`GET /v1.0/roles`, `GET /v1.0/licenses`).\n- **Team**: a named group of users inside a directory.\n- **Appointment**: a calendar meeting/event with its attendees. See „Appointments, recordings & summaries\" below for the important per-user duplication.\n- **Recording**: one audio/video recording of a meeting, plus its transcript.\n- **Recording summary** (`recordingSummaries`): the AI-generated summary of a recording — one per language. Its content is a list of section items (generated from a meeting template’s sections).\n- **Meeting template**: defines the sections a recording summary is generated into (read-only). Each section carries a `meetingTemplateSectionId` that a summary section item refers back to.\n- **Folder**: organizes recordings into a tree.\n- **Task**: an action item (e.g. derived from a meeting).\n- **Tag**: a label assigned to appointments and recordings.\n- **Vocabulary**: speech-to-text context phrases that improve transcription.\n- **Webhook**: an outbound subscription that delivers events to your URL.\n- **File**: a binary asset (e.g. a profile picture), fetched by id.\n\n#### Appointments, recordings & summaries\nThese three are related but scoped differently — this matters when you join them:\n- An **appointment** is created **per user**: every attendee of the same real-world meeting has their own appointment record. The same meeting can therefore appear **multiple times** within one directory (one per user). Do NOT assume a subject/time uniquely identifies a meeting.\n- A **recording** is **unique within the directory**: there is exactly one recording per recorded meeting, regardless of how many users attended.\n- A **recording summary** always belongs to exactly one recording. A recording can have **several summaries — one per language** — and a summary is **unique per (recording, language)**. To pick a specific summary, key on the recording plus the language (`languageCode`).\n\n### Authentication\nEvery request (except `GET /health`) requires `Authorization: Bearer <token>`. Two token types are accepted: a **REST API personal access token** (prefix `sally_user_api_`, created in the Sally app under integrations; user-scoped, does not expire unless you set an expiry) or an **OAuth 2.1 access token** issued by Sally. A token acts as the user it belongs to: it can access exactly the directories that user is a member of, with that user’s role and visibility. Use `GET /v1.0/me` and `GET /v1.0/me/directories/memberships` to discover the authenticated user and the directories (`directoryId`) it can address.\n\n### Rate limits\nRequests are rate-limited per token (and per client IP for unauthenticated requests). Additional per-directory limits apply. Exceeding a limit returns `429`. Transcription uploads are additionally governed by a fair-usage policy (see the `429` `code` field).\n\n### Pagination\nPaginated list endpoints take `?page` (1-based, default 1) and `?pageSize` (default 25, max 100) query parameters and return an envelope: `{ \"page\": number, \"pageSize\": number, \"total\": number, \"hasMore\": boolean, \"items\": [...] }`. `total` is the total number of matching items across all pages. Non-paginated list endpoints return a plain JSON array.\n\n### Search endpoints\nEach searchable resource has a dedicated `POST /v1.0/directories/{directoryId}/{resource}/search` endpoint that takes its parameters in the JSON **body** (not the query string) — this avoids any character-encoding issues with the search term in the URL. The plain `GET` list endpoints stay unchanged as pure list endpoints. A search body always has exactly ONE free-text field named `search` (a case-insensitive substring OR-matched across all relevant text fields of the resource — the exact fields are documented on each endpoint), plus the resource’s structural filters and the pagination fields `page` / `pageSize`. The response envelope is identical to the matching list endpoint. The searchable resources are `appointments`, `summaries`, `tasks`, `folders` and `memberships/users`. The summary search (`summaries`) matches word by word instead of as one substring, see „Text search\" below.\n\n### Text search\nAll free-text search parameters (e.g. `search`, `subject`, `attendee`) use the same matching: a **case-insensitive substring** match — the term matches if it appears anywhere inside the target value (equivalent to SQL `%term%`), regardless of upper/lower case. Matching is NOT limited to the beginning or end of the value, and it is not exact-only. The term is matched **literally**: wildcard/special characters such as `%`, `_` or `*` have no special meaning and are treated as ordinary characters. Leading/trailing whitespace is trimmed; an empty term means „no filter\". Some search endpoints require a minimum term length (stated on the endpoint).\n\n**Exception: the summary search** (`POST …/summaries/search`) splits `search` into words and matches ANY of them; longer words also match inside longer words and in inflected forms, short words only as a whole word, and umlaut spellings are interchangeable. The exact rule is documented on that endpoint; it is the same rule the Sally MCP uses.\n\n### Dates & times\nAll date-time values — both in responses and in request inputs (query parameters and bodies) — are **ISO-8601 in UTC** with the trailing `Z`, format `YYYY-MM-DDTHH:MM:SSZ` (e.g. `2026-08-27T14:30:00Z`). Responses are always emitted in UTC. Request inputs MUST be UTC: a value without a timezone suffix is interpreted as UTC (never as server-local time), and an explicit offset is converted to the corresponding UTC instant. Date-only values (`YYYY-MM-DD`) are treated as UTC midnight.\n\n### Errors\nEvery error response uses the same JSON body: `{ \"statusCode\": number, \"message\": string, \"code\"?: string }`. `code` is a stable, machine-readable identifier that is present only for specific errors (e.g. `FUP_LIMIT_EXCEEDED`, `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`); generic errors omit it. Status codes used across the API:\n- `400` Invalid request (validation of body/query/path).\n- `401` Missing or invalid bearer token.\n- `403` Insufficient permissions (e.g. admin/owner-only or owner-only operation).\n- `404` Resource not found or not accessible to the caller (no existence leak).\n- `409` Conflict (e.g. inviting a user who is already a member or already invited).\n- `429` Rate limit exceeded; for transcription uploads also the fair-usage limit (`code` = `FUP_LIMIT_EXCEEDED` / `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`).\n- `500` Internal server error.","version":"1.0","contact":{"name":"Sally Support","url":"https://sally.io","email":"support@sally.io"},"license":{"name":"Proprietary","url":"https://sally.io/terms"}},"tags":[{"name":"meta","description":"Health checks, metadata"}],"servers":[{"url":"https://api.sally.io","description":"Currently reachable host"}],"components":{"securitySchemes":{"SallyBearer":{"scheme":"bearer","bearerFormat":"JWT / PAT","type":"http"}},"schemas":{"PublicApiHealth":{"type":"object","properties":{"status":{"type":"string","description":"Health status of the service; `ok` when it is running.","example":"ok"},"service":{"type":"string","description":"Name of the service that answered."},"environment":{"type":"string","description":"Environment the service runs in (e.g. `dev`, `prod`)."},"version":{"type":"string","description":"Version of the running service."},"timestamp":{"type":"string","description":"Time the response was produced (UTC ISO-8601)."}},"required":["status","service","environment","version","timestamp"]},"PublicApiMe":{"type":"object","properties":{"userId":{"type":"string","description":"Unique user id."},"email":{"type":"string","description":"Email address of the user."},"firstName":{"type":"string","description":"First name."},"lastName":{"type":"string","description":"Last name."},"timeZone":{"type":"string","description":"IANA time zone identifier of the user (e.g. \"Europe/Berlin\"), suitable for Intl.DateTimeFormat and date math. Null if the user has no time zone set.","nullable":true}},"required":["userId","email","firstName","lastName","timeZone"]},"PublicApiMyMembership":{"type":"object","properties":{"directoryId":{"type":"string","description":"Id of the directory."},"directoryName":{"type":"string","description":"Display name of the directory."},"roleId":{"type":"string","description":"Role id of the caller in this directory. Resolve via `GET /v1.0/roles`."},"licenseId":{"type":"string","description":"License id of the caller in this directory. Resolve via `GET /v1.0/licenses`."},"isBlocked":{"type":"boolean","description":"Whether the caller is blocked in this directory."}},"required":["directoryId","directoryName","roleId","licenseId","isBlocked"]},"PublicApiDirectoryDetail":{"type":"object","properties":{"directoryId":{"type":"string","description":"Unique id of the directory."},"name":{"type":"string","description":"Display name of the directory."},"pictureFileUrl":{"type":"string","description":"Absolute URL of the directory logo, or null. Requires the same Bearer token as the API.","nullable":true},"languageCode":{"type":"string","description":"Default language of the directory (IETF language tag, e.g. `de-DE`, `en-US`).","nullable":true},"maxApiRequestsPerMinute":{"type":"number","description":"Maximum number of API requests per minute allowed for this directory."},"maxApiRequestsPerDay":{"type":"number","description":"Maximum number of API requests per day allowed for this directory."}},"required":["directoryId","name","pictureFileUrl","languageCode","maxApiRequestsPerMinute","maxApiRequestsPerDay"]},"PublicApiDirectoryMembership":{"type":"object","properties":{"directoryId":{"type":"string","description":"Id of the directory this membership belongs to."},"userId":{"type":"string","description":"Id of the user this membership belongs to."},"roleId":{"type":"string","description":"Role id of the user in this directory. Resolve via `GET /v1.0/roles`."},"licenseId":{"type":"string","description":"License id of the user in this directory. Resolve via `GET /v1.0/licenses`."},"isBlocked":{"type":"boolean","description":"Whether the user is blocked in this directory."}},"required":["directoryId","userId","roleId","licenseId","isBlocked"]},"PublicApiDirectoryMembershipListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The memberships on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiDirectoryMembership"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiUser":{"type":"object","properties":{"userId":{"type":"string","description":"Id of the user."},"email":{"type":"string","description":"Email address of the user."},"firstName":{"type":"string","description":"First name."},"lastName":{"type":"string","description":"Last name."},"pictureFileUrl":{"type":"string","description":"Absolute URL of the profile picture, or null. Requires the same Bearer token as the API.","nullable":true}},"required":["userId","email","firstName","lastName","pictureFileUrl"]},"PublicApiUserListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The users on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiUser"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiSearchUsersBody":{"type":"object","properties":{"search":{"type":"string","nullable":true,"description":"Case-insensitive substring match on first/last name and email. The term may appear at the start, middle, or end."},"page":{"type":"number","description":"Page (1-based, default 1)."},"pageSize":{"type":"number","description":"Page size (default 25, max 100)."}}},"PublicApiInviteMemberBody":{"type":"object","properties":{"email":{"type":"string","description":"Email address to invite."},"roleId":{"type":"string","description":"Role id to assign. Resolve valid ids via `GET /v1.0/roles`."},"licenseId":{"type":"string","description":"License id to assign. Resolve valid ids via `GET /v1.0/licenses`."},"description":{"type":"string","description":"Optional note.","nullable":true},"languageCode":{"type":"string","description":"Optional invitation language as an IETF language tag (e.g. `de-DE`).","nullable":true}},"required":["email","roleId","licenseId"]},"PublicApiInvite":{"type":"object","properties":{"userId":{"type":"string","description":"Id of the invited user."},"inviteeEmail":{"type":"string","description":"Invited email address.","nullable":true},"roleId":{"type":"string","description":"Assigned role id."},"licenseId":{"type":"string","description":"Assigned license id."},"createdOn":{"type":"string","description":"Creation timestamp (ISO-8601)."}},"required":["userId","inviteeEmail","roleId","licenseId","createdOn"]},"PublicApiRevokeResult":{"type":"object","properties":{"userHardDeleted":{"type":"boolean","description":"True if the user had no other directories and was therefore deleted entirely (GDPR)."}},"required":["userHardDeleted"]},"PublicApiRole":{"type":"object","properties":{"roleId":{"type":"string","description":"Id of the role."},"name":{"type":"string","description":"Role name.","enum":["Owner","Admin","Member"]}},"required":["roleId","name"]},"PublicApiLicense":{"type":"object","properties":{"licenseId":{"type":"string","description":"Id of the license."},"name":{"type":"string","description":"License name (tier)."}},"required":["licenseId","name"]},"PublicApiTag":{"type":"object","properties":{"tagId":{"type":"string","description":"Id of the tag."},"name":{"type":"string","description":"Tag display name."},"colorCode":{"type":"string","description":"Colour of the tag as a key from Sally's fixed tag palette — a colour NAME, not a raw hex value. One of: `indigo`, `purple`, `fuchsia`, `pink`, `rose`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `slate`, `gray`, `zinc`, `stone`, `neutral`. The client maps the key to the actual (light/dark) colour; `null` or an unknown key renders in the neutral default colour. Null if unset.","nullable":true}},"required":["tagId","name","colorCode"]},"PublicApiRecording":{"type":"object","properties":{"recordingId":{"type":"string","description":"Id of the recording."},"name":{"type":"string","description":"Display name of the recording.","nullable":true},"durationInSeconds":{"type":"number","description":"Duration in seconds.","nullable":true},"isManualUpload":{"type":"boolean","description":"True if manually uploaded (no meeting bot)."},"isTranscriptionSucceeded":{"type":"boolean","description":"True when a usable transcript exists. This is NOT the same as the run having finished: a recording without spoken audio finishes too, and then this stays false."},"isTranscriptionCompleted":{"type":"boolean","description":"True once the transcription run has finished, whether or not it produced a transcript. Use this to tell \"still running\" apart from \"finished without a result\"; whether there is something to read is `isTranscriptionSucceeded`."},"transcriptionCompletionReason":{"type":"string","enum":["succeeded","noSpokenAudio","fileEmpty","languageNotDetected","error","quotaExceeded","unknown"],"description":"Why the transcription run ended. `unknown` while it has not finished. The underlying error text is deliberately not exposed."},"languageCode":{"type":"string","description":"Language code of the recording (IETF language tag, e.g. `de-DE`, `en-US`).","nullable":true},"createdOn":{"type":"string","description":"Creation timestamp (ISO-8601)."},"tags":{"description":"Effective tags (own + inherited from linked appointments and their recurring series master).","type":"array","items":{"$ref":"#/components/schemas/PublicApiTag"}}},"required":["recordingId","name","durationInSeconds","isManualUpload","isTranscriptionSucceeded","isTranscriptionCompleted","transcriptionCompletionReason","languageCode","createdOn","tags"]},"PublicApiRecordingListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The recordings on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiRecording"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiTranscriptSpeaker":{"type":"object","properties":{"speakerId":{"type":"string","description":"Id of the transcript speaker."},"speakerNumber":{"type":"number","description":"Speaker number."},"name":{"type":"string","description":"Name of the speaker.","nullable":true},"emailAddress":{"type":"string","description":"Email of the speaker.","nullable":true}},"required":["speakerId","speakerNumber","name","emailAddress"]},"PublicApiTranscriptSegment":{"type":"object","properties":{"speakerId":{"type":"string","description":"Speaker id of the segment."},"startTime":{"type":"number","description":"Start time in seconds."},"endTime":{"type":"number","description":"End time in seconds."},"text":{"type":"string","description":"Transcribed text of the segment.","nullable":true}},"required":["speakerId","startTime","endTime","text"]},"PublicApiTranscription":{"type":"object","properties":{"recordingId":{"type":"string","description":"Associated recording id."},"speakers":{"description":"Speakers of the transcript.","type":"array","items":{"$ref":"#/components/schemas/PublicApiTranscriptSpeaker"}},"segments":{"description":"Time-coded segments.","type":"array","items":{"$ref":"#/components/schemas/PublicApiTranscriptSegment"}}},"required":["recordingId","speakers","segments"]},"PublicApiSectionPerson":{"type":"object","properties":{"userId":{"type":"string","description":"Id 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}`.","nullable":true},"name":{"type":"string","description":"Name of the person as recognized in the meeting."},"email":{"type":"string","description":"Email address as recognized in the meeting, lower-cased. Null when the meeting named the person without an address.","nullable":true}},"required":["userId","name","email"]},"PublicApiSectionTableCell":{"type":"object","properties":{"value":{"type":"string","description":"The cell value exactly as recognized, or null for an empty cell. Always a string, never a number: a value the meeting left vague (`around 12,000, depending on quantity`) stays readable instead of being forced into a number that would state more than was said. Null in a `person` column, where `people` carries the value instead.","nullable":true},"people":{"description":"The recognized people, set only in a `person` column, where it is null for an empty cell. A cell can name several people, so this is always a list, even with a single entry.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiSectionPerson"}}},"required":["value","people"]},"PublicApiSectionTableColumn":{"type":"object","properties":{"name":{"type":"string","description":"Column heading."},"type":{"type":"string","enum":["boolean","currency","date","number","percent","person","text"],"description":"Value type of the column."},"currency":{"type":"string","description":"ISO-4217 currency code of the column, set only for `type: currency`. The currency is a property of the COLUMN, so every amount in it stays comparable.","nullable":true},"personMode":{"type":"string","enum":["linkOrText","linkedOnly","plain"],"description":"How people without a matching account are handled, set only for `type: person`.","nullable":true}},"required":["name","type","currency","personMode"]},"PublicApiSectionTable":{"type":"object","properties":{"columns":{"description":"The columns, in display order.","type":"array","items":{"$ref":"#/components/schemas/PublicApiSectionTableColumn"}},"rows":{"type":"array","items":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiSectionTableCell"}},"description":"The rows. Each row has exactly one cell per entry in `columns`, in the same order, so `rows[i][j]` belongs to `columns[j]`. A short row is padded with empty cells rather than dropped."}},"required":["columns","rows"]},"PublicApiFollowUpParticipant":{"type":"object","properties":{"userId":{"type":"string","description":"Id 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}`.","nullable":true},"name":{"type":"string","description":"Name of the person as recognized in the meeting."},"email":{"type":"string","description":"Email address as recognized in the meeting, lower-cased. Null when the meeting named the person without an address.","nullable":true},"isExternal":{"type":"boolean","description":"True when the person was recognized as not belonging to your own organization. Null when the meeting gave no indication either way.","nullable":true}},"required":["userId","name","email","isExternal"]},"PublicApiEmailRecipient":{"type":"object","properties":{"userId":{"type":"string","description":"Id 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}`.","nullable":true},"name":{"type":"string","description":"Name of the person as recognized in the meeting."},"email":{"type":"string","description":"Email address as recognized in the meeting, lower-cased. Null when the meeting named the person without an address.","nullable":true},"kind":{"type":"string","enum":["to","cc"],"description":"Whether the person is a direct recipient (`to`) or on copy (`cc`)."}},"required":["userId","name","email","kind"]},"PublicApiSectionItem":{"type":"object","properties":{"sectionItemId":{"type":"string","description":"Id of the section item."},"meetingTemplateSectionId":{"type":"string","description":"Id of the meeting template section this item was generated from. Resolve it via `GET /v1.0/directories/{directoryId}/meetingtemplates/{meetingTemplateId}` (matches a section’s `meetingTemplateSectionId`). Null for derived items without a template section (e.g. task items).","nullable":true},"title":{"type":"string","description":"Title of the template section item."},"sectionType":{"type":"string","description":"Section type of the item.","enum":["summary","tasks","topics","decisions","customList","freeText","table","appointment","email","unknown"]},"outputFormat":{"type":"string","description":"Output format of the item content.","enum":["html","markdown","unknown"]},"sortOrder":{"type":"number","description":"Order within the summary."},"content":{"type":"string","description":"Free-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.","nullable":true},"subject":{"type":"string","description":"Task subject (for task-like section items).","nullable":true},"description":{"type":"string","description":"Task description.","nullable":true},"responsibleUserName":{"type":"string","description":"Responsible person (name).","nullable":true},"responsibleUserEmail":{"type":"string","description":"Responsible person (email).","nullable":true},"dueDate":{"type":"string","description":"Due date (ISO-8601).","nullable":true},"tableVariant":{"type":"string","enum":["free","structured","unknown"],"description":"Only for `sectionType: table`. `structured` means the template fixed the columns and the result is in `tableData`. `free` means Sally chose the columns herself and the result is a Markdown table in `content`. Null for every other section type.","nullable":true},"tableData":{"description":"The table result, set only for `sectionType: table` with `tableVariant: structured`. For `tableVariant: free` this is null and the table is a Markdown table in `content`.","nullable":true,"allOf":[{"$ref":"#/components/schemas/PublicApiSectionTable"}]},"followUpMeetingStartTime":{"type":"string","description":"Start 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 for `sectionType: appointment`.","nullable":true},"followUpMeetingEndTime":{"type":"string","description":"End of the proposed follow-up meeting, same local-time format as `followUpMeetingStartTime`. Null when the meeting named a start but no end.","nullable":true},"followUpMeetingParticipants":{"description":"The people the meeting named for the follow-up. Only set for `sectionType: appointment`.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiFollowUpParticipant"}},"emailRecipients":{"description":"Recipients of the proposed email, direct and on copy (see `kind`). Only set for `sectionType: email`; its subject is in `subject` and its body in `description`.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiEmailRecipient"}}},"required":["sectionItemId","meetingTemplateSectionId","title","sectionType","outputFormat","sortOrder","content","subject","description","responsibleUserName","responsibleUserEmail","dueDate","tableVariant","tableData","followUpMeetingStartTime","followUpMeetingEndTime","followUpMeetingParticipants","emailRecipients"]},"PublicApiAgendaResult":{"type":"object","properties":{"appointmentAgendaItemId":{"type":"string","description":"Id 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."},"title":{"type":"string","description":"The title of the agenda item at the time of the evaluation, recorded alongside. If the item is renamed later, the older summary stays readable."},"agendaCoverage":{"type":"string","enum":["discussed","touched","notDiscussed"],"description":"Whether 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."},"summary":{"type":"string","description":"Two 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 for `notDiscussed`, and also null for evaluations that ran before this field existed — in that case `content` still carries the full result.","nullable":true},"content":{"type":"string","description":"What 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 for `notDiscussed`, and that is not an error: the state is carried by `agendaCoverage`.","nullable":true},"timeStamps":{"description":"Every 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.","type":"array","items":{"type":"number"}}},"required":["appointmentAgendaItemId","title","agendaCoverage","summary","content","timeStamps"]},"PublicApiSummary":{"type":"object","properties":{"recordingSummaryId":{"type":"string","description":"Id of the recording summary."},"recordingId":{"type":"string","description":"Associated recording id."},"appointmentId":{"type":"string","description":"Associated appointment id (if present).","nullable":true},"languageCode":{"type":"string","description":"Language code of the summary (IETF language tag, e.g. `de-DE`, `en-US`).","nullable":true},"summary":{"type":"string","description":"Summary text.","nullable":true},"isSummarizationCompleted":{"type":"boolean","description":"True once the summarization run has finished, whether or not it produced anything."},"isSummarizationSucceeded":{"type":"boolean","description":"True 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."},"summarizationCompletionReason":{"type":"string","enum":["succeeded","noTranscript","emptyResult","error","unknown"],"description":"Why the summarization run ended. `noTranscript` means there was nothing to summarize, which is an expected outcome and not a failure. `unknown` while it has not finished. The underlying error text is deliberately not exposed."},"createdOn":{"type":"string","description":"Creation timestamp (ISO-8601)."},"sectionItems":{"description":"The 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.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiSectionItem"}},"agendaResults":{"description":"The 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. Resolve `appointmentAgendaItemId` through `GET /v1/directories/{directoryId}/appointments/{appointmentId}/agenda`.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiAgendaResult"}},"isAgendaEvaluationOutdated":{"type":"boolean","description":"True when the agenda changed after it was evaluated, so the entries in `agendaResults` no longer match the current agenda. Without this an integrator reads an agenda block that no longer fits and does not notice."}},"required":["recordingSummaryId","recordingId","appointmentId","languageCode","summary","isSummarizationCompleted","isSummarizationSucceeded","summarizationCompletionReason","createdOn","sectionItems","agendaResults","isAgendaEvaluationOutdated"]},"PublicApiRequestUploadBody":{"type":"object","properties":{"fileName":{"type":"string","description":"Original file name (incl. extension, for MIME/extension derivation)."},"mimeType":{"type":"string","description":"MIME type of the file (whitelist: video mp4/mkv/avi/mov/webm, audio mp3/wav/flac/ogg/amr/m4a/opus/aac).","example":"audio/mpeg"},"sizeBytes":{"type":"number","description":"File size in bytes (> 0, max. 5 GB)."}},"required":["fileName","mimeType","sizeBytes"]},"PublicApiUploadTicket":{"type":"object","properties":{"uploadId":{"type":"string","description":"Upload token — passed along in the finalize step."},"uploadUrl":{"type":"string","description":"Absolute Azure SAS URL. The client uploads the file bytes via `PUT` directly to it (no API roundtrip)."},"storageMode":{"type":"string","description":"Storage mode.","example":"azure-blob"},"requiredHeaders":{"type":"object","description":"Headers that must be sent with the `PUT`.","additionalProperties":{"type":"string"},"example":{"x-ms-blob-type":"BlockBlob"}},"maxBytes":{"type":"number","description":"Maximum allowed upload size in bytes."}},"required":["uploadId","uploadUrl","storageMode","requiredHeaders","maxBytes"]},"PublicApiFinalizeUploadBody":{"type":"object","properties":{"languageCode":{"type":"string","description":"Language code of the audio (IETF language tag, e.g. `de-DE`, `en-US`). Omit or null to auto-detect.","nullable":true},"speakerCount":{"type":"number","description":"Number of speakers (for diarization).","nullable":true},"appointmentId":{"type":"string","description":"Optionally linked appointment (GUID).","nullable":true},"choice":{"type":"string","enum":["copy","replace"],"description":"Only needed when the linked appointment already has an active recording; otherwise ignored. `copy` creates a copy of the appointment for the uploading user and attaches the file there, the existing recording stays on the original appointment. `replace` attaches the file to the appointment and removes the existing recording once the new one is created. Without it, an upload to an appointment that already has a recording fails with **409** and code `RECORDING_EXISTS_CHOICE_REQUIRED`.","nullable":true}}},"PublicApiUploadedRecording":{"type":"object","properties":{"recordingId":{"type":"string","description":"Id of the created recording (transcription has been triggered)."}},"required":["recordingId"]},"PublicApiIngestFromUrlBody":{"type":"object","properties":{"url":{"type":"string","description":"Publicly reachable https URL of the audio or video file. Must point to the file itself, not to a landing page or a redirect. Presigned links (S3, Azure Blob, Google Cloud Storage) work as long as they are valid at the time of the call."},"name":{"type":"string","description":"Name of the recording. Defaults to a generated name if omitted; the URL is never used as the name, since presigned links carry credentials in their query string.","nullable":true},"languageCode":{"type":"string","description":"Language code of the audio (IETF language tag, e.g. `de-DE`, `en-US`). Omit or null to auto-detect.","nullable":true},"speakerCount":{"type":"number","description":"Number of speakers (for diarization).","nullable":true}},"required":["url"]},"PublicApiSetTagsBody":{"type":"object","properties":{"tagIds":{"description":"Existing tag ids to assign; the given set replaces the current one.","type":"array","items":{"type":"string"}}}},"PublicApiAppointmentAttendee":{"type":"object","properties":{"name":{"type":"string","description":"Name of the attendee.","nullable":true},"email":{"type":"string","description":"Email/address entry of the attendee.","nullable":true},"invitationStatus":{"type":"string","description":"Invitation status of the attendee.","enum":["declined","accepted","tentative","notResponded","organizer","unknown"]},"attendanceStatus":{"type":"string","description":"Attendance status of the attendee.","enum":["notAttended","attended","unknown"]},"isRequired":{"type":"boolean","description":"True if the attendee is a required participant, false if optional (as set in the calendar)."}},"required":["name","email","invitationStatus","attendanceStatus","isRequired"]},"PublicApiAppointment":{"type":"object","properties":{"appointmentId":{"type":"string","description":"Id of the appointment."},"subject":{"type":"string","description":"Subject of the appointment."},"description":{"type":"string","description":"Body of the appointment as delivered by the calendar. Usually HTML for appointments that come from Outlook or Google Calendar, plain text for appointments created in Sally. Returned verbatim, so render it accordingly. Null if the appointment has no body.","nullable":true},"startTime":{"type":"string","description":"Start time (ISO-8601).","nullable":true},"endTime":{"type":"string","description":"End time (ISO-8601).","nullable":true},"durationInMinutes":{"type":"number","description":"Duration in minutes.","nullable":true},"isAllDay":{"type":"boolean","description":"True if the appointment spans whole days. Appointments migrated from the old system may carry no value for this, which counts as false."},"location":{"type":"string","description":"Location of the appointment.","nullable":true},"isPrivate":{"type":"boolean","description":"True if the appointment is marked as private."},"isCanceled":{"type":"boolean","description":"True if the appointment was canceled."},"isRecurring":{"type":"boolean","description":"True if the appointment is part of a recurring series."},"icalId":{"type":"string","description":"The iCalendar UID of the appointment, as reported by the calendar it came from. Use it to match an appointment against your own copy of the same calendar entry. NOT unique within a directory: the same entry appears once per calendar it was synced from, and all of those copies share this value. Null for appointments that never came from a calendar.","nullable":true},"attendees":{"description":"Attendees.","type":"array","items":{"$ref":"#/components/schemas/PublicApiAppointmentAttendee"}},"tags":{"description":"Effective tags (own + inherited from the recurring series master).","type":"array","items":{"$ref":"#/components/schemas/PublicApiTag"}}},"required":["appointmentId","subject","description","startTime","endTime","durationInMinutes","isAllDay","location","isPrivate","isCanceled","isRecurring","icalId","attendees","tags"]},"PublicApiAppointmentListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The appointments on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiAppointment"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiSearchAppointmentsBody":{"type":"object","properties":{"search":{"type":"string","nullable":true,"description":"Case-insensitive substring match over subject, description and attendee name/email. The term may appear at the start, middle, or end of any of them; a hit in any one counts."},"from":{"type":"string","nullable":true,"description":"Only appointments with StartTime >= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`)."},"to":{"type":"string","nullable":true,"description":"Only appointments with StartTime <= this UTC ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SSZ`)."},"page":{"type":"number","description":"Page (1-based, default 1)."},"pageSize":{"type":"number","description":"Page size (default 25, max 100)."}}},"PublicApiAgendaItem":{"type":"object","properties":{"appointmentAgendaItemId":{"type":"string","description":"Id of the agenda item. The same id appears as `appointmentAgendaItemId` in the evaluated agenda of a summary, which is what makes that one resolvable."},"title":{"type":"string","description":"The title of the item, as the user wrote it.","nullable":true},"note":{"type":"string","description":"The note on the item. It feeds the evaluation prompt so the result matches what the user had in mind.","nullable":true},"sortOrder":{"type":"number","description":"Position in the agenda, ascending."}},"required":["appointmentAgendaItemId","title","note","sortOrder"]},"PublicApiSearchSummariesBody":{"type":"object","properties":{"search":{"type":"string","nullable":true,"description":"Search 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."},"page":{"type":"number","description":"Page (1-based, default 1)."},"pageSize":{"type":"number","description":"Page size (default 25, max 100)."}}},"PublicApiSummarySearchHit":{"type":"object","properties":{"recordingSummaryId":{"type":"string","description":"Id of the recording summary."},"recordingId":{"type":"string","description":"Associated recording id."},"appointmentId":{"type":"string","description":"Associated appointment id (if present).","nullable":true},"languageCode":{"type":"string","description":"Language code of the summary (IETF language tag, e.g. `de-DE`, `en-US`).","nullable":true},"snippet":{"type":"string","description":"Excerpt (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."},"createdOn":{"type":"string","description":"Creation timestamp (ISO-8601)."}},"required":["recordingSummaryId","recordingId","appointmentId","languageCode","snippet","createdOn"]},"PublicApiSummarySearchResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The matching recording summaries on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiSummarySearchHit"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiWebhook":{"type":"object","properties":{"webhookId":{"type":"string","description":"Id of the webhook."},"name":{"type":"string","description":"Display name of the webhook."},"event":{"type":"string","description":"Event on which the webhook fires.","enum":["summary.ready","manual","recording.summarized","meeting.summarized"]},"url":{"type":"string","description":"Target URL that Sally POSTs the event to."},"isActive":{"type":"boolean","description":"Whether the webhook is actively firing."},"directoryWide":{"type":"boolean","description":"Whether the webhook fires directory-wide (for recordings of ALL users, admin/owner only) or personally (only for the creator's own recordings)."},"createdOn":{"type":"string","description":"Creation timestamp (ISO-8601)."}},"required":["webhookId","name","event","url","isActive","directoryWide","createdOn"]},"PublicApiCatalogTag":{"type":"object","properties":{"tagId":{"type":"string","description":"Id of the tag."},"name":{"type":"string","description":"Tag display name."},"colorCode":{"type":"string","description":"Colour of the tag as a key from Sally's fixed tag palette — a colour NAME, not a raw hex value. One of: `indigo`, `purple`, `fuchsia`, `pink`, `rose`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `slate`, `gray`, `zinc`, `stone`, `neutral`. The client maps the key to the actual (light/dark) colour; `null` or an unknown key renders in the neutral default colour. Null if unset.","nullable":true}},"required":["tagId","name","colorCode"]},"PublicApiCatalogTagListResponse":{"type":"object","properties":{"items":{"description":"The tags in the catalog.","type":"array","items":{"$ref":"#/components/schemas/PublicApiCatalogTag"}}},"required":["items"]},"PublicApiTagColor":{"type":"object","properties":{"colorCode":{"type":"string","description":"A valid tag colour key — a colour NAME (not a hex value) that can be assigned to a tag's `colorCode` (e.g. `blue`, `red`, `green`)."}},"required":["colorCode"]},"PublicApiTagUpsertBody":{"type":"object","properties":{"name":{"type":"string","description":"Tag display name (max. 100 characters)."},"colorCode":{"type":"string","nullable":true,"description":"Colour of the tag as a key from Sally's fixed tag palette — a colour NAME, not a raw hex value. One of: `indigo`, `purple`, `fuchsia`, `pink`, `rose`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `slate`, `gray`, `zinc`, `stone`, `neutral`. An unknown key renders in the neutral default colour; `null` to clear the colour."}},"required":["name"]},"PublicApiFolder":{"type":"object","properties":{"folderId":{"type":"string","description":"Id of the folder."},"name":{"type":"string","description":"Folder display name."},"description":{"type":"string","description":"Folder description; null if unset.","nullable":true},"ownerUserId":{"type":"string","description":"Id of the user who owns the folder."},"parentFolderId":{"type":"string","description":"Id of the parent folder; null if this is a top-level folder.","nullable":true},"isOwner":{"type":"boolean","description":"True if the caller is the owner of the folder."},"isFavorite":{"type":"boolean","description":"True if the caller marked the folder as favorite."},"accessLevel":{"type":"string","enum":["none","read","readWrite"],"description":"The caller's effective access level on the folder."},"recordingsCount":{"type":"number","description":"Number of recordings in the folder."},"createdOn":{"type":"string","description":"Creation timestamp (ISO 8601)."},"modifiedOn":{"type":"string","description":"Last modification timestamp (ISO 8601)."}},"required":["folderId","name","description","ownerUserId","parentFolderId","isOwner","isFavorite","accessLevel","recordingsCount","createdOn","modifiedOn"]},"PublicApiFolderListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The folders on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolder"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiSearchFoldersBody":{"type":"object","properties":{"search":{"type":"string","nullable":true,"description":"Case-insensitive substring match on the folder name. The term may appear at the start, middle, or end."},"parentRecordingFolderId":{"type":"string","nullable":true,"description":"List only the direct children of this folder; omit for all folders."},"page":{"type":"number","description":"Page (1-based, default 1)."},"pageSize":{"type":"number","description":"Page size (default 25, max 100)."}}},"PublicApiUpdateFolderBody":{"type":"object","properties":{"name":{"type":"string","description":"New folder name (omit to leave unchanged)."},"description":{"type":"string","nullable":true,"description":"New folder description; null to clear, omit to leave unchanged."},"parentFolderId":{"type":"string","nullable":true,"description":"Id of the new parent folder to move this folder under; null to make it a top-level folder, omit to leave unchanged."}}},"PublicApiFolderUserShare":{"type":"object","properties":{"userId":{"type":"string","description":"Id of the user this folder is shared with."},"accessLevel":{"type":"string","enum":["none","read","readWrite"],"description":"Access level granted to the user."}},"required":["userId","accessLevel"]},"PublicApiFolderTeamShare":{"type":"object","properties":{"teamId":{"type":"string","description":"Id of the team this folder is shared with."},"accessLevel":{"type":"string","enum":["none","read","readWrite"],"description":"Access level granted to the team."}},"required":["teamId","accessLevel"]},"PublicApiFolderInheritedShare":{"type":"object","properties":{"userId":{"type":"string","description":"Id of the user who inherits access."},"accessLevel":{"type":"string","enum":["none","read","readWrite"],"description":"Inherited access level."},"inheritedFromFolderId":{"type":"string","description":"Id of the parent folder the access is inherited from."}},"required":["userId","accessLevel","inheritedFromFolderId"]},"PublicApiFolderShares":{"type":"object","properties":{"userShares":{"description":"Direct user shares of the folder.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderUserShare"}},"teamShares":{"description":"Team shares of the folder.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderTeamShare"}},"inheritedShares":{"description":"Shares inherited from parent folders (read-only).","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderInheritedShare"}}},"required":["userShares","teamShares","inheritedShares"]},"PublicApiSetFolderSharesBody":{"type":"object","properties":{"userShares":{"description":"Direct user shares to apply. The resulting set replaces the current one.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderUserShare"}},"teamShares":{"description":"Team shares to apply. The resulting set replaces the current one.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderTeamShare"}}},"required":["userShares","teamShares"]},"PublicApiFolderRecording":{"type":"object","properties":{"recordingId":{"type":"string","description":"Id of the recording."},"durationInSeconds":{"type":"number","description":"Duration in seconds; null if unknown.","nullable":true},"isManualUpload":{"type":"boolean","description":"True if manually uploaded (no meeting bot)."},"appointmentId":{"type":"string","description":"Id of the caller's own appointment linked to the recording; null if none.","nullable":true},"createdOn":{"type":"string","description":"Creation timestamp (ISO 8601, UTC)."}},"required":["recordingId","durationInSeconds","isManualUpload","appointmentId","createdOn"]},"PublicApiFolderRecordingListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The recordings on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiFolderRecording"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiTask":{"type":"object","properties":{"taskId":{"type":"string","description":"Id of the task."},"subject":{"type":"string","description":"Task subject."},"description":{"type":"string","nullable":true,"description":"Task description; null if unset."},"ownerId":{"type":"string","description":"Id of the user who owns (created) the task."},"responsibleUserId":{"type":"string","nullable":true,"description":"Id of the user responsible for the task; null if unassigned."},"taskCategoryId":{"type":"string","nullable":true,"description":"Id of the task category; null if the task has none. Resolve via `GET /v1.0/directories/{directoryId}/task-categories`."},"dueDate":{"type":"string","nullable":true,"description":"Due date (ISO 8601); null if no due date is set."},"isDone":{"type":"boolean","description":"Whether the task is marked as done."},"doneOn":{"type":"string","nullable":true,"description":"Timestamp when the task was completed (ISO 8601); null if not done."},"isCanceled":{"type":"boolean","description":"Whether the task is canceled."},"canceledOn":{"type":"string","nullable":true,"description":"Timestamp when the task was canceled (ISO 8601); null if not canceled."},"recordingSummaryId":{"type":"string","nullable":true,"description":"Id of the recording summary this task originated from; null if none."},"appointmentId":{"type":"string","nullable":true,"description":"Id of the appointment this task is linked to; null if none."},"createdOn":{"type":"string","description":"Creation timestamp (ISO 8601)."},"modifiedOn":{"type":"string","description":"Last modification timestamp (ISO 8601)."}},"required":["taskId","subject","description","ownerId","responsibleUserId","taskCategoryId","dueDate","isDone","doneOn","isCanceled","canceledOn","recordingSummaryId","appointmentId","createdOn","modifiedOn"]},"PublicApiTaskListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The tasks on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiTask"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiSearchTasksBody":{"type":"object","properties":{"search":{"type":"string","nullable":true,"description":"Case-insensitive substring match on the task subject AND description. The term may appear at the start, middle, or end."},"responsible":{"type":"string","nullable":true,"description":"Filter by responsible user id. Your own id is always allowed; another user or omitting it (= all responsibles) requires admin/owner."},"status":{"type":"string","nullable":true,"enum":["open","completed","overdue","canceled"],"description":"Filter by status: `open` (not done/canceled), `completed`, `overdue` (open + past due), `canceled`. Omit for any status."},"page":{"type":"number","description":"Page (1-based, default 1)."},"pageSize":{"type":"number","description":"Page size (default 25, max 100)."}}},"PublicApiCreateTaskBody":{"type":"object","properties":{"subject":{"type":"string","description":"Task subject (1..1000 characters)."},"description":{"type":"string","nullable":true,"description":"Task description (max. 4000 characters); null for none."},"responsibleUserId":{"type":"string","nullable":true,"description":"Id of the responsible user; null for unassigned."},"taskCategoryId":{"type":"string","nullable":true,"description":"Id of the task category; null for none. Pick one from `GET /v1.0/directories/{directoryId}/task-categories`; an id from another directory is rejected."},"dueDate":{"type":"string","nullable":true,"description":"Due date as an ISO 8601 string; null for no due date."}},"required":["subject"]},"PublicApiUpdateTaskBody":{"type":"object","properties":{"subject":{"type":"string","description":"New task subject (1..1000 characters)."},"description":{"type":"string","nullable":true,"description":"New task description (max. 4000 characters); null to clear."},"responsibleUserId":{"type":"string","nullable":true,"description":"New id of the responsible user; null to unassign."},"taskCategoryId":{"type":"string","nullable":true,"description":"New id of the task category; null to clear. Pick one from `GET /v1.0/directories/{directoryId}/task-categories`; an id from another directory is rejected."},"dueDate":{"type":"string","nullable":true,"description":"New due date as an ISO 8601 string; null to clear."}}},"PublicApiToggleDoneBody":{"type":"object","properties":{"isDone":{"type":"boolean","description":"True to mark the task as done, false to reopen it."}},"required":["isDone"]},"PublicApiTaskCategory":{"type":"object","properties":{"taskCategoryId":{"type":"string","description":"Id of the task category."},"name":{"type":"string","description":"Category name."},"description":{"type":"string","nullable":true,"description":"What the category covers. This text is what Sally is given when it picks a category for a recognized task. Null if unset."},"active":{"type":"boolean","description":"Whether the category is still offered for new tasks. A deactivated category stays readable so existing tasks keep resolving."},"isGlobal":{"type":"boolean","description":"True for a global Sally default, false for a category the directory owns."}},"required":["taskCategoryId","name","description","active","isGlobal"]},"PublicApiTaskCategoryListResponse":{"type":"object","properties":{"items":{"description":"The task categories of the directory.","type":"array","items":{"$ref":"#/components/schemas/PublicApiTaskCategory"}}},"required":["items"]},"PublicApiVocabulary":{"type":"object","properties":{"vocabularyId":{"type":"string","description":"Id of the vocabulary entry."},"phrase":{"type":"string","description":"The term/phrase Sally should recognize preferentially."},"manualReplacePhrases":{"type":"string","nullable":true,"description":"Alternative spellings/misrecognitions that get replaced with `phrase` in the transcript (comma-separated); null if none."},"description":{"type":"string","nullable":true,"description":"Free-text note describing the entry (not used for recognition); null if unset."},"languageCode":{"type":"string","nullable":true,"description":"Language the entry applies to, as an IETF language tag (e.g. `de-DE`, `en-US`); null = applies to all languages."},"isDirectoryWide":{"type":"boolean","description":"Scope: `true` = directory-wide (recognized for every member of the organization), `false` = personal (only the caller's own recordings)."},"createdOn":{"type":"string","description":"When the entry was created (UTC ISO-8601)."},"modifiedOn":{"type":"string","description":"When the entry was last changed (UTC ISO-8601)."}},"required":["vocabularyId","phrase","manualReplacePhrases","description","languageCode","isDirectoryWide","createdOn","modifiedOn"]},"PublicApiUpsertVocabularyBody":{"type":"object","properties":{"phrase":{"type":"string","description":"The term/phrase Sally should recognize preferentially (max. 400 characters)."},"manualReplacePhrases":{"type":"string","nullable":true,"description":"Alternative spellings/misrecognitions to replace with `phrase` (comma-separated, max. 4000 characters); null to clear."},"description":{"type":"string","nullable":true,"description":"Free-text note describing the entry, not used for recognition (max. 4000 characters); null to clear."},"languageCode":{"type":"string","nullable":true,"description":"Language the entry applies to, as an IETF language tag (e.g. `de-DE`, `en-US`); null/omitted = applies to all languages."},"isDirectoryWide":{"type":"boolean","description":"Scope (default `false`): `true` = directory-wide (recognized for every member), `false` = personal (only the caller's own recordings). Directory-wide entries require admin/owner rights; personal entries are allowed for any member."}},"required":["phrase"]},"PublicApiTeam":{"type":"object","properties":{"teamId":{"type":"string","description":"Id of the team."},"name":{"type":"string","description":"Team name."},"code":{"type":"string","description":"Optional team code.","nullable":true},"isScimGroup":{"type":"boolean","description":"Whether the team is synced from a SCIM identity provider."},"memberCount":{"type":"number","description":"Number of active members."}},"required":["teamId","name","code","isScimGroup","memberCount"]},"PublicApiCreateTeamBody":{"type":"object","properties":{"name":{"type":"string","description":"Team name."},"code":{"type":"string","description":"Optional team code.","nullable":true}},"required":["name"]},"PublicApiUpdateTeamBody":{"type":"object","properties":{"name":{"type":"string","description":"New team name."},"code":{"type":"string","description":"New team code; null to clear, omit to leave unchanged.","nullable":true}},"required":["name"]},"PublicApiTeamMembership":{"type":"object","properties":{"teamId":{"type":"string","description":"Id of the team."},"userId":{"type":"string","description":"Id of the member user."},"isMainTeam":{"type":"boolean","description":"Whether this team is the user’s main team."}},"required":["teamId","userId","isMainTeam"]},"PublicApiSetTeamMembersBody":{"type":"object","properties":{"userIds":{"description":"Full replace-set of member user ids. Users not in the list are removed.","type":"array","items":{"type":"string"}}},"required":["userIds"]},"PublicApiMeetingTemplate":{"type":"object","properties":{"meetingTemplateId":{"type":"string","description":"Id of the meeting template."},"title":{"type":"string","description":"Display title of the template."},"description":{"type":"string","description":"Description of the template.","nullable":true}},"required":["meetingTemplateId","title","description"]},"PublicApiMeetingTemplateListResponse":{"type":"object","properties":{"page":{"type":"number","description":"Current page (1-based).","example":1},"pageSize":{"type":"number","description":"Number of items per page.","example":25},"total":{"type":"number","description":"Total number of items across all pages.","example":137},"hasMore":{"type":"boolean","description":"True if further pages exist.","example":true},"items":{"description":"The templates on this page.","type":"array","items":{"$ref":"#/components/schemas/PublicApiMeetingTemplate"}}},"required":["page","pageSize","total","hasMore","items"]},"PublicApiMeetingTemplateSectionTableColumn":{"type":"object","properties":{"name":{"type":"string","description":"Column heading."},"type":{"type":"string","enum":["boolean","currency","date","number","percent","person","text"],"description":"Value type of the column."},"currency":{"type":"string","description":"ISO-4217 currency code, set only for `type: currency`.","nullable":true},"personMode":{"type":"string","enum":["linkOrText","linkedOnly","plain"],"description":"How people without a matching account are handled, set only for `type: person`. `linkOrText` keeps them by name, `linkedOnly` drops them, `plain` never links.","nullable":true}},"required":["name","type","currency","personMode"]},"PublicApiMeetingTemplateSection":{"type":"object","properties":{"meetingTemplateSectionId":{"type":"string","description":"Id of the meeting template section. This is the same id referenced by a summary section item (`meetingTemplateSectionId`)."},"title":{"type":"string","description":"Title of the section."},"description":{"type":"string","description":"User-facing description of the section.","nullable":true},"sectionType":{"type":"string","enum":["summary","tasks","topics","decisions","customList","freeText","table","appointment","email","unknown"],"description":"Type of the section."},"outputFormat":{"type":"string","enum":["html","markdown","unknown"],"description":"Output format of the generated section content."},"detailLevel":{"type":"string","enum":["short","medium","long","unknown"],"description":"Desired verbosity of the AI section."},"sortOrder":{"type":"number","description":"Sort order within the template (ascending)."},"additionalInstructions":{"type":"string","description":"Additional AI instructions (prompt) for this section.","nullable":true},"isTimestampInclusionEnabled":{"type":"boolean","description":"Whether timestamps are included in the section output."},"isResponsibleDetectionEnabled":{"type":"boolean","description":"Whether responsible-person detection is enabled."},"isDueDateInferenceEnabled":{"type":"boolean","description":"Whether due-date inference is enabled."},"smallTalk":{"type":"string","enum":["inherit","include","exclude"],"description":"Whether small talk may appear in THIS section. `inherit` follows the template, whose own setting is `isSmallTalkInclusionEnabled` on the template detail; `include` keeps it even when the template leaves it out, `exclude` drops it even when the template keeps it."},"privateTopics":{"type":"string","enum":["inherit","include","exclude"],"description":"Whether private or personal topics may appear in THIS section. Same three values as `smallTalk`, and independent of it."},"tableVariant":{"type":"string","enum":["free","structured","unknown"],"description":"Only for `sectionType: table`. `structured` means the columns are fixed in `tableColumns` and a summary carries its result as data; `free` lets Sally choose the columns per meeting and the result is a Markdown table. Null for every other section type.","nullable":true},"tableColumns":{"description":"The columns this section asks for, set only for `tableVariant: structured`.","nullable":true,"type":"array","items":{"$ref":"#/components/schemas/PublicApiMeetingTemplateSectionTableColumn"}}},"required":["meetingTemplateSectionId","title","description","sectionType","outputFormat","detailLevel","sortOrder","additionalInstructions","isTimestampInclusionEnabled","isResponsibleDetectionEnabled","isDueDateInferenceEnabled","smallTalk","privateTopics","tableVariant","tableColumns"]},"PublicApiMeetingTemplateDetail":{"type":"object","properties":{"meetingTemplateId":{"type":"string","description":"Id of the meeting template."},"title":{"type":"string","description":"Display title of the template."},"description":{"type":"string","description":"Description of the template.","nullable":true},"scope":{"type":"string","enum":["global","directory","team","personal"],"description":"Visibility scope. `global` = Sally standard template; otherwise tenant-owned."},"isSmallTalkInclusionEnabled":{"type":"boolean","description":"Whether small talk may appear in the summaries this template produces. A section with `smallTalk: inherit` follows this value."},"isPrivateTopicsInclusionEnabled":{"type":"boolean","description":"Whether private or personal topics may appear in the summaries this template produces. A section with `privateTopics: inherit` follows this value."},"sections":{"description":"The published sections of the template in sort order.","type":"array","items":{"$ref":"#/components/schemas/PublicApiMeetingTemplateSection"}}},"required":["meetingTemplateId","title","description","scope","isSmallTalkInclusionEnabled","isPrivateTopicsInclusionEnabled","sections"]},"PublicApiError":{"type":"object","description":"Uniform error body returned by all error responses.","properties":{"statusCode":{"type":"integer","example":400},"message":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","nullable":true,"description":"Machine-readable error code for specific errors (e.g. `FUP_LIMIT_EXCEEDED`, `TOO_MANY_CONCURRENT_TRANSCRIPTIONS`). Absent for generic errors."}},"required":["statusCode","message"]}}}}