Errors
When a call fails, the HTTP status tells you what kind of problem it is and the body tells you where. Read the status first: it is the one thing every error has. Bulk endpoints are the exception, because they report failed rows inside a 200.
Status codes#
| Status | What it means |
|---|---|
| 200 | Done. On bulk endpoints also when some rows failed; see Failures inside a 200. |
| 201 | Created, for example by POST /customers and POST /warehouses. POST /int/cards answers 201 also when the card already existed. |
| 204 | Deleted. There is no body. |
| 400 | We could not read the request: the body is not valid JSON, the top-level key the endpoint expects is missing (stocks on PUT /int/stocks), a required query parameter is missing, or a value has the wrong type. |
| 401 | The token is missing, invalid, expired or revoked. See Authentication. |
| 404 | The path does not exist, or the record is not in your app. |
| 406 | Your Accept header rules out JSON. Send application/json or leave the header out. |
| 413 | The request body is larger than we accept. Split the batch. |
| 422 | We read the request and rejected it: a field failed validation, or a rule said no, for example an order that already exists. |
| 500 | Something failed on our side. See When to retry. |
The error body#
Most errors come as an errors object. Each key names where the problem is, and each value is a list of messages:
- A field name, such as
city, when a field of the object you sent failed validation. - An area, such as
Warehouses, taken from the message key:API.Warehouses.NotFoundgoes underWarehouses. message, when the message has no area.
Some responses differ from that shape:
- 401:
errors.tokenholds one string, not a list. - 400, 406, 413 and some 500s that happen before your request reaches an endpoint:
{"errors": {"detail": "Bad Request"}}with the status text. - A few rule errors:
{"error": "<message>"}, singular, for example a duplicatePOST /int/orders. - 404: an unknown path answers
[]; a record that is not there may answer with an empty body. - 500 from an endpoint:
{"message": "An unexpected error occurred."}.
Some error responses are sent without a Content-Type header. Parse the body as JSON whatever the header says.
{
"errors": {
"Warehouses": ["API.Warehouses.NotFound"]
}
}
{
"error": "API.Order.OrderAlreadyExists"
}
{
"errors": {
"detail": "Bad Request"
}
}
Messages#
Messages are keys such as API.Warehouses.NotFound or API.Order.AppLocationNotFound: API, the area, then what went wrong. When your app has a translation for a key in your token's language, you get the translation instead of the key. Validation of a single object answers with the plain text of the rule, such as can't be blank.
Branch on the status and the key, not on the message text. The same error can come back as a key or as a translated sentence, depending on your app's translations and your token's language. Log the full body: it is what we need when you ask us.
Validation errors#
When you create or update one object, every field that fails comes back under its own name, with the rules it broke. A warehouse without an address, city, postcode and market answers like the first sample.
Bulk writes report the first failure only, as one key that names the area, the field and the rule. The same warehouse sent through PUT /warehouse answers like the second sample. Fix it and send again to see the next one.
Where a value has the wrong type, POST /int/orders adds a fields list that names each field, the type we expected and what we got.
{
"errors": {
"address_line_1": ["can't be blank"],
"city": ["can't be blank"],
"postcode": ["can't be blank"],
"market_id": ["can't be blank"]
}
}
{
"errors": {
"Warehouses": ["API.Warehouses.AddressLine1CantBeBlank"]
}
}
{
"errors": {
"Utils": ["API.Utils.WrongDataType"],
"fields": [
{ "field": "email", "expected_type": "string", "received_type": "integer", "received_value": 5 }
]
}
}
Failures inside a 200#
Endpoints that take a list write what they can and tell you what they could not. The status is 200 either way, so always read the list of failures:
| Endpoint | Where the failures are |
|---|---|
| PUT /int/stocks | data.failed: the row you sent in input, the reason in error.message. See Stock. |
| POST /stock/reconcile | data.failed: the entry you sent in product, the reason in error. See Stock. |
| POST /int/app_locations | Inside data, among the locations written: an entry with a message instead of a location. |
A lookup that finds nothing can also answer 200: GET /products with a single code, barcode, slug or id answers {"data": {}}.
When to retry#
- 4xx: do not send the same request again. Fix what the body names, then send it.
- 500 and network errors: retry after a pause, and wait longer each time. If it keeps failing, write to [email protected] with the request and the response.
- Know what a retry does before you automate it.
POST /int/ordersanswers422withAPI.Order.OrderAlreadyExistsfor an order it already has, so a retry cannot create it twice.PUT /int/stocksadds quantities, so a retry after a timeout that did get through adds them twice: check withGET /stocksfirst, or use reconcile.
Related#
- Authentication: every
401value and what it means. - Stock: per-row results in practice.
- Pagination & filtering: the query parameters list endpoints accept.