I prefer simplicity and using the first example but I’d be happy to hear other options. Here’s a few examples:

HTTP/1.1 403 POST /endpoint
{ "message": "Unauthorized access" }
HTTP/1.1 403 POST /endpoint
Unauthorized access (no json)
HTTP/1.1 403 POST /endpoint
{ "error": "Unauthorized access" }
HTTP/1.1 403 POST /endpoint
{
  "code": "UNAUTHORIZED",
  "message": "Unauthorized access",
}
HTTP/1.1 200 (🤡) POST /endpoint
{
  "error": true,
  "message": "Unauthorized access",
}
HTTP/1.1 403 POST /endpoint
{
  "status": 403,
  "code": "UNAUTHORIZED",
  "message": "Unauthorized access",
}

Or your own example.

  • 1 or 4 but wrapped in a top level error object. It’s usually best to not use the top level namespace because then you can’t add meta details about the request easily later without changing the original response schema.

    Codes are great but I’m usually too lazy to introduce them right away, so I instead have message (which is guaranteed to come back) and context, which is any JSON object and doesn’t adhere to a guaranteed structure. Another poster pointed out that code is way easier for localization since you are probably not localizing your message.

    The HTTP status code is generally sufficient to describe what happened without having to catalogue every error with a unique “code”. A context blob is useful for dumping validation errors or any other details about the error that a human could at least rely on for help.

    Putting the status code on the body seems helpful but is actually useless, since the only place you can assume it’s always provided is on the response itself and not the body.