Mobile Content Moderation — Integration Guide
Blocked-word filtering is no longer chat-only. Every request body and query string the app sends is now checked server-side, so any screen with a text input can come back with a 422. One rejection shape, one handler. Chat's existing behaviour is unchanged — see
mobile-chat-moderation-guide.mdfor the composer-specific detail.
- API base URL (local):
http://localhost:3001 - Auth:
Authorization: Bearer <accessToken>on every endpoint that had it before
What's new
- 2026-09-07 — Blocked-word checking now covers every text field in the app, not just chat. Posts, comments, events, your profile, activities, achievements, school submissions, onboarding and the search box can all return 422
CONTENT_BLOCKEDwith afieldtelling you which input to highlight. Chat keepsMESSAGE_BLOCKEDexactly as before. Report descriptions are deliberately never filtered.
How it works in one paragraph
The server runs one gate in front of every route. It walks the whole request body (including nested objects and arrays) and the query string, and rejects the request if any user-typed value trips the blocklist. There is nothing to opt into and no new endpoint — existing endpoints simply gained a rejection case. The client-side list you already ship for the chat composer is still UX only; the server is the boundary and always re-checks.
The rejection shape
HTTP 422 Unprocessable Content — the request is well-formed, its content violates policy (not a 400 malformed request).
{
"success": false,
"error": "Please use kinder words.", // display-ready, kid-appropriate
"code": "CONTENT_BLOCKED", // switch on this, not the status code
"reason": "PROFANITY", // may gain values later
"field": "data.profile.bio", // dotted path into YOUR request body
}
field is the path of the offending value inside the request you sent —
"content", "title", "media[1].altText", "data.profile.bio", or "q" for
a query string. Use it to focus and outline the right input. It is the only new
property; everything else matches the chat contract you already handle.
Three codes exist. They differ only in copy — treat all three the same way:
| Code | Where |
|---|---|
MESSAGE_BLOCKED | POST /api/chat/conversations/:id/messages, PATCH /api/chat/messages/:messageId, POST /api/posts/:id/requests |
SCHOOL_NAME_BLOCKED | POST /api/schools |
CONTENT_BLOCKED | everywhere else |
If you only handle 422 generically, you are already correct. The two older codes are byte-identical to what shipped before (same status, code,
reasonand copy —fieldis purely additive), andCONTENT_BLOCKEDonly appears on endpoints that never returned a 422 at all. A screen with no special case falls through to its normal error path and showserror, which is written to be displayed as-is.
There is one more reason you can receive: UNSCANNABLE (no field). It
means the payload was too deep or too large for the server to inspect and was
rejected rather than waved through. A normal screen cannot produce it — if you
see it, you are sending something pathological. Treat it as a bug in the request.
Where it applies
Everything a user types, in the body or the query string:
| Surface | Fields |
|---|---|
POST /api/posts, PATCH /api/posts/:id | title, content, location, details |
POST /api/posts/:id/comments | content |
POST /api/events, PATCH /api/events/:id | title, description, location |
PATCH /api/users/me/profile | displayName, bio, location |
POST /api/auth/register | username, displayName |
POST/PATCH /api/users/me/activities, …/achievements, …/education | title, role, description, typeOther, shareCaption, schoolName, grade, city… |
POST /api/onboarding/session, PATCH …, POST …/complete | the whole nested data blob |
POST /api/chat/conversations, PATCH /api/chat/conversations/:id | group name |
POST /api/schools | name, state, district |
GET /api/search, mention search, school search, GIF search | q |
| any media payload | altText |
Search is gated too. A blocked query returns 422 instead of an empty result set — show the message rather than "no results".
Never moderated
- Passwords (
password,newPassword) and the loginidentifier. A password containing an awkward substring still works, and an existing account whose username predates this gate can still sign in. - Emails, OTP codes, tokens, cursors, storage keys, file names, ids, slugs, URLs and timestamps. Machine strings, not prose.
- Path parameters.
GET /api/users/:usernameis a read and is never gated. POST /api/reports→details. Deliberate: reporting bullying means quoting it. Never filter the report box in your UI either — let the child type what happened. Everything else about the endpoint is unchanged (auth, rate limit, 500-char cap).
What to build
- One shared 422 handler. On
status === 422with acodeending in_BLOCKED, showerrorand, iffieldis present, focus that input. - Keep the composer pre-check you already have for chat, and reuse the same
helper on the other big text inputs (post body, comment, bio, event
description) so the user gets instant feedback before the round trip. Copy
the
CLIENT_BLOCKED_TERMSblock and matcher frommobile-chat-moderation-guide.md— it is the same list, it is not chat-specific. - Roll back optimistic UI. A blocked request never reaches the handler: nothing is written, no socket event fires, no rate-limit budget is spent.
Don't
- Don't rely on the client check. It is a courtesy; the server 422 is the boundary.
- Don't echo the blocked word back to the user or into analytics. The response deliberately never tells you which word tripped.
- Don't reconstruct the server list. You only ever hold the small superficial one.
- Don't pre-filter the report box.
One ordering detail
401 wins. On any authenticated endpoint, a request with no Authorization
header gets the normal 401 — never a 422 — so a signed-out user is told to sign
in, not told to watch their language. The gate only ever fires for a caller who
presented a token.
The exception is the endpoints that are public by design — POST /api/auth/register
and the onboarding routes. Those are how an unauthenticated user submits text
(username, displayName, the onboarding data blob), so they stay gated with
no token and can return 422.
Reference
- Chat-specific behaviour and the composer list:
mobile-chat-moderation-guide.md - Reporting and blocking:
mobile-report-block-guide.md