POST /api/v1/moderation/flags/{flag_id}/action - Take Flag Action
Overview
This endpoint allows a moderator to change the status of a flag and optionally add notes explaining their decision. It is the core action endpoint for the moderation workflow — the moment a human reviewer makes a decision about flagged content.
Why it exists: A flag by itself is just a report. Action on a flag is what drives moderation outcomes: a video gets removed when a flag is approved, a flag gets dismissed when the content is acceptable. This endpoint records the moderator's decision, assigns them as the reviewer, and transitions the flag through the state machine.
HTTP Details
- Method: POST
- Path:
/api/v1/moderation/flags/{flag_id}/action - Auth Required: Yes (moderator role)
- Success Status: 200 OK
Path Parameters
| Parameter | Type | Description |
|---|---|---|
flag_id |
uuid | Unique identifier of the flag to act on |
Request Body
{
"status": "approved",
"moderatorNotes": "Confirmed spam. Video has been removed."
}
Field rules:
status: Required. Must be a valid FlagStatusEnum:open,under_review,approved,rejectedmoderatorNotes: Optional. Free text, max 1000 characters
Response Body (200)
{
"flagId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userId": "11111111-2222-3333-4444-555555555555",
"contentType": "video",
"contentId": "550e8400-e29b-41d4-a716-446655440000",
"reasonCode": "spam",
"reasonText": "Fake giveaway scam.",
"status": "approved",
"createdAt": "2025-11-01T14:22:00Z",
"updatedAt": "2025-11-02T09:15:00Z",
"moderatorId": "99999999-8888-7777-6666-555555555555",
"moderatorNotes": "Confirmed spam. Video has been removed.",
"resolvedAt": "2025-11-02T09:15:00Z"
}
Note that moderatorId is now populated with the acting moderator's ID, and resolvedAt is set when the flag reaches a terminal state (approved or rejected).
Cassandra Concepts Explained
State Machine Transitions
The status field on a flag follows a defined state machine:
┌────────────────┐
│ open │ ← Initial state (all new flags)
└───────┬────────┘
│ moderator claims
▼
┌────────────────┐
│ under_review │ ← Moderator is actively reviewing
└───────┬────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌─────────────┐
│ approved │ │ rejected │ ← Terminal states
└──────────────┘ └─────────────┘
The API does not enforce these transitions strictly — a moderator can technically jump straight from open to approved. However, the under_review state is valuable for team coordination: if two moderators both see an open flag, they can claim it by setting it to under_review before reviewing, preventing duplicate work.
Audit Trail
Every action on a flag is recorded. The flags table captures:
- Who acted (
moderatorId— the currently authenticated moderator) - What they decided (
statustransition +moderatorNotes) - When they decided (
updatedAt, andresolvedAtfor terminal states)
This audit trail is important for compliance, appeals, and moderator performance reviews.
Note: the current schema stores only the most recent moderator action. For a full audit history (every state change logged), you would need a separate flag_audit_log table. That is a natural extension for a more complex moderation system.
Moderator Assignment
When a moderator takes action, their userId (extracted from the JWT) is written to the moderatorId column. This serves two purposes:
- Accountability: You can see who made each decision
- Coordination: Other moderators can see that someone is already handling a flag
Partial Updates in Cassandra
Unlike SQL's UPDATE ... WHERE, Cassandra updates are actually upserts at the partition level. When updating a flag's status, you update only the columns you want to change:
UPDATE killrvideo.flags
SET status = 'approved',
moderatorid = <uuid>,
moderatornotes = 'Confirmed spam.',
updatedat = '2025-11-02T09:15:00Z',
resolvedat = '2025-11-02T09:15:00Z'
WHERE flagid = a1b2c3d4-e5f6-7890-abcd-ef1234567890;
Cassandra writes only the specified columns — untouched columns (like userid, contenttype, reasoncode) retain their existing values.
Data Model
Table: flags
CREATE TABLE killrvideo.flags (
flagid uuid PRIMARY KEY,
userid uuid,
contenttype text,
contentid uuid,
reasoncode text,
reasontext text,
status text, -- Updated by this endpoint
createdat timestamp,
updatedat timestamp, -- Updated by this endpoint
moderatorid uuid, -- Set to acting moderator's ID
moderatornotes text, -- Set by this endpoint
resolvedat timestamp -- Set when status is terminal
);
Database Queries
1. Fetch Existing Flag
async def get_flag_by_id(flag_id: str):
flags_table = await get_table("flags")
flag = await flags_table.find_one(filter={"flagid": flag_id})
if not flag:
raise HTTPException(status_code=404, detail="Flag not found")
return flag
Equivalent CQL:
SELECT * FROM killrvideo.flags WHERE flagid = a1b2c3d4-e5f6-7890-abcd-ef1234567890;
Performance: O(1) — direct partition key lookup
2. Update Flag with Moderator Action
async def apply_flag_action(
flag_id: str,
moderator_id: str,
action: FlagUpdateRequest
):
flags_table = await get_table("flags")
now = datetime.now(timezone.utc)
update_doc = {
"status": action.status,
"moderatorid": moderator_id,
"moderatornotes": action.moderatorNotes,
"updatedat": now.isoformat()
}
# Set resolvedAt only for terminal states
if action.status in ("approved", "rejected"):
update_doc["resolvedat"] = now.isoformat()
await flags_table.update_one(
filter={"flagid": flag_id},
update={"$set": update_doc}
)
Equivalent CQL:
UPDATE killrvideo.flags
SET status = 'approved',
moderatorid = 99999999-8888-7777-6666-555555555555,
moderatornotes = 'Confirmed spam. Video has been removed.',
updatedat = '2025-11-02T09:15:00Z',
resolvedat = '2025-11-02T09:15:00Z'
WHERE flagid = a1b2c3d4-e5f6-7890-abcd-ef1234567890;
Performance: O(1) — direct partition key update
Implementation Flow
┌─────────────────────────────────────────────────────────┐
│ 1. POST /api/v1/moderation/flags/{flag_id}/action │
│ {status, moderatorNotes?} │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. Auth middleware verifies JWT │
│ └─ Requires moderator role │
│ └─ Extracts moderatorId from token claims │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. Validate path parameter and request body │
│ ├─ flag_id must be valid UUID format │
│ ├─ status must be valid FlagStatusEnum │
│ └─ moderatorNotes length ≤ 1000 characters │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. Fetch flag to verify it exists │
│ └─ Returns 404 if flagId not found │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. Build update document │
│ ├─ Set status, moderatorId, moderatorNotes │
│ ├─ Set updatedAt = now() │
│ └─ If terminal status: set resolvedAt = now() │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 6. UPDATE flags WHERE flagid = ? │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 7. Return 200 with updated FlagResponse │
└─────────────────────────────────────────────────────────┘
Special Notes
1. Terminal State Detection
resolvedAt is only set when status moves to approved or rejected. If a moderator sets status to under_review, resolvedAt is left null (or the existing value is preserved if previously set — though logically a flag shouldn't re-open after resolution).
TERMINAL_STATUSES = {"approved", "rejected"}
if action.status in TERMINAL_STATUSES:
update_doc["resolvedat"] = now.isoformat()
2. Read-Then-Write Pattern
The endpoint fetches the flag before updating it. This serves two purposes:
- Verify the flag exists (return 404 if not)
- Return the full updated flag in the response (merge old fields with new)
This creates a small race window between the read and the write. For moderation workflows, this is generally acceptable — two moderators acting on the same flag simultaneously would result in the last write winning, which is a known trade-off.
3. No Downstream Side Effects Here
This endpoint only updates the flags table. Downstream effects (like actually removing flagged content) are expected to be triggered separately — either by a background job that processes approved flags, or by additional API calls from the moderator interface. Keeping this endpoint focused on flag state prevents partial-failure complexity.
4. Moderator Notes are Overwritten
If a moderator updates a flag multiple times (e.g., from open to under_review, then to approved), each update overwrites moderatorNotes. There is no history of intermediate notes. For a full audit trail, a separate flag_actions log table would be needed.
Developer Tips
Common Pitfalls
-
Not setting updatedAt: Always update
updatedAton every write. It is the timestamp consumers use to sort and detect changes. -
Forgetting resolvedAt logic:
resolvedAtshould only be set for terminal states. Setting it prematurely (e.g., onunder_review) creates confusing data. -
Trusting client-supplied moderatorId: Always extract
moderatorIdfrom the verified JWT, not from the request body. Never allow a moderator to attribute an action to a different user. -
Race conditions on concurrent updates: Two moderators could simultaneously claim
under_reviewon the same flag. For production, consider a lightweight transaction (LWT) withIF status = 'open'to ensure only one claim succeeds.
Query Performance Expectations
| Operation | Performance | Why |
|---|---|---|
| Fetch flag by ID | < 5ms | Partition key lookup |
| Update flag | < 5ms | Partition key write |
| Total action flow | < 15ms | Two partition key operations |
Related Endpoints
- GET /api/v1/moderation/flags - View the queue to find flags to act on
- GET /api/v1/moderation/flags/{flag_id} - Review flag details before acting
- POST /api/v1/flags - How flags are originally created