Errors
Errors share one shape, an HTTP status plus a stable error code. Branch on the code, show the message to people, and retry only 429 and 500.
On this page
Error shape
Every error, from every endpoint, has the same body:
{
"error": {
"code": "read_only_key",
"message": "This API key is read only. Create a read and write key to make changes."
}
}| Field | Type | Meaning |
|---|---|---|
error.code |
string | A stable, machine-readable code. Branch on this. |
error.message |
string | A sentence for people. It may change at any time, so don’t parse it. |
The HTTP status says what kind of error it is. New codes may be added within v1, so treat an unknown code by its status.
Creating a key happens in the app, not through the API, so it has no error code here. The app refuses to create a key when the workspace isn’t on the Business plan, has 25 active keys, or its subscription has ended.
All statuses and codes
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_json |
The body isn’t valid JSON, or is JSON but not an object. | No |
| 401 | missing_key |
No Authorization header, or it isn’t Bearer <key>. |
No |
| 401 | invalid_key |
The key is malformed (not sc_live_ plus 64 hex characters) or doesn’t exist. |
No |
| 401 | revoked_key |
The key was revoked. | No |
| 401 | creator_not_admin |
The person who created the key was removed from the workspace or is no longer an owner or admin. | No |
| 401 | key_in_url |
A key appeared in the query string. Revoke it. | No |
| 403 | plan_required |
The workspace isn’t on the Business plan. | No |
| 403 | read_only_key |
A read only key called a POST endpoint. |
No |
| 403 | workspace_read_only |
The workspace’s subscription has ended, so writes are refused. Reads still work. | No |
| 403 | library_read_only |
POST /sops into a library shared in from another workspace. |
No |
| 404 | not_found |
No such SOP or library in this workspace. Something in another workspace is always 404, never 403. |
No |
| 404 | unknown_endpoint |
No such endpoint under /api/v1. Check the path. |
No |
| 405 | method_not_allowed |
The path exists but not with this method. The Allow header lists the methods it takes. |
No |
| 415 | unsupported_media_type |
A POST without Content-Type: application/json. |
No |
| 422 | invalid_parameter |
A parameter or body field has the wrong type or format, or is out of range. The message names it. | No |
| 422 | invalid_cursor |
The cursor isn’t one the API issued. |
No |
| 422 | invalid_query |
q on GET /search is empty or longer than 200 characters. |
No |
| 422 | invalid_status |
status on GET /assignments isn’t open, overdue or completed. |
No |
| 422 | invalid_title |
title on POST /sops is missing, blank or longer than 200 characters. |
No |
| 422 | invalid_description |
description on POST /sops is longer than 2,000 characters. |
No |
| 422 | invalid_body |
POST /sops needs exactly one of markdown or text, as a non-empty string. |
No |
| 422 | invalid_note |
note on POST /assignments isn’t a string, or is longer than 1,000 characters. |
No |
| 422 | too_large |
The new SOP is over 500,000 characters, over 50 sections, or over about 1 MB once converted. | No |
| 422 | not_a_member |
Nobody in the workspace signs in with that email. Invite them in the app first. | No |
| 422 | already_assigned |
The person already has this SOP assigned and not yet completed. | No, it already exists |
| 422 | sop_archived |
The SOP is archived. Restore it in the app before assigning it. | No |
| 429 | rate_limited |
Over 60 requests this minute for this key. | Yes, after Retry-After |
| 500 | internal_error |
Something failed on our side. | Yes, with backoff |
Example bodies
401 missing_key
{
"error": {
"code": "missing_key",
"message": "Send your API key as: Authorization: Bearer sc_live_..."
}
}401 creator_not_admin
{
"error": {
"code": "creator_not_admin",
"message": "The person who created this API key is no longer an admin of this workspace, so the key stopped working. An admin can create a new one."
}
}Promote the creator back to admin, or have a current admin create a replacement key. See Keys follow their creator.
401 key_in_url
{
"error": {
"code": "key_in_url",
"message": "API keys go in the Authorization header, never in the URL. Revoke this key in the app, since URLs are logged."
}
}403 plan_required
{
"error": {
"code": "plan_required",
"message": "The API is part of the Business plan. A workspace owner can switch plans in People, Settings & billing."
}
}404 not_found
{
"error": {
"code": "not_found",
"message": "No SOP with that id in this workspace."
}
}405 method_not_allowed
HTTP/2 405
allow: GET, POST{
"error": {
"code": "method_not_allowed",
"message": "Use GET or POST here."
}
}422 invalid_parameter
{
"error": {
"code": "invalid_parameter",
"message": "limit must be a whole number from 1 to 100."
}
}422 already_assigned
{
"error": {
"code": "already_assigned",
"message": "That person already has this SOP assigned and not yet completed."
}
}429 rate_limited
{
"error": {
"code": "rate_limited",
"message": "Too many requests. The limit is 60 a minute per key."
}
}500 internal_error
{
"error": {
"code": "internal_error",
"message": "Something went wrong on our side. Try again."
}
}The order checks run in
A request is checked in this order, and the first failure is the one you get:
- The path and method (
404 unknown_endpoint,405 method_not_allowed). - A key in the URL, then the
Authorizationheader’s form (401 key_in_url,401 missing_key,401 invalid_key). - For
POST, the content type and JSON body (415,400). - Parameters and body fields (
422). - The key itself: exists, not revoked, creator still an admin (
401). - The rate limit (
429). - Plan, then for writes the key’s scope and the workspace’s billing (
403). - The request itself: what it names exists and can be used (
404,403 library_read_only,422).
Because step 4 runs before step 5, a request with a bad parameter answers 422 even when the key is also invalid. Only steps 6 onwards count against the rate limit.
Handling errors well
- Branch on
error.code, not onmessageand not only on the status. - Retry only
429and500. Every other error is the same on retry. See backoff code. POSTretries are safe to reason about. A429or a4xxchanged nothing.POST /assignmentsrefuses a duplicate with422 already_assigned, so a retried assignment never assigns twice; treat that code as “already done”.POST /sopshas no such guard: if a request timed out without a response, check withGET /sops?updated_since=before sending it again.- Alert on
401and403. They mean a person must act: a revoked key, a creator who lost admin, or a plan change. Retrying won’t fix them. - Log
X-SOPCompass-API-Versionalongside errors so you can tell which API version answered.