Errors & Permissions
Refusals in the Quazzar Space API follow one rule, and knowing it saves you from debugging the wrong thing.
404 means “not yours, or not there” — and you cannot tell which
A tenancy check answers 404 for both “no such object” and “that object is not in a project you belong to”, with the same status and the same response body.
That is deliberate. A caller who supplies ids they do not own is doing so precisely because they are guessing; answering 403 for a real id and 404 for an absent one grades each guess and turns guessing into a search. From outside the tenant the two cases are indistinguishable, which is the point.
The consequence for your code: 404 is not proof that an id does not exist. If you are certain an id is real and you get a 404, the likely cause is that the calling account is not a member of the project that owns it.
Where this is observable today:
| Request | 404 detail |
|---|---|
POST /impex/export, POST /impex/import | Project not found |
POST /invites/{invite_id}/accept (address does not match the invitation) | Invitation not found |
POST /invites/{invite_id}/resend, DELETE /invites/{invite_id} | Invitation not found |
PATCH /attachments/services/{service_id}/networks | Service not found |
PATCH /attachments/service-floors/{floor_id}/networks | ServiceFloor not found |
POST /integrations/cloud-accounts/{account_id}/discover | CloudAccount not found |
| Any project-scoped route reached through the shared tenancy helper | Project not found |
Several of these answered 403 until recently, which made the pair of responses a working enumeration oracle. They now answer 404, and the body for a real id belonging to someone else is byte-identical to the body for an id that does not exist.
The /integrations case is worth calling out: it answers CloudAccount not found rather than Project not found, because a caller who could compare the two bodies would learn which uuids name real cloud accounts.
The rule is not yet universal. Some endpoints still answer 403 when the caller is not a member of the project — GET /environments?project_id={id} returns 403 Not authorized to view environments for this project, and GET /impex/history returns 403 Not a member of this project. Do not rely on the status alone to decide whether an id exists; treat both 403 and 404 on a project-scoped read as “you have no access to this”.
403 means “you are inside, and you still may not do this”
A permission refusal is 403, and it runs after the tenancy check has passed. By then the caller is established to be inside the project — they can already list its contents — so telling them what they may not do leaks nothing they could not already infer.
Typical details: Missing permission: connections.manage, Project owner required, Not authorized for this environment, Not authorized to manage environment roles.
403 also covers refusals that are about the account rather than about one object:
| Detail | Cause |
|---|---|
Active subscription required to create projects. Please choose a plan. | POST /projects with no active subscription |
Project limit reached for your '<plan>' plan (max N)... | Plan limit on POST /projects |
You are not allowed to create projects | The account is not a customer — see Account classes |
Registration is disabled / Registration is invite-only... | POST /auth/register under those policies |
The rest of the status codes
| Status | What it means here |
|---|---|
400 | The request is well-formed but the operation is refused: a duplicate name, an expired or already-used token, an environment that does not belong to the project, a password that fails the policy |
401 | No usable access token, or credentials rejected. See Authentication for the exact details a token refusal returns |
403 | Permission or account-level refusal, after tenancy passed |
404 | Tenancy refusal, or genuinely absent — indistinguishable, see above |
409 | POST /invites/accept/by-token for an address that already has an account. See Invitations |
422 | Request-schema validation. Runs before any lookup, so it is returned identically for a real id and an absent one |
423 | Login lockout after repeated failures. The body carries locked_until |
429 | Rate limit; see Rate Limits |
422 preceding every lookup matters for one thing: it is not a disclosure. A request that fails schema validation is refused before the API looks anything up, so an over-long array or a malformed uuid tells you nothing about whether the ids in it exist.
Next Steps
- Authentication — tokens, sessions, and the refusals they produce
- Invitations — the invitation flow and account classes
- Projects API — project and environment roles