Errors

Error responses come in two JSON shapes:

  • Errors the endpoint builds itself carry error and message, and errors for field validation failures.
  • Errors raised by the framework, or rejected by an endpoint as a whole, carry title, message, status and code.

message is in every body, so read it first. Always send Accept: application/json: the error shape is JSON either way, but without the header the errors in the title shape are returned as an HTML page instead of JSON (and on incoming webhooks an invalid payload turns into a redirect).

401 Unauthorized

{
  "error": "unauthorized",
  "message": "Invalid or inactive API key."
}

Causes: unknown or malformed token, expired token, revoked token. A request with no bearer token gets the same shape with the message Missing API key. Use Authorization: Bearer <your-api-key>.

403 Forbidden

{
  "error": "forbidden",
  "message": "This token does not have the required scope: write:posts"
}

Causes, each with its own message:

  • a scope the token does not have: This token does not have the required scope: <scope>
  • the token's IP restriction: Request IP is not allowed for this token.
  • a feature that is switched off for the company: The <feature> feature is not enabled. (see feature-gated endpoints)
  • attaching media without the write:media scope: Attaching media requires the write:media scope.
  • reading or writing an article that belongs to other content without the owner's scope: This article belongs to <owner>; reading it requires the read:<noun> or write:<noun> scope., or for a write This article belongs to <owner>; writing it requires the write:<noun> scope.
  • creating an incoming webhook, or changing its action or config, without the action's write scope: Webhooks with the <action> action require the <scope> scope.
  • a bulk save that would add, delete or reorder the elements of a branded article (see articles): the branded-content message below

A 403 raised by an endpoint itself, for example when deleting an admin through DELETE /users/{id}, uses the title, message, status and code shape instead:

{
  "title": "Not permitted.",
  "message": "Admins cannot be changed through the API.",
  "status": 403,
  "code": 0
}

Editing branded content, a handbook or course imported from a content provider (see handbooks and courses), is refused in the same shape with code 403. The message names the provider, and says course for a course and its chapters:

{
  "title": "Not permitted.",
  "message": "This handbook comes from Sideways and is kept up to date automatically, so it can’t be edited here.",
  "status": 403,
  "code": 403
}

A delete or update that other records still point at, for example deleting content that users have already interacted with, is refused with the same shape, but with code 23000 (the database's integrity constraint code) instead of 0:

{
  "status": 403,
  "code": 23000,
  "title": "Not permitted.",
  "message": "Existing references do not permit this. Users may have already interacted with the content."
}

404 Not Found

{
  "title": "Entity does not exist.",
  "message": "Not Found",
  "status": 404,
  "code": 0
}

The resource ID does not exist or the token does not have access to it. An unknown path gets the same answer. The JSON body is sent when the request has Accept: application/json.

405 Method Not Allowed

The path exists but not with this HTTP method, for example PATCH /users/{id}. The body uses the title, message, status and code shape, with title Not permitted..

409 Conflict

{
  "error": "conflict",
  "message": "This chapter is referenced by one or more courses and cannot be deleted."
}

The request clashes with the current state of the resource. Only DELETE /handbook-chapters/{id} answers 409, for a chapter whose article a course chapter shares.

422 Validation Error

{
  "error": "validation_error",
  "message": "The given data was invalid.",
  "errors": {
    "body": ["The body field is required."]
  }
}

This shape is used when one or more fields are invalid. The errors object maps field names to arrays of human-readable messages. Use them to build per-field validation feedback in your integration's UI.

When the request is rejected as a whole, for example when deleting a user who is synced from an external system, there is no errors object:

{
  "title": "Validation failed.",
  "message": "Only manually created users can be deleted — this user is synced from an external system.",
  "status": 422,
  "code": 0
}

An incoming webhook whose action type is no longer supported answers 422 with its own error key and no errors object:

{
  "error": "invalid_action",
  "message": "Unknown webhook action type."
}

Read errors when it is present, and message otherwise.

429 Too Many Requests

{
  "title": "ThrottleRequestsException",
  "message": "Too Many Attempts.",
  "status": 429,
  "code": 0
}

The response carries a Retry-After header. See Rate limiting for backoff guidance.

5xx

Internal server errors are returned as 500 with a generic message and no further detail. They are tracked server-side. Retry with exponential backoff — most are transient.

{
  "title": "Error",
  "message": "An unexpected error occurred.",
  "status": 500,
  "code": 0
}