Skip to content

Posts API

Retrieve, filter, and manage posts collected from Fediverse streams.

Overview

The Posts API provides full access to FenLiu's post database. Posts are collected from monitored hashtag streams and can be filtered, scored, reviewed, and exported. Each post contains metadata about its source, content, spam score, and review status.

Post Lifecycle

  1. Fetched: Post retrieved from Fediverse (via Streams API)
  2. Scored: Spam score calculated automatically
  3. Reviewed: Human review (approve/reject) or auto-rejected by filters
  4. Exported: Post delivered via Curated Queue API

Authentication

All Posts 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

See Authentication Guide for details.

Endpoints

GET /api/v1/posts

List posts with advanced filtering and pagination.

Request

curl -H "X-API-Key: your-api-key" \
  "http://localhost:8000/api/v1/posts?skip=0&limit=50&approved=true&spam_score_min=0&spam_score_max=50"

Query Parameters

Parameter Type Default Description
skip integer 0 Number of items to skip (pagination)
limit integer 100 Number of items to return (max 1000)
stream_id integer - Filter by source stream (optional)
approved boolean - Filter by approval status (optional)
reviewed boolean - Filter by review status (optional)
spam_score_min integer 0 Minimum spam score (0-100)
spam_score_max integer 100 Maximum spam score (0-100)
queue_status string - Filter by queue status: pending, reserved, delivered, error

Response (200 OK)

{
  "items": [
    {
      "id": 42,
      "post_id": "https://example.com/users/alice/statuses/123",
      "url": "https://example.com/@alice/123",
      "content": "<p>Check out this amazing post!</p>",
      "author_username": "alice",
      "author_id": "https://example.com/users/alice",
      "instance": "example.com",
      "created_at": "2026-03-02T10:30:00Z",
      "reviewed_at": "2026-03-02T11:00:00Z",
      "approved": true,
      "spam_score": 25,
      "manual_spam_score": null,
      "reviewer_notes": "High-quality content",
      "hashtags": ["python", "programming"],
      "media_attachments": [
        {
          "type": "image",
          "url": "https://example.com/media/abc123.jpg"
        }
      ],
      "queue_status": "pending",
      "stream_id": 1,
      "stream_hashtag": "python"
    }
  ],
  "total": 150,
  "skip": 0,
  "limit": 50
}

Response Fields

Field Type Description
id integer FenLiu post identifier
post_id string ActivityPub URI (canonical identifier)
url string Human-readable web URL
content string Post content (HTML)
author_username string Fediverse username
author_id string ActivityPub author URI
instance string Source instance domain
created_at string ISO 8601 post creation time
reviewed_at string ISO 8601 review time (null if not reviewed)
approved boolean Whether post was approved
spam_score integer Automatic spam score (0-100)
manual_spam_score integer Manually adjusted score (null if not adjusted)
reviewer_notes string Notes from reviewer
hashtags array Hashtags in post content
media_attachments array Images, videos, attachments
queue_status string Curated Queue status
stream_id integer Source stream identifier
stream_hashtag string Source hashtag name

GET /api/v1/posts/{id}

Get detailed information about a specific post.

Request

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

Response (200 OK)

{
  "id": 42,
  "post_id": "https://example.com/users/alice/statuses/123",
  "url": "https://example.com/@alice/123",
  "content": "<p>Check out this amazing post!</p>",
  "author_username": "alice",
  "author_id": "https://example.com/users/alice",
  "instance": "example.com",
  "created_at": "2026-03-02T10:30:00Z",
  "reviewed_at": "2026-03-02T11:00:00Z",
  "approved": true,
  "spam_score": 25,
  "manual_spam_score": null,
  "reviewer_notes": "High-quality content",
  "hashtags": ["python", "programming"],
  "media_attachments": [
    {
      "type": "image",
      "url": "https://example.com/media/abc123.jpg"
    }
  ],
  "queue_status": "pending",
  "stream_id": 1,
  "stream_hashtag": "python"
}

Error Responses

{ "detail": "Post not found" }  // 404

PATCH /api/v1/posts/{id}

Update a post (review, approve, adjust score, etc.).

Request

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

Request Body

{
  "approved": true,
  "manual_spam_score": 10,
  "reviewer_notes": "Excellent post"
}
Field Type Required Description
approved boolean No Set approval status
manual_spam_score integer No Manually adjust spam score (0-100)
reviewer_notes string No Add reviewer notes

Response (200 OK)

{
  "id": 42,
  "post_id": "https://example.com/users/alice/statuses/123",
  "approved": true,
  "spam_score": 25,
  "manual_spam_score": 10,
  "reviewer_notes": "Excellent post",
  "reviewed_at": "2026-03-03T12:00:00Z",
  "updated_at": "2026-03-03T12:00:00Z"
}

Error Responses

{ "detail": "Post not found" }  // 404

DELETE /api/v1/posts/{id}

Delete a post from the database.

Request

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

Response (204 No Content)

Error Responses

{ "detail": "Post not found" }  // 404

GET /api/v1/posts/{id}/score

Get detailed spam score analysis for a post.

Request

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

Response (200 OK)

{
  "post_id": 42,
  "automatic_score": 25,
  "manual_score": null,
  "final_score": 25,
  "rules": {
    "all_caps": 0,
    "repeated_characters": 0,
    "spam_keywords": 5,
    "suspicious_links": 0,
    "excessive_hashtags": 10,
    "external_links": 5,
    "media_spam": 5
  },
  "rules_triggered": [
    "spam_keywords",
    "excessive_hashtags",
    "external_links",
    "media_spam"
  ]
}

Response Fields

Field Type Description
post_id integer Post identifier
automatic_score integer Score from detection rules
manual_score integer Manually set score (null if not set)
final_score integer Effective score (manual overrides automatic)
rules object Individual rule scores
rules_triggered array Names of rules that triggered

POST /api/v1/posts/{id}/recalculate

Recalculate spam score for a post (regenerates from rules).

Request

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

Response (200 OK)

{
  "post_id": 42,
  "old_score": 25,
  "new_score": 30,
  "changes": {
    "spam_keywords": 5,
    "excessive_hashtags": 15
  }
}

Error Responses

{ "detail": "Post not found" }  // 404

POST /api/v1/posts/batch-score

Get spam scores for multiple posts in one request.

Request

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "post_ids": [42, 43, 44]
  }' \
  http://localhost:8000/api/v1/posts/batch-score

Request Body

{
  "post_ids": [42, 43, 44]
}
Field Type Required Description
post_ids array Yes List of post IDs (max 100)

Response (200 OK)

{
  "results": [
    {
      "post_id": 42,
      "automatic_score": 25,
      "manual_score": null,
      "final_score": 25
    },
    {
      "post_id": 43,
      "automatic_score": 75,
      "manual_score": 80,
      "final_score": 80
    },
    {
      "post_id": 44,
      "automatic_score": 10,
      "manual_score": null,
      "final_score": 10
    }
  ],
  "processed": 3,
  "not_found": 0
}

Filtering Examples

Filter by Approval Status

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

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

Filter by Spam Score

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

# Low spam (0-25)
curl "http://localhost:8000/api/v1/posts?spam_score_min=0&spam_score_max=25" \
  -H "X-API-Key: your-api-key"

Filter by Stream

# Posts from stream #1 only
curl "http://localhost:8000/api/v1/posts?stream_id=1" \
  -H "X-API-Key: your-api-key"

Filter by Review Status

# Posts that have been reviewed
curl "http://localhost:8000/api/v1/posts?reviewed=true" \
  -H "X-API-Key: your-api-key"

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

Filter by Queue Status

# Pending posts (ready for export)
curl "http://localhost:8000/api/v1/posts?queue_status=pending" \
  -H "X-API-Key: your-api-key"

# Currently reserved posts
curl "http://localhost:8000/api/v1/posts?queue_status=reserved" \
  -H "X-API-Key: your-api-key"

# Posts that failed export
curl "http://localhost:8000/api/v1/posts?queue_status=error" \
  -H "X-API-Key: your-api-key"

Examples

Python

import httpx

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

# Get approved low-spam posts
async with httpx.AsyncClient() as client:
    response = await client.get(
        f"{base_url}/posts",
        params={
            "approved": True,
            "spam_score_max": 50,
            "limit": 100
        },
        headers=headers
    )
    posts = response.json()["items"]

# Review a post (approve)
async with httpx.AsyncClient() as client:
    response = await client.patch(
        f"{base_url}/posts/42",
        json={
            "approved": True,
            "reviewer_notes": "Quality content"
        },
        headers=headers
    )
    updated_post = response.json()

# Get score analysis
async with httpx.AsyncClient() as client:
    response = await client.get(
        f"{base_url}/posts/42/score",
        headers=headers
    )
    score_info = response.json()
    print(f"Score: {score_info['final_score']}")
    print(f"Triggered rules: {score_info['rules_triggered']}")

JavaScript

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

// List approved posts
const posts = await fetch(
  `${baseUrl}/posts?approved=true&spam_score_max=50&limit=50`,
  { headers }
).then(r => r.json());

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

// Batch score multiple posts
const scores = await fetch(
  `${baseUrl}/posts/batch-score`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ post_ids: [42, 43, 44] })
  }
).then(r => r.json());

cURL

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

# Get approved posts
curl "$BASE_URL/posts?approved=true&spam_score_max=50" \
  -H "X-API-Key: $API_KEY"

# Get specific post
curl "$BASE_URL/posts/42" \
  -H "X-API-Key: $API_KEY"

# 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":"Quality"}'

# Get score analysis
curl "$BASE_URL/posts/42/score" \
  -H "X-API-Key: $API_KEY"

# Batch score
curl -X POST "$BASE_URL/posts/batch-score" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_ids":[42,43,44]}'

Spam Score Details

The spam score ranges from 0 (high quality) to 100 (definite spam). The score is calculated using 7 detection rules:

Rule Range Description
all_caps 0-15 Excessive uppercase letters
repeated_characters 0-10 Repeated characters (e.g., "!!!")
spam_keywords 0-20 Common spam words
suspicious_links 0-15 Shortened URLs, suspicious domains
excessive_hashtags 0-15 Too many hashtags
external_links 0-10 Links to external sites
media_spam 0-15 Suspicious media or bot-generated content


Best Practices

  1. Pagination: Always use skip and limit for large result sets
  2. Filtering: Use query parameters to reduce data transfer
  3. Score Analysis: Check score endpoint before manual adjustments
  4. Batch Operations: Use batch-score for multiple posts
  5. Monitoring: Track queue_status for export pipeline health