Error response formats
The API returns error bodies in three formats, depending on the endpoint. Your error handling needs to read all three.
Format 1: Flat field errors
Used by deals (POST /deals/, POST /deals/update_or_create/).
{"user_id": ["Invalid pk \"999999\" - object does not exist."],"products": ["Invalid pk \"999999\" - object does not exist."]}Errors are at the root level, keyed by field name. All field errors come back in one response.
Format 2: Nested under errors
Used by events (POST /events/).
{"errors": {"user_id": ["Client user with ID '999999' does not exist"],"name": ["This field is required."]}}Field errors are inside an errors object.
Format 3: Detail string
Used when the record in the URL doesn't exist, for example GET /users/:id/ or POST /users-by-id/:user_custom_id/events/.
{
"detail": "No ClientUser matches the given query."
}
A single detail string, with status 404.
Read all three formats
This function turns any of the three formats into one dictionary of errors by field:
def parse_api_error(response):
data = response.json()
if "detail" in data:
return {"_general": [data["detail"]]}
if "errors" in data:
return data["errors"]
return data
Validation order
The API validates fields in a set order, and an early failure can hide later ones.
Events for a contact by custom ID: the contact is validated first
POST /users-by-id/:user_custom_id/events/ checks the contact in the URL before anything else. If the contact doesn't exist, you get 404 and no errors about the request body, even if it is empty.
If you get a contact-not-found error, fix it first: more errors may show up once the contact exists.
Events by contact ID: all fields are validated at once
POST /events/ returns every field error in one response, the contact included:
{"errors": {"name": ["This field is required."],"user_id": ["Client user with ID '999999' does not exist"]}}Deals: all fields are validated at once
The deals endpoints validate all fields together and return every error in one response, so you can fix them all at once.