# Why Does the API Return 101, 119, 142 or 209? Every Backend Error Code, Reproduced [Full Repo]

We fired 48 requests at two live backends and caught 15 numeric error codes, each with its HTTP status, body and cause. One code, 101, covers five unrelated situations, and it is the only one that arrives as a 404.

URL: https://www.back4app.com/blog/why-does-the-api-return-101-119-142-or-209
Published: 2026-10-02 · Tested: 2026-09-24
Tested on: Backend: Back4app, free plan, USA East · Probes: bash + curl on macOS · Rules: Cloud Code hooks deployed from the dashboard editor
Publisher: Back4app Engineering

Your login form just showed *101 Invalid username/password* to someone who typed the right password. Or the app got a 209 seconds after logging in, or a 119 on a query that worked yesterday, or a 142 with a sentence nobody on your team wrote. The number is the whole diagnosis you have, and the documentation gives each one a single line.

**A backend error code is the numeric `code` field in the JSON body of a failed request, separate from the HTTP status; one code can have several causes, and the `error` message next to it is what tells them apart.** That sentence is the post. The rest is proof, one code at a time.

We wrote a script that triggers every code on purpose and ran it three times against two live backends on September 24, 2026: 48 requests, 43 error responses, 15 numeric codes. Every status and body below is pasted from those runs, and the runs are in the repo.

**Get the working code:** the companion repo is at [github.com/templates-back4app/backend-error-codes](https://github.com/templates-back4app/backend-error-codes) — `reproduce.sh` prints one row per request (expected code, HTTP status, code, message), `seed.sh` prepares the classes and users, `cloud/main.js` is the Cloud Code behind the hook errors, and `results/` holds the runs this post quotes.

## Which code means what, and where is each one produced?

The table below is the answer to the title. The code is the `code` field in the body; the HTTP status is what `curl -w '%{http_code}'` printed; the cause is what we did to get it. The first stage inside the backend that objects answers, and nothing after it runs, so the section order below is the order a request meets them.

![Architecture: a request passes through five stages inside the backend and the first stage that objects answers: the API gateway checks the App ID, key, JSON body and route (401, 403, 400, 404, no code); class-level permissions answer 101 or 119; Cloud Code hooks and functions throw 142, 206, 119, 209 or 141; the schema answers 105, 107, 111; the row ACL hides rows as 101; the users class adds 101, 202, 203, 125, 200, 201, 204, 209](/blog/blog-assets/why-does-the-api-return-101-119-142-or-209/architecture-diagram.svg)

| Code | HTTP | Meaning | Most likely cause | Section |
|---|---|---|---|---|
| 101 | 404 | `Invalid username/password.` | wrong password, or a username that does not exist | [POST /login](#why-does-post-login-return-101-invalid-usernamepassword) |
| 101 | 404 | `Object not found.` | an id that does not exist, or a row the caller's ACL cannot see | [a row you can see](#why-does-a-row-i-can-see-in-the-dashboard-return-101-object-not-found) |
| 101 | 404 | `Permission denied, user needs to be authenticated.` | the class-level permission requires a session and none was sent | [anonymous request](#why-does-an-anonymous-request-return-101-permission-denied-user-needs-to-be-authenticated) |
| 119 | 400 | `Permission denied for action count on class Note.` | the class-level permission allows nobody for that operation; `count` is its own entry | [count vs find](#why-does-count-return-119-when-find-on-the-same-class-works) |
| 119 | 400 | your own message (`moderators only.`) | a Cloud Function threw `OPERATION_FORBIDDEN` | [count vs find](#why-does-count-return-119-when-find-on-the-same-class-works) |
| 142 | 400 | your own message (`username needs at least 3 characters.`) | a `beforeSave` hook threw `VALIDATION_ERROR` | [signup 142](#why-does-signup-return-142-username-needs-at-least-3-characters) |
| 202 / 203 | 400 | `Account already exists for this username.` / `…email address.` | signup with a username or e-mail that is taken | [signup 202 / 203](#why-does-signup-return-202-or-203-account-already-exists) |
| 209 | 400 | `Invalid session token` | a made-up token, no token, or a token that was logged out | [209 after logout](#why-does-the-api-return-209-invalid-session-token-right-after-logout) |
| 206 | 400 | your own message (`log in to create a note.`) | a hook needed `request.user` and the request had no session (master key included) | [master key 206](#why-does-the-master-key-get-206-session-missing-from-a-beforesave-hook) |
| 141 | 400 | `Invalid function: "doesNotExist"`, or the exception's message | function not deployed, or a non-Parse error thrown in Cloud Code | [141](#why-does-a-cloud-function-return-141-and-what-is-in-the-message) |
| 105 / 107 / 111 / 125 | 400 | bad field name / bad class name / wrong type / bad e-mail | the request body | [smaller codes](#which-smaller-codes-did-the-probes-catch-105-107-111-125-200-201-and-204) |
| 200 / 201 / 204 | 400 | username / password / e-mail missing | the request body | [smaller codes](#which-smaller-codes-did-the-probes-catch-105-107-111-125-200-201-and-204) |
| none | 401 | `unauthorized` | no `X-Parse-Application-Id`, or one that names no app | [404 vs 403](#why-is-a-wrong-password-a-404-but-a-wrong-key-a-403) |
| none | 403 | `unauthorized` | right App ID, wrong or missing key | [404 vs 403](#why-is-a-wrong-password-a-404-but-a-wrong-key-a-403) |
| none | 400 | the JSON parser's sentence | the body is not valid JSON | [404 vs 403](#why-is-a-wrong-password-a-404-but-a-wrong-key-a-403) |
| none | 404 | `{"message":"Not Found","error":{}}` | the path does not exist | [404 vs 403](#why-is-a-wrong-password-a-404-but-a-wrong-key-a-403) |

How we measured: `reproduce.sh`, 29 requests against the `user-auth-starter` backend and 19 against `lockdown-lab`, both on the free plan in USA East, each group run three times between 13:13 and 13:29 UTC on September 24, 2026.

The status is what `curl -w '%{http_code}'` printed; `code` and `error` are read from the body by `python3`. The rows were identical across the three runs, with the JavaScript key and again with the Client key.

## Why does POST /login return 101 Invalid username/password?

Because the password is wrong, or the username does not exist, and the backend refuses to tell you which. That is deliberate: a login form that says *no such user* hands an attacker a list of accounts. Both cases answered HTTP 404 with the same body, three runs out of three.

```bash
# Stack: bash + curl | File: reproduce.sh (rows 8 and 9)
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/login" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "Content-Type: application/json" -d '{"username":"ana","password":"wrong"}'
```

```json
{"code":101,"error":"Invalid username/password."}
HTTP 404
```

The fix is on your side of the form: one generic message (*wrong username or password*), a link to the reset flow, and no retry loop. If a user insists the password is right, check the username for a trailing space and check whether the account was created with a different username than the one they type; the e-mail is not accepted in place of the username unless you send it in the `email` field.

One more way to reach this row: `GET /login?username=ana&password=wrong` answers the same 404 / 101 (row 29). The endpoint accepts a query string, so a password in a URL ends up in a log. Send it in the body.

## Why does a row I can see in the dashboard return 101 Object not found?

Because the ACL on that row does not include you. A row the caller may not read and a row that does not exist get the same status, code and message, on purpose, so that an id cannot be probed for existence. We measured both, as `ana` for a made-up id and as `bob` for a note that belongs to `ana`:

```bash
# Stack: bash + curl | File: reproduce.sh (data rows 4, 8 and 9)
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/Note/xxxxxxxxxx" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "X-Parse-Session-Token: $ANA_TOKEN"
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/Note/$ANA_NOTE_ID" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "X-Parse-Session-Token: $BOB_TOKEN"
```

```json
{"code":101,"error":"Object not found."}
HTTP 404
```

The same body came back for `bob`'s `PUT` on that note, and for the master key asking for an id that does not exist (`GET /classes/_User/xxxxxxxxxx`).

The `Note` class in this test gets its ACL from a `beforeSave` hook that grants read and write to the creator alone, which is the setup from [Your Frontend Talks Straight to the Database. Here Is How to Lock It Down](https://www.back4app.com/blog/lock-down-a-backend-your-frontend-talks-to).

The fix depends on which case you are in. If the id is real, open the row in the dashboard and read its ACL column: if it names one user id or is empty, the caller is not on the list, and the right move is a Cloud Function that runs the read with the master key for the callers who should see it. If the ACL is public and you still get 101, the id is wrong; ids are case-sensitive.

## Why does an anonymous request return 101 Permission denied, user needs to be authenticated?

Because the class-level permission for that operation is *Authenticated* and the request carried no session token. This is the third and last face of 101, and the only one whose message says what to do. Every anonymous request to `Note` (find, create, and get by id) got it, three runs out of three:

```bash
# Stack: bash + curl | File: reproduce.sh (data rows 1 to 3)
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/Note" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY"
```

```json
{"code":101,"error":"Permission denied, user needs to be authenticated."}
HTTP 404
```

Note the order: an anonymous `GET /classes/Note/xxxxxxxxxx` gets this message, not `Object not found.` The class permission is checked before the row is looked up. The fix is a session: log the user in and send `X-Parse-Session-Token`, or, if the operation really should be public, change the class-level permission under Database → the class → Security → Class Level Permission.

{/* screenshot: clp-dialog-note-count-empty.jpg — The Class Level Permission dialog for the Note class: Public unchecked, Authenticated checked for Read and Write, and the Advanced JSON view showing "count": {} */}

Both settings take time to reach every server: after a permission change we have seen anonymous requests answered inconsistently for 22 minutes, so probe until you get the same answer several times in a row.

## Why does count return 119 when find on the same class works?

Because `count` is its own entry in the class-level permissions, and the entry was empty. `ana`, logged in, got 200 on `GET /classes/Note` and 400 / 119 on `GET /classes/Note?count=1` in the same run. Nobody had locked `count` on purpose. The permissions had been written through the schema API without a `count` key, and the backend stored that as `count: {}`, which allows nobody.

```bash
# Stack: bash + curl | File: reproduce.sh (data rows 5 and 6)
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/Note?count=1&limit=0" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "X-Parse-Session-Token: $ANA_TOKEN"
```

```json
{"code":119,"error":"Permission denied for action count on class Note."}
HTTP 400
```

That is the general shape of 119: *the operation is denied by configuration, whoever you are*. We reproduced it two more ways. A `Vault` class created with every permission set to `{}` answered 119 on find and create, for `ana` and for an anonymous caller alike (`Permission denied for action find on class Vault.`). And a Cloud Function that checks a role threw it on purpose:

```javascript
// Stack: Node.js 22.x | Parse Server 7.x | File: cloud/main.js (moderatorList)
const isMod = await new Parse.Query(Parse.Role).equalTo("name", "moderator").equalTo("users", request.user).first({ useMasterKey: true });
if (!isMod) throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, "moderators only.");
```

`ana` calling `moderatorList` got 400 / 119 `moderators only.`; `bob`, who is in the role, got the list. The fix for a 119 is never a retry. Either the permission is right and the client should not be making that call, or it is wrong and the fix is in the dashboard's Class Level Permission dialog (or one `PUT /schemas/Note` with the master key, this time with a `count` entry).

## Why does signup return 142 username needs at least 3 characters?

Because a `beforeSave` hook on the `_User` class threw `Parse.Error.VALIDATION_ERROR` with that sentence, and the backend forwards the hook's message unchanged. 142 always means *your own Cloud Code said no*; the text is whatever the person who wrote the hook typed. On the `user-auth-starter` backend the hook is 17 lines:

```javascript
// Stack: Node.js 22.x | Parse Server 7.x | File: cloud/main.js (beforeSave on _User)
Parse.Cloud.beforeSave(Parse.User, (request) => {
  const user = request.object;
  const email = (user.get("email") ?? "").trim().toLowerCase();
  if (!email) throw new Parse.Error(Parse.Error.VALIDATION_ERROR, "an e-mail address is required.");
  user.set("email", email);
  const username = (user.get("username") ?? "").trim();
  if (username.length < 3) throw new Parse.Error(Parse.Error.VALIDATION_ERROR, "username needs at least 3 characters.");
  user.set("username", username);
});
```

```bash
# Stack: bash + curl | File: reproduce.sh (row 13)
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/users" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "Content-Type: application/json" -d '{"username":"ab","email":"ab@example.com","password":"Passw0rd!"}'
```

```json
{"code":142,"error":"username needs at least 3 characters."}
HTTP 400
```

The fix is in the request, and the message is safe to show the user because you wrote it. The case to watch for is a 142 with a sentence you do not recognise: grep your Cloud Code for `VALIDATION_ERROR`, and if it is not there, look at the `beforeSave` of every class the request touches, including the ones a pointer drags in.

If a signup as `ab` answers 201 instead, the hook is not live. On a fresh backend the first Deploy in the Cloud Code editor ships nothing; deploy again and re-test. Details in [How to Add Authentication to a Web or Flutter App Without Writing an Auth Server](https://www.back4app.com/blog/add-signup-login-password-reset-without-an-auth-server).

## Why does signup return 202 or 203 Account already exists?

Because the username (202) or the e-mail (203) is already on another account, and both are unique across the `_User` class. The two are checked in that order: a request that repeats both the username and the e-mail gets 202. We signed up a probe user, then signed up twice more:

```bash
# Stack: bash + curl | File: reproduce.sh (rows 11 and 12)
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/users" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "Content-Type: application/json" -d '{"username":"ana","email":"other@example.com","password":"Passw0rd!"}'
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/users" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "Content-Type: application/json" -d '{"username":"other","email":"ana@example.com","password":"Passw0rd!"}'
```

```json
{"code":202,"error":"Account already exists for this username."}
HTTP 400
{"code":203,"error":"Account already exists for this email address."}
HTTP 400
```

The fix is a form that reacts: on 202 offer the login form with the username filled in; on 203 offer the password reset, since the person probably has an account under another username. Both messages are safe to show. Note that the hook above lower-cases the e-mail before the check, so `Ana@Example.com` and `ana@example.com` collide as 203; without such a hook they would be two accounts.

## Why does the API return 209 Invalid session token right after logout?

Because `POST /logout` deleted that session on the server, and the client kept the token. We signed up (token A), logged out with A, and asked `GET /users/me` with A: 400 / 209. The same status, code and message came back for a made-up token and for no token at all:

```bash
# Stack: bash + curl | File: reproduce.sh (rows 20, 21 and 24)
curl -s -w '\nHTTP %{http_code}\n' "$BASE/users/me" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "X-Parse-Session-Token: r:0000000000000000000000000000000f"
```

```json
{"code":209,"error":"Invalid session token"}
HTTP 400
```

A Cloud Function can throw the same code on purpose: `whoami` on the `user-auth-starter` backend and `moderatorList` on `lockdown-lab` both answer 400 / 209 `log in first.` when `request.user` is empty. The message differs, the code does not, so the client's handler is one branch: clear the stored token, show the login form, and do not retry. A revoked token stays revoked.

Logout revokes one token, not the user. If the same person is logged in on a phone and a laptop, the laptop's token still works after the phone logs out; that was measured in the authentication post and it holds. If "log out everywhere" is a feature, it is a Cloud Function that deletes the user's `_Session` rows with the master key.

## Why does the master key get 206 Session missing from a beforeSave hook?

Because the master key bypasses class-level permissions and ACLs, not Cloud Code. The `Note` hook on `lockdown-lab` needs a user to stamp as the owner; a create with the master key and no session token reached it with `request.user` undefined, and the hook threw `SESSION_MISSING`:

```javascript
// Stack: Node.js 22.x | Parse Server 7.x | File: cloud/main.js (beforeSave on Note)
const owner = request.user ?? note.get("owner");
if (!owner) throw new Parse.Error(Parse.Error.SESSION_MISSING, "log in to create a note.");
```

```bash
# Stack: bash + curl (master key: server side only) | File: reproduce.sh (data row 17)
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/classes/Note" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-Master-Key: $MASTER_KEY" \
  -H "Content-Type: application/json" -d '{"text":"no owner"}'
```

```json
{"code":206,"error":"log in to create a note."}
HTTP 400
```

An anonymous client never sees this row: the class permission turns it away with 101 before the hook runs. So a 206 in your logs is almost always a server-side job, a migration script or a dashboard import that writes with the master key and forgets that the hook expects a user.

The fix is to pass the owner explicitly (the hook falls back to `note.get("owner")`), or to let the hook skip its check when `request.master` is true, if that is what you want.

## Why does a Cloud Function return 141, and what is in the message?

Because 141 is the code for *Cloud Code did not produce a result*, and there are two very different reasons. The first is a function name that is not deployed; the message names it. The second is any error thrown inside a hook or function that is not a `Parse.Error`; the backend wraps it in 141 and passes the exception's own message through. We hit both:

```bash
# Stack: bash + curl | File: reproduce.sh (row 25; data row 18 uses the master key)
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/functions/doesNotExist" \
  -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-REST-API-Key: $REST_KEY" \
  -H "Content-Type: application/json" -d '{}'
```

```json
{"code":141,"error":"Invalid function: \"doesNotExist\""}
HTTP 400
```

For the second reason we sent the `Note` hook an `owner` that is a plain object instead of a user pointer, with the master key. `new Parse.ACL({"foo":"bar"})` throws a `TypeError`, and this is what the client saw:

```json
{"code":141,"error":"Tried to create an ACL with an invalid permission type."}
HTTP 400
```

The message is the JavaScript exception's text, not *Script failed*. That is useful for you and dangerous for your users: a `TypeError` message can leak variable names and object shapes.

The fix for the first case is a deploy (and a second one on a fresh backend). The fix for the second is a `try`/`catch` in the hook that re-throws a `Parse.Error` with a message you chose, and a look at the Logs page, where the stack trace is.

{/* screenshot: logs-cloud-code-typeerror-141.jpg — The Logs page of the backend showing the TypeError "Tried to create an ACL with an invalid permission type." thrown inside beforeSave("Note"), with its timestamp */}

We could not make a hook throw a *plain* `Error` on these backends without deploying new Cloud Code, which needs the dashboard, so the `TypeError` above is the only 141-from-code we measured; the wrapping is the same for any non-Parse error.

## Why is a wrong password a 404 but a wrong key a 403?

Because the HTTP status is decided by two different layers. Every numeric code we reproduced arrived as HTTP 400, except 101, which arrives as 404 because the backend maps *object not found* to *Not Found* and the login error reuses that code. Before any of that, a gateway checks the headers and the body, and its four answers carry no code at all:

```bash
# Stack: bash + curl | File: reproduce.sh (rows 1 to 6)
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/_User"                                        # no App ID
curl -s -w '\nHTTP %{http_code}\n' "$BASE/classes/_User" -H "X-Parse-Application-Id: $APP_ID"  # no key
curl -s -w '\nHTTP %{http_code}\n' -X POST "$BASE/login" -H "X-Parse-Application-Id: $APP_ID" \
  -H "X-Parse-REST-API-Key: $REST_KEY" -H "Content-Type: application/json" -d '{"username": "ana", '
```

```json
{"error":"unauthorized"}
HTTP 401
{"error":"unauthorized"}
HTTP 403
{"error":"Expected double-quoted property name in JSON at position 20"}
HTTP 400
```

| What you sent | HTTP | Body |
|---|---|---|
| no `X-Parse-Application-Id`, or an App ID that names no app | 401 | `{"error":"unauthorized"}` |
| right App ID, wrong key, or no key | 403 | `{"error":"unauthorized"}` |
| a body that is not valid JSON | 400 | `{"error":"Expected double-quoted property name in JSON at position 20"}` |
| a path that does not exist (`GET /nothing-here`) | 404 | `{"message":"Not Found","error":{}}` |
| wrong password, missing row, hidden row, no session (101) | 404 | `{"code":101,"error":"…"}` |
| every other numeric code (119, 141, 142, 202, 203, 206, 209, …) | 400 | `{"code":N,"error":"…"}` |

So the diagnosis order is: no `code` in the body means the request never reached your app's rules, and 401 versus 403 tells you which header is wrong; a 404 with code 101 is one of three situations the message separates; anything else is a 400 whose code is the whole story. The official error table lists 107 for badly formed JSON; the live backend answered with no code.

## Which smaller codes did the probes catch: 105, 107, 111, 125, 200, 201 and 204?

Seven more, all HTTP 400, all about the request body, and one of them with a side effect. Each row below is three runs out of three:

| Code | What we sent | `error` |
|---|---|---|
| 105 | `POST /classes/ErrProbe` with the field `"bad key"` | `Invalid field name: bad key.` |
| 107 | `POST /classes/bad-name` (a hyphen in the class name) | `schema class name does not revalidate` |
| 111 | `POST /classes/Note` with `"text": 123` on a String column | `schema mismatch for Note.text; expected String but got Number` |
| 125 | `POST /users` with `"email": "not-an-email"` | `Email address format is invalid.` |
| 200 | `POST /users` without `username` | `bad or missing username` |
| 200 | `POST /login` with an empty body | `username/email is required.` |
| 201 | `POST /users` without `password` | `password is required` |
| 204 | `POST /requestPasswordReset` without `email` | `you must provide an email` |

The side effect is on 105. The create was refused, and the `ErrProbe` class appeared in the schema anyway, empty, with default permissions. We deleted it, sent the request again, and it appeared again. If your schema has classes you do not remember creating, a rejected write from a client may have made them; `reproduce.sh` deletes its own with `DELETE /schemas/ErrProbe` after each run.

107 is the other surprise: the documentation lists 103 for an invalid class name, and the live backend answered 107 on `POST` and, on `GET /classes/bad-name`, a plain 200 with `{"results":[]}`. A typo in a class name on a read does not fail; it returns nothing.

## Which codes could we not reproduce?

Two, and they are not in the table because we did not see them. **103** (invalid class name): a `GET` on `bad-name` answered 200 with an empty result and a `POST` answered 107, three runs out of three.

**205** (no user with that e-mail): `POST /requestPasswordReset` with an address nobody has answered `200 {}`, which is the right behaviour for a reset form and means your client will never see 205 from that endpoint.

We also did not try to provoke code 1 (internal server error) or the rate-limit and timeout codes; they need conditions a probe script should not create on a shared backend.

## What goes wrong when you run reproduce.sh against your own backend?

**`users ana/bob missing: run ./seed.sh first`** the `data` group logs in as `ana` and `bob` and stops if either login fails. Run `./seed.sh` with the same `.env`; it creates both users, the `moderator` role, and the `Note` and `Vault` classes, and it is safe to re-run. If you changed the passwords, export `ANA_PASS` and `BOB_PASS`.

**`401 unauthorized` on every row** the `APP_ID` in `.env` is wrong or has a trailing character. Copy it again from App Settings → Security & Keys. The script's row 2 sends a deliberately wrong App ID; if rows 3 to 29 look like row 2, the problem is your `.env`.

**`403 unauthorized` on every row** the App ID is right and `JS_KEY` is not. Same page, the *JavaScript key* entry. Any client key works (`KEY_HEADER=X-Parse-Client-Key KEY_VALUE=… ./reproduce.sh` gave the same rows); the master key is used only where the label says so.

**`142` row answers `201`** the `_User` hook is not live. On a fresh backend the first Deploy in the Cloud Code editor ships nothing: edit `main.js`, deploy again, re-run. The signup created a user `ab`; delete it in Database → `_User` or with the master key.

**`141 Invalid function: "whoami"`** on row 22, same cause: `cloud/main.js` is not deployed, or was deployed once. Both functions and both hooks live in that one file.

**`206` row answers `201`** the `Note` hook is not live, so the master-key create succeeded and left a note with no owner and a public ACL. Deploy, then delete the row (`migrate-acl.sh` in the lockdown-lab repo removes ownerless notes).

**`101` rows answer `200` in the `data` group** the class-level permission on `Note` has not reached every server yet. We have measured up to 22 minutes for a permission change to settle; re-run until three runs agree, and deploy Cloud Code once if it stalls.

**`python3: command not found`** the script reads the JSON bodies with Python 3. Install it, or replace the two `python3` calls with `jq -r`.

## When is branching on the numeric code the wrong move?

When the caller is not your own client. If you put an API of your own in front of the backend (an Express or FastAPI service in a container, say), its consumers should never see 101 or 209.

Map them at the boundary: 101 on login becomes your 401, 209 becomes your 401 with a *re-authenticate* hint, 119 becomes 403, 142 becomes 422 with the message, everything else 500 with the code in your logs only.

When the message was not written by you. The sentences behind 142, 206, 119-from-a-function and 141-from-a-throw are yours and can go on screen. `schema mismatch for Note.text; expected String but got Number` and `Tried to create an ACL with an invalid permission type.` are for developers; show a generic message and log the original.

When the code hides intent on purpose. 101 will not tell you whether a row exists, and the login will not tell you whether the username does. A client that tries to infer either from timing or from message wording is guessing, and a change on the backend will break the guess. If the product needs the distinction, put it in a Cloud Function for the callers who may know.

And when the HTTP status is the only signal. 401, 403, malformed JSON and unknown paths carry no code; a handler that starts with `body.code` throws on them. Check the status first, then the presence of `code`, then its value.

## What should you run against your own backend next?

- **`./reproduce.sh auth`** with your own `.env`. Compare the 29 rows with the table in this post; a row that differs is either a hook you have (good) or one you thought you had (not yet deployed).
- **Add a row per hook you write.** Every `throw new Parse.Error(...)` in your Cloud Code deserves a line in the script, so the message your users will see is one you have read.
- **One handler for 209.** Clear the token, show the login form, never retry. It is the code you will see most in production, and the one with the simplest fix.
- **Lock, then probe.** If you change a class-level permission, run the `data` group until three runs agree; the door closes on every server eventually, not at once. The lockdown of `Note` and the 22 minutes are in [Your Frontend Talks Straight to the Database. Here Is How to Lock It Down](https://www.back4app.com/blog/lock-down-a-backend-your-frontend-talks-to).
- **Accounts first.** The 101, 202, 203 and 209 rows come from the six REST calls of [How to Add Authentication to a Web or Flutter App Without Writing an Auth Server](https://www.back4app.com/blog/add-signup-login-password-reset-without-an-auth-server); if your app has no users class yet, that post is the previous step.
