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¶
- Fetched: Post retrieved from Fediverse (via Streams API)
- Scored: Spam score calculated automatically
- Reviewed: Human review (approve/reject) or auto-rejected by filters
- 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 |
Related APIs¶
- Streams API - Source streams
- Review API - Manual review workflow
- Curated Queue API - Export posts
- Statistics API - Post metrics
Best Practices¶
- Pagination: Always use
skipandlimitfor large result sets - Filtering: Use query parameters to reduce data transfer
- Score Analysis: Check
scoreendpoint before manual adjustments - Batch Operations: Use batch-score for multiple posts
- Monitoring: Track
queue_statusfor export pipeline health