Reblog Controls API¶
Manage export filters and blocklists for the Curated Queue.
Overview¶
The Reblog Controls API provides fine-grained control over which posts are eligible for export via the Curated Queue. You can block specific users, hashtags, and configure export settings (media requirements, etc.). These filters are applied after the manual review/approval workflow, ensuring only posts matching your curation criteria reach external consumers.
Filter Architecture¶
Approved Posts → Export Filters → Curated Queue → External Tools (e.g., Zhongli)
↓
Blocked Users
Blocked Hashtags
Media Requirements
Other Settings
Authentication¶
All Reblog Controls API endpoints require API key authentication via X-API-Key header:
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/reblog-controls/settings
See Authentication Guide for details.
Endpoints¶
GET /api/v1/reblog-controls/settings¶
Get current export filter settings.
Request
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/reblog-controls/settings
Response (200 OK)
{
"require_media": false,
"auto_reject_blocked_users": true,
"auto_reject_blocked_hashtags": true,
"created_at": "2026-02-15T10:00:00Z",
"updated_at": "2026-03-03T14:30:00Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
require_media |
boolean | Only export posts with media attachments |
auto_reject_blocked_users |
boolean | Auto-reject posts from blocked users during fetch |
auto_reject_blocked_hashtags |
boolean | Auto-reject posts with blocked hashtags during fetch |
created_at |
string | ISO 8601 creation timestamp |
updated_at |
string | ISO 8601 last update timestamp |
PUT /api/v1/reblog-controls/settings¶
Update export filter settings.
Request
curl -X PUT \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"require_media": true,
"auto_reject_blocked_users": true,
"auto_reject_blocked_hashtags": true
}' \
http://localhost:8000/api/v1/reblog-controls/settings
Request Body
{
"require_media": true,
"auto_reject_blocked_users": true,
"auto_reject_blocked_hashtags": true
}
| Field | Type | Required | Description |
|---|---|---|---|
require_media |
boolean | No | Require media attachments for export |
auto_reject_blocked_users |
boolean | No | Auto-reject on fetch |
auto_reject_blocked_hashtags |
boolean | No | Auto-reject on fetch |
Response (200 OK)
{
"require_media": true,
"auto_reject_blocked_users": true,
"auto_reject_blocked_hashtags": true,
"updated_at": "2026-03-03T15:00:00Z"
}
GET /api/v1/reblog-controls/blocked-users¶
List all blocked users.
Request
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/reblog-controls/blocked-users?skip=0&limit=50"
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
integer | 0 | Number of items to skip |
limit |
integer | 100 | Number of items to return |
Response (200 OK)
{
"items": [
{
"id": 1,
"account_id": "https://example.com/users/spammer",
"username": "spammer",
"instance": "example.com",
"reason": "Persistent spam promoter",
"created_at": "2026-02-20T12:00:00Z"
},
{
"id": 2,
"account_id": "https://badactors.com/users/troll",
"username": "troll",
"instance": "badactors.com",
"reason": "Harassment and misinformation",
"created_at": "2026-02-25T09:30:00Z"
}
],
"total": 2,
"skip": 0,
"limit": 50
}
Response Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Blocklist entry identifier |
account_id |
string | ActivityPub user URI |
username |
string | Fediverse username |
instance |
string | Instance domain |
reason |
string | Why user is blocked |
created_at |
string | ISO 8601 block time |
POST /api/v1/reblog-controls/blocked-users¶
Add a user to the blocklist.
Request
curl -X POST \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"account_id": "https://example.com/users/spammer",
"reason": "Persistent spam promoter"
}' \
http://localhost:8000/api/v1/reblog-controls/blocked-users
Request Body
{
"account_id": "https://example.com/users/spammer",
"reason": "Persistent spam promoter"
}
| Field | Type | Required | Description |
|---|---|---|---|
account_id |
string | Yes | Full ActivityPub user URI or @user@instance format |
reason |
string | No | Blocking reason (for records) |
Response (201 Created)
{
"id": 3,
"account_id": "https://example.com/users/spammer",
"username": "spammer",
"instance": "example.com",
"reason": "Persistent spam promoter",
"created_at": "2026-03-03T15:30:00Z"
}
Error Responses
{ "detail": "User already blocked" } // 409
{ "detail": "Invalid account format" } // 400
DELETE /api/v1/reblog-controls/blocked-users/{id}¶
Remove a user from the blocklist.
Request
curl -X DELETE \
-H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/reblog-controls/blocked-users/1
Response (204 No Content)
Error Responses
{ "detail": "Blocked user not found" } // 404
GET /api/v1/reblog-controls/blocked-hashtags¶
List all blocked hashtags.
Request
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/reblog-controls/blocked-hashtags?skip=0&limit=50"
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
integer | 0 | Number of items to skip |
limit |
integer | 100 | Number of items to return |
Response (200 OK)
{
"items": [
{
"id": 1,
"hashtag": "cryptocurrency",
"reason": "Off-topic spam promotion",
"created_at": "2026-02-18T14:00:00Z"
},
{
"id": 2,
"hashtag": "buymecoffe",
"reason": "Commercial spam",
"created_at": "2026-02-20T10:15:00Z"
}
],
"total": 2,
"skip": 0,
"limit": 50
}
Response Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Blocklist entry identifier |
hashtag |
string | Hashtag name (without #) |
reason |
string | Why hashtag is blocked |
created_at |
string | ISO 8601 block time |
POST /api/v1/reblog-controls/blocked-hashtags¶
Add a hashtag to the blocklist.
Request
curl -X POST \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"hashtag": "cryptocurrency",
"reason": "Off-topic spam promotion"
}' \
http://localhost:8000/api/v1/reblog-controls/blocked-hashtags
Request Body
{
"hashtag": "cryptocurrency",
"reason": "Off-topic spam promotion"
}
| Field | Type | Required | Description |
|---|---|---|---|
hashtag |
string | Yes | Hashtag to block (without #) |
reason |
string | No | Blocking reason |
Response (201 Created)
{
"id": 3,
"hashtag": "cryptocurrency",
"reason": "Off-topic spam promotion",
"created_at": "2026-03-03T15:45:00Z"
}
Error Responses
{ "detail": "Hashtag already blocked" } // 409
DELETE /api/v1/reblog-controls/blocked-hashtags/{id}¶
Remove a hashtag from the blocklist.
Request
curl -X DELETE \
-H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/reblog-controls/blocked-hashtags/1
Response (204 No Content)
Error Responses
{ "detail": "Blocked hashtag not found" } // 404
Examples¶
Python¶
import httpx
api_key = "your-api-key"
base_url = "http://localhost:8000/api/v1/reblog-controls"
headers = {"X-API-Key": api_key}
# Get current settings
async with httpx.AsyncClient() as client:
response = await client.get(f"{base_url}/settings", headers=headers)
settings = response.json()
print(f"Require media: {settings['require_media']}")
# Block a user
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/blocked-users",
json={
"account_id": "@spammer@example.com",
"reason": "Persistent spam"
},
headers=headers
)
blocked = response.json()
print(f"Blocked user {blocked['username']}")
# List blocked users
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/blocked-users",
headers=headers
)
users = response.json()["items"]
print(f"Total blocked users: {len(users)}")
# Block a hashtag
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/blocked-hashtags",
json={
"hashtag": "nsfw",
"reason": "Not suitable for feed"
},
headers=headers
)
blocked = response.json()
print(f"Blocked hashtag: #{blocked['hashtag']}")
# Update settings
async with httpx.AsyncClient() as client:
response = await client.put(
f"{base_url}/settings",
json={"require_media": True},
headers=headers
)
updated = response.json()
print(f"Updated settings")
JavaScript¶
const apiKey = "your-api-key";
const baseUrl = "http://localhost:8000/api/v1/reblog-controls";
const headers = { "X-API-Key": apiKey };
// Get settings
const settings = await fetch(`${baseUrl}/settings`, { headers })
.then(r => r.json());
// Block a user
const blockedUser = await fetch(
`${baseUrl}/blocked-users`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
account_id: "@spammer@example.com",
reason: "Persistent spam"
})
}
).then(r => r.json());
// List blocked users
const blockedUsers = await fetch(
`${baseUrl}/blocked-users`,
{ headers }
).then(r => r.json());
// Block a hashtag
const blockedHashtag = await fetch(
`${baseUrl}/blocked-hashtags`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
hashtag: "nsfw",
reason: "Not suitable"
})
}
).then(r => r.json());
// Update settings
const updated = await fetch(
`${baseUrl}/settings`,
{
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ require_media: true })
}
).then(r => r.json());
cURL¶
export API_KEY="your-api-key"
export BASE_URL="http://localhost:8000/api/v1/reblog-controls"
# Get settings
curl -H "X-API-Key: $API_KEY" "$BASE_URL/settings"
# Block a user
curl -X POST "$BASE_URL/blocked-users" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "@spammer@example.com",
"reason": "Persistent spam"
}'
# List blocked users
curl -H "X-API-Key: $API_KEY" "$BASE_URL/blocked-users"
# Remove blocked user
curl -X DELETE "$BASE_URL/blocked-users/1" \
-H "X-API-Key: $API_KEY"
# Block a hashtag
curl -X POST "$BASE_URL/blocked-hashtags" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"hashtag":"nsfw","reason":"Not suitable"}'
# List blocked hashtags
curl -H "X-API-Key: $API_KEY" "$BASE_URL/blocked-hashtags"
# Remove blocked hashtag
curl -X DELETE "$BASE_URL/blocked-hashtags/1" \
-H "X-API-Key: $API_KEY"
# Update settings
curl -X PUT "$BASE_URL/settings" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"require_media":true}'
Filter Strategies¶
Strategy 1: Permissive (Default)¶
- Block only known bad actors and obvious spam hashtags
- Allow most approved posts through
- Good for discovering new quality content
# Configuration
require_media: false
auto_reject_blocked_users: true
auto_reject_blocked_hashtags: true
blocked_users: [known spammers]
blocked_hashtags: [obvious spam hashtags]
Strategy 2: Restrictive¶
- Require media for all exports
- Block more users/hashtags
- Good for high-quality curation
# Configuration
require_media: true
auto_reject_blocked_users: true
auto_reject_blocked_hashtags: true
blocked_users: [many accounts]
blocked_hashtags: [broad categories]
Strategy 3: Custom (Per-Stream)¶
Use separate FenLiu instances with different settings for different purposes: - Instance A: High-volume discovery (permissive) - Instance B: Quality flagship content (restrictive)
Common Patterns¶
Block a Category of Users¶
# Block all cryptocurrency scam accounts
for account in @cryptoscam1@spam.com @cryptoscam2@badactors.com; do
curl -X POST "$BASE_URL/blocked-users" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"account_id\": \"$account\", \"reason\": \"Cryptocurrency scam\"}"
done
Media-Only Export¶
# Only export posts with images/videos
curl -X PUT "$BASE_URL/settings" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"require_media": true}'
Temporary Block¶
# Block a hashtag temporarily
curl -X POST "$BASE_URL/blocked-hashtags" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"hashtag": "temporary", "reason": "Blocked during event"}'
# Later, unblock when done
curl -X DELETE "$BASE_URL/blocked-hashtags/123" \
-H "X-API-Key: $API_KEY"
Related Concepts¶
Auto-Reject During Fetch¶
When auto_reject_blocked_users or auto_reject_blocked_hashtags is enabled:
- Posts are fetched from Fediverse
- Posts from blocked users → rejected automatically
- Posts with blocked hashtags → rejected automatically
- Remaining posts → added to database for review
This saves review effort by filtering known-bad content at fetch time.
Manual Review vs. Filters¶
- Manual Review: Humans approve/reject individual posts (flexible)
- Auto Filters: Block entire users/hashtags (efficient for spam)
- Combined: Use both for defense-in-depth approach
Related APIs¶
- Posts API - View filtered posts
- Curated Queue API - Posts reaching external consumers
- Review API - Manual approval decisions
- Settings UI - Web interface for filters
Best Practices¶
- Start Permissive: Begin with minimal blocks, add as needed
- Document Reasons: Record why users/hashtags are blocked
- Regular Review: Periodically review blocklist for false positives
- Gradual Escalation: Consider suspending users before blocking
- Community Input: Consider feedback from reviewers when blocking
- Monitor Impact: Track how filters affect post volume and quality