Handle and recover from validation errors
By the end of this guide your client will react correctly to every failure this API can produce, without parsing a body it was not expecting.
Before you start
Section titled “Before you start”- A working call from the Quickstart.
- A read of Errors — this guide sequences the handling; that page is the catalogue.
1. Branch on the status code first
Section titled “1. Branch on the status code first”This is the whole design. The API emits three different error bodies — Problem Details, the empty 401, and the operation-specific 409 on cost centres — so the body cannot be the thing you dispatch on.
switch (response.status) { case 400: → the request is wrong. Do not retry it unchanged. case 401: → re-authenticate, then retry once. case 403: → a permissions matter. Stop; a human has to change something. case 404: → not visible to you. Stop. case 409: → a conflict with a soft-deleted record. Recoverable, see step 4. default: → 5xx. Retry once with backoff, then surface it.}Only after you have branched should you attempt to read the body, and reading it must never be required to decide what to do.
2. Parse the body defensively
Section titled “2. Parse the body defensively”Treat the body as diagnostic detail, not as control flow.
- A 401 has no body at all. Do not attempt to parse it.
- Most errors are Problem Details, where
errorsmaps field names to messages on a validation failure and is absent otherwise. Check for the key rather than assuming it. - A 409 on cost centres is the exception: it carries an operation-specific body naming the soft-deleted record, not Problem Details.
A parser that throws when the shape is unfamiliar turns a handled 400 into an unhandled exception in your own code. Fall back to logging the raw body and the status.
3. Surface field errors without guessing at them
Section titled “3. Surface field errors without guessing at them”When errors is present, each key is the name of a field in the request you sent and each value is
an array of messages. Map them back onto your own form or payload by name.
Resist normalising these messages into your own error codes. They are human-readable text, not a stable machine contract, and the field names are the part you can rely on.
4. Recover from a conflict rather than renaming blindly
Section titled “4. Recover from a conflict rather than renaming blindly”A 409 means the name you asked for is held by a soft-deleted record that still occupies the underlying unique constraint. The response tells you which record.
You have exactly two moves, and picking the right one is a business decision rather than a technical one:
- Restore the existing record, if it is the same thing coming back — its history and its links survive.
- Choose a different name, if it is genuinely a new record that happens to clash.
Creating a near-duplicate with a suffixed name to get past the error is the wrong move. It leaves two records competing for the same meaning.
5. Retry only what is retryable
Section titled “5. Retry only what is retryable”Retrying a deterministic failure is the most common way an integration turns one problem into a rate problem.
- Never retry a 400, 403, 404 or 409. The identical request produces the identical failure. Fix it, or stop and report it.
- Retry a 401 once, after obtaining a fresh token — not with the token that just failed.
- Retry a 500 once with backoff. If it persists it is a defect on the Recruso side.
Cap retries and give up loudly. An integration that silently retries forever looks healthy from the outside while doing nothing.
Verify the outcome
Section titled “Verify the outcome”Exercise each branch deliberately rather than waiting to meet it in production:
- Send a request with a required field omitted — a 400 with
errorspopulated. - Send a request with no
Authorizationheader — a 401 with an empty body. - Request a record id that does not belong to you — a 404, not a 403.
If your client handles those three without throwing, the remaining cases are variations on them.
Failure modes
Section titled “Failure modes”| What you see | Why | What to do |
|---|---|---|
| Your parser throws on an error response | The body was not the shape you expected — most likely the empty 401, or the cost-centre 409 | Branch on status before parsing; treat an unparseable body as loggable, not fatal |
| A 400 repeats forever | It is being retried | Validation failures are deterministic. Stop retrying and fix the payload |
| A 401 storm after a token expires | Every queued request retries and each one re-authenticates | Re-authenticate once, then release the queue |
| A 404 on an id you just received | The id came from different credentials, or the record is soft-deleted | Confirm the id came from a list call made with the same token |
Errors — the full catalogue behind every row above.

