---
url: /guide/errors.md
---
# Errors

When something goes wrong you get an HTTP status other than `200`, and the usual [envelope](/guide/getting-started#response-envelope) with `"status": "error"`.

```json
{
  "status": "error",
  "data": "Please provide a modeOfPayment of cash or bank",
  "message": "Please provide a modeOfPayment of cash or bank"
}
```

Read the HTTP status first, then the text. For most errors `data` and `message` hold the same text. For authentication errors, `message` is always `"Unauthorized to access"` and **`data` says exactly what is wrong**.

## HTTP status codes

| HTTP | Meaning | Retry? |
|---|---|---|
| `200` | It worked | |
| `400` | Something in your data is wrong or missing | No. Fix the data first |
| `401` | Signing or API key problem | No. Fix your signing, key or clock |
| `403` | The key's user cannot use Johari yet, or may not do this (for example, a request was returned to someone else) | No. Read the message |
| `404` | Not found, not visible to your key, or a [wrong path](#wrong-path) | No |
| `429` | Too many calls. See [Too many requests](#too-many-requests) | Yes, after the `Retry-After` header's number of seconds. For `POST`, use the **same** `idempotencyKey` and body |
| `409` | That `idempotencyKey` was already used | Depends on `data`. With `data`: no, it worked, `data` is what the first call returned. With `data: null`: yes, after a few seconds with the **same** key. See [Idempotency](/guide/idempotency#what-johari-answers) |
| `500` | Something went wrong on Johari's side | Yes, after a short wait. For `POST`, use the **same** `idempotencyKey` and body |

## Authentication errors

These can come from **any** endpoint.

| HTTP | `data` | Cause | Fix |
|---|---|---|---|
| `401` | `Please provide the x-api-key, x-signature and x-timestamp headers` | One or more headers missing. The message does not say which one | Send all three. See [Signing](/guide/signing#the-three-headers) |
| `401` | `Request timestamp is too old or invalid` | `x-timestamp` is more than 60 seconds away from Johari's clock, or is not only digits (Unix seconds) | Use the current time in **seconds**, digits only, made fresh for each call. Check your server clock |
| `401` | `Invalid or disabled API key` | The key is wrong, it was disabled in Johari, or it was rotated and you are still using the old pair | Check the key: the whole value, `jh_key_` included. If it was rotated, use the new pair. If it was disabled, get a new key. See [Getting Started](/guide/getting-started#rotating-a-key) |
| `401` | `Invalid signature` | The signature does not match, or `x-signature` is not exactly 64 hex characters | Sign with the whole signing secret, `jh_secret_` included. After a rotation, use the new secret. See [Common mistakes](/guide/signing#common-mistakes). Check your code against the [test vectors](/guide/signing#test-vectors) |
| `401` | `The owner of this API key is disabled or no longer exists` | The Johari user the key belongs to was disabled | Ask your Johari administrator |
| `403` | `The owner of this API key must review and accept their permission agreement before using the system. Please sign in to the web portal to complete this.` | The key's user has not accepted Johari's permissions agreement | That user must sign in to Johari once and accept it |

Example:

```json
{
  "status": "error",
  "data": "Invalid signature",
  "message": "Unauthorized to access"
}
```

## Too many requests

These can come from **any** endpoint. Johari answers `429` when:

* an API key goes over its limit. Per API key, per server, per minute: at most **120 calls** in total, and at most **30 writes** within those. Writes are creates, responses to returns and file uploads together (`POST /api/requests`, `POST /api/requests/:requestID/respond-to-return` and `POST /api/requests/:requestID/files`), or
* one address had **30 failed sign-ins** (any `401`) within a minute. Every call from that address then gets `429` for the rest of that minute, even with a valid key.

The answer has a `Retry-After` header: the number of seconds until the minute resets. The body is always:

```json
{
  "status": "error",
  "data": "Too many requests. Please wait before trying again",
  "message": "Too many requests. Please wait before trying again"
}
```

Wait `Retry-After` seconds, then send the call again. For a `POST`, send the **same** `idempotencyKey` and the same body, so nothing is done twice.

## Lookup errors

| Endpoint | HTTP | `message` | Cause |
|---|---|---|---|
| `GET /api/companies/:companyID/sites` | `404` | `Company not found` | Wrong ID, or the key's user cannot raise requests for this company |
| `GET /api/companies/:companyID/cost-codes` | `404` | `Company not found` | Same as above |
| `GET /api/sites/:siteID/approvers` | `404` | `Site not found` | Wrong ID, or the key's user cannot raise requests at this site |

## Create request errors

`POST /api/requests`. Checks run in the order below, and you get the first problem found. In three stages: (1) the `idempotencyKey`, so a key that was already used is answered before any other field is looked at; (2) each field's type and format, in the order of the table, then `callbackURL`, then `files`; (3) whether `requestingCompany`, `requestingSite`, `costCode` and `admin` are in your lookup lists.

In the `files[0]` messages, the number is the position of the bad file in `files`, starting at 0. For example, `files[2]` is the third file.

| HTTP | `message` | Fix |
|---|---|---|
| `400` | `Please provide an idempotencyKey of up to 255 characters` | Send a text `idempotencyKey`, 1 to 255 characters |
| `400` | `This idempotencyKey was already used for a different request. Use a new key for a new request` | Every new request needs a new key. A key already used for a response or an upload cannot be used either. On a retry, send exactly the same body. See [Idempotency](/guide/idempotency) |
| `400` | `Please provide a description` | Send a non-empty text `description` |
| `400` | `description cannot be longer than 500 characters` | Shorten it |
| `400` | `Please provide an amount greater than 0` | Send a number above 0, or a string like `"150000"`, `"150,000"` or `"150,000.50"` |
| `400` | `amount must be a number such as 150000, or a string such as "150,000.50"` | A string `amount` held something other than digits, thousands commas and one decimal part (e.g. `"0x10"`, `"1e3"`, `"-5"`, `".5"`). Send a JSON number, or a string like `"150,000.50"` |
| `400` | `Amount cannot have more than 2 decimal places` | Round to 2 decimal places |
| `400` | `amount is too large` | The amount is above 90,071,992,547,409.91, the most Johari can store. Check the amount |
| `400` | `Please provide a valid requestingCompany` | Send a company `id` from [List Companies](/api/companies) |
| `400` | `Please provide a requestingSite` | Send a site `id` as text |
| `400` | `Please provide a costCode` | Send the cost code `value` as **text**: `"1003"`, not `1003` |
| `400` | `Please provide a modeOfPayment of cash or bank` | Use `"cash"` or `"bank"` |
| `400` | `Please provide a currency of USD, TZS, KES, ZAR, AED, GBP` | Use one of those, or leave `currency` out for TZS |
| `400` | `isUrgent must be true or false` | Send `true` or `false`, not `"yes"` |
| `400` | `notes must be text` | Send a string |
| `400` | `notes cannot be longer than 2000 characters` | Shorten it |
| `400` | `Please provide an admin: an approver id from the site's approvers list` | `admin` is missing, or is not a 24-hex-character ID. It is required: send an approver `id` from [List Approvers](/api/approvers) for `requestingSite` |
| `400` | `callbackURL cannot be longer than 2048 characters` | Shorten it. `callbackURL` is at most 2048 characters |
| `400` | `Please provide a valid callbackURL` | Send a full `https://` URL your server answers on. On the live server it must point to a public address: `localhost`, and IP addresses that are loopback, private, link-local, carrier-grade NAT, multicast or otherwise reserved, are refused. See [Turning callbacks on](/guide/callbacks#turning-callbacks-on) |
| `400` | `files must be a list` | Send `files` as a JSON array, or leave it out |
| `400` | `You can attach at most 5 files` | Send 5 files or fewer |
| `400` | `files[0] must be an object with name, contentType and content` | Each item in `files` must be an object. The number in `files[N]` is the position of the bad file, starting at 0 |
| `400` | `files[0].name must be a file name of up to 255 characters, without slashes or invisible characters` | Send a non-empty `name` of up to 255 characters, with no `/` or `\`, and no control or invisible formatting characters (for example right-to-left marks) |
| `400` | `files[0].contentType must be one of application/pdf, image/jpeg, image/png, image/webp, image/heic, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Use one of the listed types, exactly as written. See [Attaching files](/api/create-request#attaching-files) |
| `400` | `files[0].content must be the file encoded as base64` | Send the file as standard base64: `A`–`Z`, `a`–`z`, `0`–`9`, `+`, `/`, with `=` padding, length a multiple of 4. No `data:` prefix, no line breaks |
| `400` | `files[0].content is empty` | The file has no bytes. Send the whole file |
| `400` | `files[0].content does not match its contentType` | The file's first bytes are not of the type in `contentType` (for example a PDF without `%PDF-` in its first 1024 bytes). Pick `contentType` from the file's extension with the [table](/api/create-request#attaching-files), and send the real file, not placeholder text |
| `400` | `The attached files cannot be more than 25 MB together` | Keep all files together at or under 25 MB (decoded) |
| `400` | `requestingCompany was not found among the companies you can raise requests for` | Use an `id` from [List Companies](/api/companies) |
| `400` | `requestingSite was not found among this company's sites you can raise requests for` | Use an `id` from [List Sites](/api/sites) **for the same company** |
| `400` | `costCode is not one of this company's cost codes` | Use a `value` from [List Cost Codes](/api/cost-codes) **for the same company** |
| `400` | `admin is not one of this site's approvers` | Use an `id` from [List Approvers](/api/approvers) **for the same site** as `requestingSite` |
| `400` | `Requesting company is disabled` | The company was disabled in Johari. Ask your administrator |
| `400` | `An error occurred, please try again` | The body is not valid JSON. Check your JSON. (`data` is `null` here) |
| `413` | `An error occurred, please try again` | The body is over 35 MB, usually because of the attached files. Keep the files at or under 25 MB together, which keeps the body under 35 MB. (`data` is `null` here) |
| `409` | `A request with this idempotencyKey has already been processed` | Not a failure: `data` has the request that was created. See [Idempotency](/guide/idempotency) |
| `409` | `A request with this idempotencyKey is still being processed` | Wait a few seconds and send it again with the same key |
| `500` | `Failed to upload the files, please try again later` | Retry after a short wait with the **same** `idempotencyKey` and the same body |
| `500` | `Failed to create request, please try again later` | Retry after a short wait with the **same** `idempotencyKey` |

## Get request errors

`GET /api/requests/:requestID`

| HTTP | `message` | Cause |
|---|---|---|
| `404` | `Request not found` | Wrong ID, the request was raised with a different API key, or it was deleted in Johari |

## Get file errors

`GET /api/requests/:requestID/files/:fileID`

| HTTP | `message` | Cause |
|---|---|---|
| `404` | `Request not found` | Wrong request ID, the request was raised with a different API key, or it was deleted in Johari |
| `404` | `File not found` | The request has no file with that `fileID`. Use a `files[].id` from [Get a Request](/api/get-request#files) or a callback |

For both, `data` is `null`.

## Respond to a Return errors

`POST /api/requests/:requestID/respond-to-return`. Checks run in the order below, and you get the first problem found. `data` holds the same text as `message`, except where noted.

| HTTP | `message` | Fix |
|---|---|---|
| `400` | `Please provide an idempotencyKey of up to 255 characters` | Send a text `idempotencyKey`, 1 to 255 characters |
| `400` | `This idempotencyKey was already used for a different request. Use a new key for a new request` | The key was already used with a different body, for a different request, or for another call (a create or an upload). Use a new key for each new response. On a retry, send exactly the same body to the same URL. See [Idempotency](/guide/idempotency) |
| `409` | `A request with this idempotencyKey has already been processed` | Not a failure: your response was already submitted. `data` is what that call returned. See [Idempotency](/guide/idempotency) |
| `409` | `A request with this idempotencyKey is still being processed` | Wait a few seconds and send it again with the same key. (`data` is `null` here) |
| `404` | `Request not found` | Wrong request ID, the request was raised with a different API key, or it was deleted in Johari |
| `403` | `This request was returned to its confirmer (Amina Juma), so only they can respond to it` | The request was returned to its `approver` or `confirmer` (the message names them), not to your key's user. That person answers it in Johari. Wait for the callback |
| `400` | `Please provide a response` | `note` is missing, empty or not text. Send your answer as text |
| `400` | `note cannot be longer than 2000 characters` | Shorten it |
| `400` | `This request is not in returned status` | The request is not `returned`: it was never returned, or the return was already answered. Check it with [Get a Request](/api/get-request) |
| `403` | `You do not have permission` | Your key's user can no longer raise requests at the request's site. Ask your Johari administrator |

A body that is not valid JSON gets `400`, and a body over 35 MB gets `413`, before any of these checks. Both are the same as for [creating a request](#create-request-errors) (`message` `An error occurred, please try again`, `data` `null`).

## Add Files errors

`POST /api/requests/:requestID/files`. Checks run in the order below, and you get the first problem found. `data` holds the same text as `message`, except where noted.

| HTTP | `message` | Fix |
|---|---|---|
| `400` | `Please provide an idempotencyKey of up to 255 characters` | Send a text `idempotencyKey`, 1 to 255 characters |
| `400` | `This idempotencyKey was already used for a different request. Use a new key for a new request` | The key was already used with a different body, for a different request, or for another call (a create or a response). Use a new key for each new upload. On a retry, send exactly the same body to the same URL. See [Idempotency](/guide/idempotency) |
| `409` | `A request with this idempotencyKey has already been processed` | Not a failure: the files were already added. `data` is what that call returned. See [Idempotency](/guide/idempotency) |
| `409` | `A request with this idempotencyKey is still being processed` | Wait a few seconds and send it again with the same key. (`data` is `null` here) |
| `404` | `Request not found` | Wrong request ID, the request was raised with a different API key, or it was deleted in Johari |
| `400` | `You can only upload up to 3 files` | Send 1 to 3 files. For more, make another call with a new key |
| `400` | `files must be a list` | Send `files` as a JSON array |
| `400` | Any `files[0]...` message | The same messages and fixes as for [creating a request](#create-request-errors). The number is the position of the bad file in `files`, starting at 0 |
| `400` | `The attached files cannot be more than 25 MB together` | Keep all files together at or under 25 MB (decoded) |
| `400` | `Please provide a file` | `files` is missing or an empty list. Send 1 to 3 files |
| `500` | `Failed to upload the files, please try again later` | Retry after a short wait with the **same** `idempotencyKey` and the same body |
| `403` | `You do not have permission` | Your key's user no longer has access to the request's site. Ask your Johari administrator |

A body that is not valid JSON gets `400`, and a body over 35 MB gets `413`, before any of these checks. Both are the same as for [creating a request](#create-request-errors) (`message` `An error occurred, please try again`, `data` `null`).

## Server errors

| HTTP | `message` | What to do |
|---|---|---|
| `500` | `An error occurred, please try again` (`data` is `null`) | Retry after a short wait. For a `POST`, use the **same** `idempotencyKey` and body, so nothing is done twice. If it keeps happening, contact your Johari administrator |

## Wrong path

A path that does not exist (for example a typo like `/api/companys`, or `/api/requests` without an ID) returns:

```json
{
  "status": "error",
  "data": null,
  "message": "This endpoint does not exist"
}
```

with HTTP `404`. The signature is checked **before** the path, so an unsigned call to a wrong path gets the `401` for missing headers instead. If a signed call that works on other paths gets `404`, check the path for a typo.

## When to retry

```
Network error or timeout ──► retry (same idempotencyKey)
429                      ──► retry after Retry-After seconds (same idempotencyKey)
500                      ──► retry after a short wait (same idempotencyKey)
409 with data null       ──► retry after a few seconds (same idempotencyKey)
409 with data            ──► stop: it worked
400 / 401 / 403 / 404    ──► stop: fix the cause first
```

## Related pages

* [Signing Requests](/guide/signing)
* [Idempotency](/guide/idempotency)
* [Create a Request](/api/create-request)
* [Respond to a Return](/api/respond-to-return)
* [Add Files](/api/add-files)
