Skip to content

Review API

Manage post review and approval workflows.

Overview

The Review API provides endpoints for approving, rejecting, and tracking review decisions on posts. FenLiu supports both manual review via the web interface and programmatic review via this API. All review decisions are tracked historically for learning and analytics.

Review Workflow

  1. Fetch: Posts are collected from streams
  2. Score: Automatic spam scoring applied
  3. Review: Human or API decision (approve/reject)
  4. Feedback: Decision tracked for analytics and future ML training
  5. Export: Approved posts moved to Curated Queue

Authentication

All Review API endpoints require API key authentication via X-API-Key header:

curl -H "X-API-Key: your-api-key" \
  http://localhost:8000/api/v1/posts/42

See Authentication Guide for details.

Endpoints

PATCH /api/v1/posts/{id}

Update post review status (approve/reject with optional notes).

This is the primary endpoint for review decisions. It records approval/rejection, optional spam score adjustments, and reviewer notes.

Request

curl -X PATCH \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": true,
    "reviewer_notes": "High-quality technical content",
    "manual_spam_score": 15
  }' \
  http://localhost:8000/api/v1/posts/42

Request Body

{
  "approved": true,
  "reviewer_notes": "High-quality technical content",
  "manual_spam_score": 15
}
Field Type Required Description
approved boolean No Approval decision (true=approve, false=reject)
manual_spam_score integer No Manually adjusted spam score (0-100)
reviewer_notes string No Notes explaining the decision

Response (200 OK)

{
  "id": 42,
  "post_id": "https://example.com/users/alice/statuses/123",
  "approved": true,
  "reviewed_at": "2026-03-03T12:00:00Z",
  "reviewer_notes": "High-quality technical content",
  "spam_score": 25,
  "manual_spam_score": 15,
  "queue_status": "pending"
}

Response Fields

Field Type Description
id integer Post identifier
post_id string ActivityPub URI
approved boolean Current approval status
reviewed_at string ISO 8601 review timestamp
reviewer_notes string Associated notes
spam_score integer Automatic spam score
manual_spam_score integer Manually adjusted score (null if not set)
queue_status string Curated Queue status

Error Responses

{ "detail": "Post not found" }  // 404
{ "detail": "Invalid spam score (0-100)" }  // 400

Review Decisions

Approve a Post

Mark a post as high-quality and eligible for export:

curl -X PATCH \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": true,
    "reviewer_notes": "Excellent content about Python async/await"
  }' \
  http://localhost:8000/api/v1/posts/42

When to Approve - Post is on-topic and relevant - Content is original or properly attributed - Author is trusted or has good history - No spam indicators - Adds value to the community

Reject a Post

Mark a post as low-quality or spam:

curl -X PATCH \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": false,
    "reviewer_notes": "Spam promotion with suspicious links",
    "manual_spam_score": 85
  }' \
  http://localhost:8000/api/v1/posts/42

When to Reject - Spam or advertising - Off-topic for the stream - Misleading or false information - Excessive self-promotion - Low-effort content (e.g., just hashtags) - Policy violations

Adjust Spam Score

Override automatic scoring with manual adjustment:

curl -X PATCH \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "manual_spam_score": 10
  }' \
  http://localhost:8000/api/v1/posts/42

Use this when the automatic score doesn't match the actual quality. For example: - Automatic score is 70 (high spam) but content is actually quality → set to 20 - Automatic score is 20 (low spam) but content is spam → set to 85


Review Patterns

Bulk Review via API

import httpx

api_key = "your-api-key"
base_url = "http://localhost:8000/api/v1"
headers = {"X-API-Key": api_key}

# Get posts awaiting review
async with httpx.AsyncClient() as client:
    response = await client.get(
        f"{base_url}/posts?reviewed=false&limit=100",
        headers=headers
    )
    unreviewed = response.json()["items"]

    # Review each post
    for post in unreviewed:
        # Your review logic here
        approval = determine_approval(post)

        # Submit review
        await client.patch(
            f"{base_url}/posts/{post['id']}",
            json={
                "approved": approval,
                "reviewer_notes": f"Reviewed via bulk API"
            },
            headers=headers
        )

Conditional Review Based on Score

# Approve low-spam, reject high-spam, flag medium for manual review
async with httpx.AsyncClient() as client:
    for post in unreviewed:
        score = post["spam_score"]

        if score < 30:
            await client.patch(
                f"{base_url}/posts/{post['id']}",
                json={"approved": True},
                headers=headers
            )
        elif score > 70:
            await client.patch(
                f"{base_url}/posts/{post['id']}",
                json={
                    "approved": False,
                    "reviewer_notes": f"Auto-rejected (score: {score})"
                },
                headers=headers
            )
        # else: score 30-70, skip for manual review

Consensus Review

# Approve only if multiple signals indicate quality
def should_approve(post):
    # Low spam score
    if post["spam_score"] > 50:
        return False

    # Has media attachments (higher quality)
    if not post["media_attachments"]:
        return False

    # Multiple relevant hashtags
    if len(post["hashtags"]) < 2:
        return False

    return True

async with httpx.AsyncClient() as client:
    for post in unreviewed:
        if should_approve(post):
            await client.patch(
                f"{base_url}/posts/{post['id']}",
                json={"approved": True},
                headers=headers
            )

Examples

Python

import httpx

api_key = "your-api-key"
base_url = "http://localhost:8000/api/v1"
headers = {"X-API-Key": api_key}

# Approve a post with notes
async with httpx.AsyncClient() as client:
    response = await client.patch(
        f"{base_url}/posts/42",
        json={
            "approved": True,
            "reviewer_notes": "Excellent Python content"
        },
        headers=headers
    )
    result = response.json()
    print(f"Post {result['id']} approved")

# Reject a post
async with httpx.AsyncClient() as client:
    response = await client.patch(
        f"{base_url}/posts/43",
        json={
            "approved": False,
            "manual_spam_score": 90,
            "reviewer_notes": "Spam promotion"
        },
        headers=headers
    )

# Get unreviewed posts
async with httpx.AsyncClient() as client:
    response = await client.get(
        f"{base_url}/posts?reviewed=false&limit=50",
        headers=headers
    )
    unreviewed = response.json()["items"]
    print(f"Found {len(unreviewed)} posts to review")

JavaScript

const apiKey = "your-api-key";
const baseUrl = "http://localhost:8000/api/v1";
const headers = { "X-API-Key": apiKey };

// Approve a post
const approved = await fetch(
  `${baseUrl}/posts/42`,
  {
    method: "PATCH",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      approved: true,
      reviewer_notes: "Excellent Python content"
    })
  }
).then(r => r.json());

// Reject with high spam score
const rejected = await fetch(
  `${baseUrl}/posts/43`,
  {
    method: "PATCH",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      approved: false,
      manual_spam_score: 90,
      reviewer_notes: "Spam promotion"
    })
  }
).then(r => r.json());

// Get unreviewed posts
const unreviewed = await fetch(
  `${baseUrl}/posts?reviewed=false&limit=50`,
  { headers }
).then(r => r.json());

cURL

export API_KEY="your-api-key"
export BASE_URL="http://localhost:8000/api/v1"

# Approve a post
curl -X PATCH "$BASE_URL/posts/42" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": true,
    "reviewer_notes": "Excellent content"
  }'

# Reject with spam score
curl -X PATCH "$BASE_URL/posts/43" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": false,
    "manual_spam_score": 90,
    "reviewer_notes": "Spam promotion"
  }'

# Get unreviewed posts
curl "$BASE_URL/posts?reviewed=false&limit=50" \
  -H "X-API-Key: $API_KEY"

# Adjust score without changing approval
curl -X PATCH "$BASE_URL/posts/44" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"manual_spam_score": 5}'

Query Filters

Use the Posts API with filters to find posts for review:

# Unreviewed posts
curl "http://localhost:8000/api/v1/posts?reviewed=false" \
  -H "X-API-Key: your-api-key"

# High-spam posts (flagged for review)
curl "http://localhost:8000/api/v1/posts?spam_score_min=60&reviewed=false" \
  -H "X-API-Key: your-api-key"

# Recent posts from specific stream
curl "http://localhost:8000/api/v1/posts?stream_id=1&reviewed=false" \
  -H "X-API-Key: your-api-key"

# Approved posts (for quality control)
curl "http://localhost:8000/api/v1/posts?approved=true" \
  -H "X-API-Key: your-api-key"

Review Feedback Tracking

All review decisions are automatically tracked in the database. This historical data enables:

  • Learning: Future machine learning models can be trained on review patterns
  • Analytics: Track reviewer agreement, spam detection accuracy
  • Audit Trail: See who approved/rejected and when
  • Iteration: Improve filters based on review outcomes

Best Practices

For Manual Review

  1. Read the Content: Don't rely solely on spam score
  2. Check the Author: Look at account history/patterns
  3. Verify Links: Suspicious URLs are often spam
  4. Add Notes: Always explain your decision
  5. Be Consistent: Apply same standards across reviews

For Automated Review

  1. Score Range Strategy:
  2. < 30: Auto-approve (high confidence)
  3. 30-70: Manual review (uncertain)
  4. 70: Auto-reject (likely spam)

  5. Adjust Scores Carefully: Manual overrides should be documented

  6. Monitor Accuracy: Track approval/rejection rates over time
  7. Iterate: Adjust detection rules based on misclassifications

Review Queue Management

  1. Regular Scheduling: Set aside time for reviews
  2. Batch Processing: Review multiple posts efficiently
  3. Priority: Review high-quality content first
  4. Feedback Loop: Use review decisions to improve detection rules


Review Interface

For a more interactive review experience, use the Review Interface web page to: - Visually browse posts - See spam score breakdowns - Read full content with formatting - Approve/reject with single click - View review history and notes