Reading the error surface
Two envelope shapes, a status-type flip, and what each 4xx teaches about the server's state machine.
Errors are data — and here they come in two shapes
See finding G6: the saved examples use both an RFC 7807-style
envelope (title/detail/status/type) and a bare code/message
envelope, with status sometimes a string and sometimes an integer. Your
client should parse defensively:
def parse_error(payload: dict) -> tuple[int, str]:
"Handle both envelopes observed in the saved examples."
if "code" in payload: # {code, message}
return int(payload["code"]), payload.get("message", "")
return ( # {title, detail, status, type}
int(payload.get("status", 0)), # str OR int in the wild
payload.get("detail") or payload.get("title", ""),
)
The 4xx examples are a map of the server's rules
Each saved error example documents a real constraint:
403on Step 3 — "The requested project is not in a state that allows this request." The lifecycle is enforced server-side; you cannot process a project twice, or before loading records.400on Create Profile — "The given data does not provide all necessary fields for generating an iWave profile." There is a minimum viable input; name and address fields must be parsed into their proper fields, not concatenated.202on Step 5 — not an error at all: "The requested project is still being processed." A202on a GET is unusual (it's typically a response to submission); here it means come back later — poll Step 4 and retry.404— same body for a missing profile and a missing project ("title": "An error occured"— typo in source), so distinguish by which URL you called, not by the body.
What you cannot learn from the examples
No 401, 429, or 5xx examples exist in the collection. The description
tells you they happen ("Re-authenticate automatically if you receive a 401
Unauthorized error", "Implement exponential backoff for retries to avoid
rate limit blocks") but shows no bodies for them. Build those handlers
without assuming an envelope shape — you've already seen the server uses two.