Skip to content

Errors

Write your error handling against the status code, not against the shape of the body.

Most failures are RFC 7807 Problem Details, but not all of them: an unauthenticated request returns no body at all, and a 409 on cost centres carries an operation-specific body. A parser that assumes a single shape will throw on a failure it was written to handle.

RFC 7807 Problem Details

Content-Type: application/problem+json

The API's standard error shape. Emitted by FastEndpoints validation failures (c.Errors.UseProblemDetails()) and by the unhandled-exception handler. This is the shape to write your error handling against.

The status field always repeats the HTTP status code.

FieldWhat it carries
typeA URI reference identifying the problem type.
titleA short, human-readable summary. Stable for a given problem type.
statusThe HTTP status code.
detailA human-readable explanation. On 500 this is populated only in Development; in production it is null so internal messages are never disclosed.
instanceThe request path that produced the error.
errorsOn validation failures, a map of field name to an array of messages. Absent on non-validation errors.
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "instance": "/branches",
  "errors": {
    "branchLocation": ["Branch location is required."]
  }
}

Bare 401

No response body.

Every 401 has an empty body — there is no JSON payload to parse, so branch on the status code. A business operation rejecting a missing or expired token also sends WWW-Authenticate: Bearer, because that 401 comes from the JWT handler's challenge. POST /apiauth is anonymous, so nothing challenges and the header is absent. Do not branch on the header.

HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Bearer          # business operations only, not POST /apiauth
StatusBodyWhat it meansWhat to doRetry?
400RFC 7807 Problem DetailsThe request was rejected before anything changed.Read errors for the offending field names. These are deterministic — retrying the identical request produces the identical failure. Most come from the payload, so fix it rather than retrying; some come from data already stored, such as an entry's date or status or your agency's settings, which no change to the request fixes: the message says which.No — fix the request
401Bare 401No token, an expired token, or a token the endpoint will not accept.Obtain a fresh token with POST /apiauth. Request a new token when the current one expires rather than on every call.After re-authenticating or backing off
429RFC 7807 Problem DetailsToo many token exchanges from the same source address in a short window.Cache the access token and re-exchange only when it expires — read expiresIn for the lifetime. Only POST /apiauth is rate limited; business operations are not.After re-authenticating or backing off
403RFC 7807 Problem DetailsThe token is valid but does not carry the permission the endpoint requires.This is a configuration matter, not a request matter. Contact Recruso support; changing the request will not help.No — fix the request
404RFC 7807 Problem DetailsNo such record that your credentials can see. Deliberately indistinguishable from "exists but belongs to another agency" — the API never discloses the existence of another tenant's data.Confirm the id came from a list call made with the same credentials. If you expect access, check that user's branch access in the Recruso UI.No — fix the request
409Operation-specificThe name you asked for is held by a soft-deleted record, which still occupies the underlying unique constraint.The response body carries that record's costCentreId. Either call POST /branches/{branchId}/cost-centres/{id}/restore to bring it back, or choose a different name.No — fix the request
500RFC 7807 Problem DetailsAn unhandled exception. Always a defect on the Recruso side.Retry once with backoff. If it persists, contact Recruso support with the instance path and the approximate time. detail is null in production by design.After re-authenticating or backing off

400 — The request was rejected before anything changed.

Body: RFC 7807 Problem Details · Not retryable — fix the request

Why it happens

  • A required field was missing or empty.
  • A field exceeded the length of its underlying column.
  • A uniqueness rule was violated by an active record — for example a branch name already in use, or a cost centre name already used in the same branch.
  • Restoring a cost centre whose name has since been taken by an active one.
  • For a worker's availability list, from or to is missing, from is after to, or the range covers more days than your agency shows at once in Recruso CRM, at most 30.
  • An availability date, new or already stored, is more than 20 days before today or more than a year after it.
  • An availability status that affects a worker's AWR parity is chosen, or is already held by the entry being changed or removed.
  • The entry being changed or removed holds an availability status that is not one of your agency's.
  • A worker already has as many availability entries on a date as your agency allows, which is one, or two when it allows more than one status a day.
  • Your agency's holiday accrual settings hold more than one entry for the worker's type, so an availability entry cannot be created until they are corrected in Recruso CRM.
  • Renaming, or changing the AWR flag of, an availability status that any availability entry in your agency uses.
  • For POST /apiauth, the client id is malformed — it must be the whole value issued by the Developer Console — clientSecret is missing or empty, or grantType is anything other than client_credentials. These are request-shape failures, caught before any credential is looked up — which is why they are 400 and not the 401 a wrong-but-well-formed credential gets.

What to do

Read errors for the offending field names. These are deterministic — retrying the identical request produces the identical failure. Most come from the payload, so fix it rather than retrying; some come from data already stored, such as an entry's date or status or your agency's settings, which no change to the request fixes: the message says which.

401 — No token, an expired token, or a token the endpoint will not accept.

Body: Bare 401 · Retryable, but not with the same request

Why it happens

  • No Authorization: Bearer {token} header was sent.
  • The access token has expired.
  • For POST /apiauth, the client id or secret is wrong, the credential has been revoked or has expired, the API app is inactive, the caller is outside the app's configured IP allow-list, or the Recruso user the app belongs to has been disabled. These are deliberately not distinguished from one another.

What to do

Obtain a fresh token with POST /apiauth. Request a new token when the current one expires rather than on every call.

Retrying with the same token will fail identically. Retry only after re-authenticating.

429 — Too many token exchanges from the same source address in a short window.

Body: RFC 7807 Problem Details · Retryable, but not with the same request

Why it happens

  • Calling POST /apiauth before every request instead of caching the token for its lifetime.
  • Several instances of an integration authenticating independently from one address.

What to do

Cache the access token and re-exchange only when it expires — read expiresIn for the lifetime. Only POST /apiauth is rate limited; business operations are not.

Back off before retrying. A client that retries immediately stays throttled. The response carries a Retry-After header and a retryAfterSeconds member in the body — wait that long rather than guessing.

403 — The token is valid but does not carry the permission the endpoint requires.

Body: RFC 7807 Problem Details · Not retryable — fix the request

Why it happens

  • The token lacks the permission claim the policy demands.
  • The endpoint is restricted to Recruso staff and is not part of the integrator surface.

What to do

This is a configuration matter, not a request matter. Contact Recruso support; changing the request will not help.

404 — No such record that your credentials can see. Deliberately indistinguishable from "exists but belongs to another agency" — the API never discloses the existence of another tenant's data.

Body: RFC 7807 Problem Details · Not retryable — fix the request

Why it happens

  • The id does not exist.
  • The record belongs to a different agency.
  • The record has been soft-deleted.
  • The worker has been deleted under data-protection law — and so, with them, is every record they held, from a compliance document to an activity.
  • The record is under a branch outside the accessible-branch list of the Recruso user behind your API app.
  • Nested ids do not line up — for example a real cost centre id under the wrong branch id.

What to do

Confirm the id came from a list call made with the same credentials. If you expect access, check that user's branch access in the Recruso UI.

409 — The name you asked for is held by a soft-deleted record, which still occupies the underlying unique constraint.

Body: an operation-specific body, not one of the shapes above · Not retryable — fix the request

Why it happens

  • Creating or renaming a cost centre to a name a deleted cost centre in the same branch holds.

What to do

The response body carries that record's costCentreId. Either call POST /branches/{branchId}/cost-centres/{id}/restore to bring it back, or choose a different name.

This status carries an operation-specific body, not Problem Details. See the SoftDeletedCostCentreConflictResponse schema in the reference.

500 — An unhandled exception. Always a defect on the Recruso side.

Body: RFC 7807 Problem Details · Retryable, but not with the same request

Why it happens

  • Nothing you can correct in the request.

What to do

Retry once with backoff. If it persists, contact Recruso support with the instance path and the approximate time. detail is null in production by design.

Worth restating on its own, because it is the single most common misreading of this API. A 404 means no such record that your credentials can see. It is deliberately indistinguishable from “exists, but belongs to another agency” — the API never discloses the existence of another tenant’s data, so a probe cannot be used to enumerate it.

The consequence for your integration is that a 404 is not proof of absence. It is equally consistent with a record you own that has been soft-deleted, a worker deleted under data-protection law, or a record sitting under a branch outside the accessible-branch list of the Recruso user behind your API app. Confirm the id came from a list call made with the same credentials before treating it as gone.

Only three of these statuses are worth a retry, and none is worth an immediate one.

  • 401 — retry once, after obtaining a fresh token. Retrying with the same token fails identically.
  • 429 — back off, then retry. This one is nearly always self-inflicted: cache the access token for its full lifetime instead of exchanging credentials before every call.
  • 500 — retry once with backoff. If it persists, it is a defect on the Recruso side; contact support with the instance path from the body and the approximate time.

Everything else is deterministic. A 400 produced by the same payload will be produced again by the same payload, so fix the request rather than queueing it for retry.

Responses and pagination — the conventions that apply when a call succeeds.