Error codes are an information channel. A carefully chosen code can confirm the existence of a tenant, a database or a key without returning any of its data.
The rule
Every lookup is scoped to the authenticated owner. A request for a database that belongs to another account returns 404, identical to a request for a database that never existed. The platform does not distinguish "not yours" from "not found" in the response.
The same principle applies to keys. A key name that exists in another tenant namespace is not observable through this account. Within your own database, a missing or expired key is also 404, which keeps the cache-miss contract simple.
GET /api/data/rdb_someone_else/keys/session:1
Authorization: Bearer rk_your_key
404 { "success": false, "error": { "code": "ERR_NOT_FOUND" } }Where 403 is still correct
403 is reserved for cases where the caller already has a legitimate relationship with the resource: a read-only key attempting a write, or a key used outside the database it was minted for. Those responses tell an authenticated caller that the request was understood and refused, without revealing anything they did not already know.
Admin endpoints are a separate surface. A non-admin session receives 403 because the session is valid and the role check failed. That is not tenant enumeration; the route itself is public knowledge.
Why it matters in a cache
Cache keys often contain predictable identifiers: user IDs, session prefixes, feature names. If a cross-tenant probe returned 403 for existing databases and 404 for missing ones, an attacker could map another account's namespace without reading a single value.
Non-disclosure is cheap to implement at the service boundary and expensive to retrofit after clients depend on richer error semantics. Doing it from the first request keeps the contract honest.
Try the contract yourself.
Five databases are free. Create one, mint a scoped key and send a command.