How errors are reported
Every error of the Sally API uses the same JSON body, so one error handler is enough. This page shows that body, what each status code means for your integration, every machine-readable code, an example body per status and a handler you can copy.
The examples work as they stand once SALLY_TOKEN and DIRECTORY_ID are set, see the Quickstart.
Quick navigation
- The error body
- The status codes
- The error codes
- Example bodies
- 403 and 404 mean different things
- Handle errors in code
1. The error body
{
"statusCode": 429,
"message": "<human-readable text>",
"code": "FUP_LIMIT_EXCEEDED"
}
| Field | Type | Always present | Meaning |
|---|---|---|---|
statusCode | integer | yes | The HTTP status code, repeated in the body. |
message | string | yes | A human-readable description. Show or log it, but do not branch on it. |
code | string | no | A stable, machine-readable identifier for specific errors. |
code is a stable, machine-readable identifier. It is only present for errors that deserve their own reaction, such as FUP_LIMIT_EXCEEDED or TOO_MANY_CONCURRENT_TRANSCRIPTIONS. Generic errors leave it out, so branch on code when it is there and on statusCode otherwise.
The text of message is meant for people and can change, and it may be German. Live responses may also carry an error field with the HTTP reason phrase, such as Unauthorized. It is not part of the documented body, so do not rely on it.
2. The status codes
| Code | Meaning | What to do | Retry? |
|---|---|---|---|
400 | The request is invalid: body, query or path failed validation. | Fix the request, for example a misspelled name in fields. | No |
401 | The bearer token is missing or no longer valid. | Check the Authorization header or create a new token. | No |
403 | You can see the resource, but the operation needs a higher role, for example admin or owner, or the company account has disabled it, for example a download. Check code here. | Use a token of a user with the required role, or have the setting of the company account changed. | No |
404 | Not found or not visible to you. | Check the id and whether the token user has access. | No |
409 | Conflict with the current state, for example inviting someone who is already a member, or requesting a transcript file before the transcript exists. Check code here. | Treat it as already done, or read the current state. | No |
429 | Rate limit exceeded, for uploads also the fair-usage limit. Check code here. | See Rate limits & fair usage. | Depends on code |
500 | Internal server error. Retrying later is reasonable. | Retry later with a growing pause. | Yes |
3. The error codes
These are all values code can take. Branch on them, they do not change.
code | Status | Where | Meaning and what to do |
|---|---|---|---|
UNKNOWN_DOCUMENT_FORMAT | 400 | Summary and transcript as a file | format is missing or not supported by this endpoint. The body lists the allowed values. |
UNKNOWN_LANGUAGE_CODE | 400 | Summary and transcript as a file | languageCode is unknown. The body lists the allowed values. |
SUMMARY_DOWNLOAD_PREVENTED | 403 | Summary as a file | The company account has disabled downloading summaries for its members. A retry gives the same answer. |
TRANSCRIPT_DOWNLOAD_PREVENTED | 403 | Transcript as a file | The company account has disabled downloading transcripts for its members. A retry gives the same answer. |
RECORDING_EXISTS_CHOICE_REQUIRED | 409 | Finalizing an upload | The linked appointment already has a recording. Repeat the call with choice set to copy or replace, the uploaded file is kept. |
SUMMARY_HAS_NO_CONTENT | 409 | Summary as a file | The summary has no content for a document, because it was generated before meeting templates existed or its template has no sections. |
TRANSCRIPT_NOT_AVAILABLE | 409 | Transcript as a file | The recording has no transcript yet. Check isTranscriptionSucceeded on the recording first. |
FUP_LIMIT_EXCEEDED | 429 | Uploads | The fair-usage limit is used up. Stop uploading, retrying soon gives the same answer. See Rate limits & fair usage. |
TOO_MANY_CONCURRENT_TRANSCRIPTIONS | 429 | Uploads | Too many transcriptions of the company account are running at once. Wait until some have finished. |
4. Example bodies
These are the bodies you can expect per status. Where the example shows <human-readable text>, the real message varies with the cause.
400, for example an unknown field name:
{
"statusCode": 400,
"message": "<human-readable text>"
}
401, request without a token and request with an invalid token. These two are real answers of the API:
{
"message": "`Authorization: Bearer <token>` ist erforderlich.",
"error": "Unauthorized",
"statusCode": 401
}
{
"message": "Ungültiger oder abgelaufener API-Key.",
"error": "Unauthorized",
"statusCode": 401
}
403, the operation needs a higher role:
{
"statusCode": 403,
"message": "<human-readable text>"
}
403 with a code, for example when the company account has disabled downloading summaries:
{
"statusCode": 403,
"message": "<human-readable text>",
"code": "SUMMARY_DOWNLOAD_PREVENTED"
}
404, not found or not visible:
{
"statusCode": 404,
"message": "<human-readable text>"
}
409, for example the invited person is already a member:
{
"statusCode": 409,
"message": "<human-readable text>"
}
429 comes in three variants. The plain rate limit has no code, the two fair-usage cases do:
{
"statusCode": 429,
"message": "<human-readable text>"
}
{
"statusCode": 429,
"message": "<human-readable text>",
"code": "FUP_LIMIT_EXCEEDED"
}
{
"statusCode": 429,
"message": "<human-readable text>",
"code": "TOO_MANY_CONCURRENT_TRANSCRIPTIONS"
}
500, internal server error:
{
"statusCode": 500,
"message": "<human-readable text>"
}
5. 403 and 404 mean different things
A 403 tells you the resource exists and your role is too low or the company account has disabled the operation, which a person can fix. A 404 deliberately tells you nothing: not found and not visible cannot be told apart, so a response never reveals that data exists. Authentication explains why a token sees what it sees.
| 403 Forbidden | 404 Not Found | |
|---|---|---|
| Does the resource exist? | Yes | Unknown, on purpose |
| Can you see it? | Yes | Not with this token |
| Who can fix it? | An admin or owner, by giving the user a higher role or changing the setting of the company account | Nobody through the API. Check the id and the token user's access. |
6. Handle errors in code
Read the body on every non-2xx answer, then decide by code first and statusCode second:
- cURL
- JavaScript
- Python
# Body and status separately, then decide on code and statusCode
response=$(curl -s -w '\n%{http_code}' \
"https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID" \
-H "Authorization: Bearer $SALLY_TOKEN")
status=$(echo "$response" | tail -n 1)
body=$(echo "$response" | sed '$d')
if [ "$status" -ge 400 ]; then
echo "$body" | jq -r '"\(.statusCode) \(.code // "-") \(.message)"'
fi
class SallyApiError extends Error {
constructor(status, body) {
super(body?.message ?? `HTTP ${status}`);
this.statusCode = body?.statusCode ?? status;
this.code = body?.code ?? null;
}
}
async function sally(path, init = {}) {
const response = await fetch(`https://api.sally.io${path}`, {
...init,
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`, ...init.headers},
});
if (response.ok) return response.status === 204 ? null : response.json();
const body = await response.json().catch(() => null);
throw new SallyApiError(response.status, body);
}
try {
const recording = await sally(`/v1.0/directories/${directoryId}/recordings/${recordingId}`);
} catch (error) {
if (!(error instanceof SallyApiError)) throw error;
if (error.code === 'FUP_LIMIT_EXCEEDED') {
// quota used up: stop uploading, retrying soon gives the same answer
} else if (error.code === 'TOO_MANY_CONCURRENT_TRANSCRIPTIONS') {
// wait until running transcriptions have finished
} else if (error.statusCode === 401) {
// token missing or invalid
} else if (error.statusCode === 404) {
// not found or not visible
} else if (error.statusCode === 429 || error.statusCode >= 500) {
// retry later with a growing pause
} else {
throw error; // 400, 403, 409: fix the request
}
}
import os
import requests
class SallyApiError(Exception):
def __init__(self, status, body):
body = body or {}
super().__init__(body.get("message", f"HTTP {status}"))
self.status_code = body.get("statusCode", status)
self.code = body.get("code")
def sally(method, path, **kwargs):
response = requests.request(
method,
f"https://api.sally.io{path}",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
**kwargs,
)
if response.ok:
return response.json() if response.status_code != 204 else None
try:
body = response.json()
except ValueError:
body = None
raise SallyApiError(response.status_code, body)
try:
recording = sally("GET", f"/v1.0/directories/{directory_id}/recordings/{recording_id}")
except SallyApiError as error:
if error.code == "FUP_LIMIT_EXCEEDED":
pass # quota used up: stop uploading, retrying soon gives the same answer
elif error.code == "TOO_MANY_CONCURRENT_TRANSCRIPTIONS":
pass # wait until running transcriptions have finished
elif error.status_code == 401:
pass # token missing or invalid
elif error.status_code == 404:
pass # not found or not visible
elif error.status_code == 429 or error.status_code >= 500:
pass # retry later with a growing pause
else:
raise # 400, 403, 409: fix the request
Log statusCode, code and message together with the method and path of the request. That is usually enough to find the cause without reproducing the call.