Hashtag Streams API¶
Manage Fediverse hashtag streams for monitoring and post collection.
Overview¶
Hashtag streams are the foundation of FenLiu's content collection. Each stream monitors posts with a specific hashtag on a Fediverse instance. FenLiu automatically fetches new posts from active streams and applies spam filtering and review workflows.
Stream Lifecycle¶
- Create: Define a hashtag and instance to monitor
- Active: Fetch posts periodically or on-demand
- Update: Modify instance or active status
- Delete: Stop monitoring (historical data retained)
Authentication¶
All Streams API endpoints require API key authentication via X-API-Key header:
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/hashtags
See Authentication Guide for details.
Endpoints¶
GET /api/v1/hashtags¶
List all hashtag streams with optional filtering.
Request
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/hashtags?skip=0&limit=50&active=true"
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) |
active |
boolean | - | Filter by active status (optional) |
Response (200 OK)
{
"items": [
{
"id": 1,
"hashtag": "python",
"instance": "mastodon.social",
"active": true,
"last_fetch": "2026-03-03T15:30:00Z",
"post_count": 245,
"created_at": "2026-02-15T10:00:00Z",
"updated_at": "2026-03-03T15:30:00Z"
},
{
"id": 2,
"hashtag": "django",
"instance": "fosstodon.org",
"active": true,
"last_fetch": "2026-03-03T14:20:00Z",
"post_count": 89,
"created_at": "2026-02-20T12:30:00Z",
"updated_at": "2026-03-03T14:20:00Z"
}
],
"total": 2,
"skip": 0,
"limit": 50
}
Response Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Stream identifier (use in other endpoints) |
hashtag |
string | Hashtag name (without #) |
instance |
string | Fediverse instance domain |
active |
boolean | Whether stream is actively fetching |
last_fetch |
string | ISO 8601 timestamp of last fetch |
post_count |
integer | Total posts collected from this stream |
created_at |
string | ISO 8601 stream creation time |
updated_at |
string | ISO 8601 last update time |
POST /api/v1/hashtags¶
Create a new hashtag stream.
Request
curl -X POST \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"hashtag": "python",
"instance": "mastodon.social",
"active": true
}' \
http://localhost:8000/api/v1/hashtags
Request Body
{
"hashtag": "python",
"instance": "mastodon.social",
"active": true
}
| Field | Type | Required | Description |
|---|---|---|---|
hashtag |
string | Yes | Hashtag to monitor (without #) |
instance |
string | Yes | Fediverse instance domain |
active |
boolean | No | Enable fetching (default: true) |
Response (201 Created)
{
"id": 3,
"hashtag": "python",
"instance": "mastodon.social",
"active": true,
"last_fetch": null,
"post_count": 0,
"created_at": "2026-03-03T16:00:00Z",
"updated_at": "2026-03-03T16:00:00Z"
}
Error Responses
{ "detail": "Stream already exists" } // 409 Conflict
{ "detail": "Invalid instance domain" } // 400 Bad Request
GET /api/v1/hashtags/{id}¶
Get details of a specific stream.
Request
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/hashtags/1
Response (200 OK)
{
"id": 1,
"hashtag": "python",
"instance": "mastodon.social",
"active": true,
"last_fetch": "2026-03-03T15:30:00Z",
"post_count": 245,
"created_at": "2026-02-15T10:00:00Z",
"updated_at": "2026-03-03T15:30:00Z"
}
Error Responses
{ "detail": "Stream not found" } // 404
PATCH /api/v1/hashtags/{id}¶
Update a stream (active status, etc.).
Request
curl -X PATCH \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"active": false}' \
http://localhost:8000/api/v1/hashtags/1
Request Body
{
"active": false
}
| Field | Type | Required | Description |
|---|---|---|---|
hashtag |
string | No | Hashtag name |
instance |
string | No | Instance domain |
active |
boolean | No | Enable/disable fetching |
Response (200 OK)
{
"id": 1,
"hashtag": "python",
"instance": "mastodon.social",
"active": false,
"last_fetch": "2026-03-03T15:30:00Z",
"post_count": 245,
"created_at": "2026-02-15T10:00:00Z",
"updated_at": "2026-03-03T16:15:00Z"
}
Error Responses
{ "detail": "Stream not found" } // 404
DELETE /api/v1/hashtags/{id}¶
Delete a stream (historical posts retained).
Request
curl -X DELETE \
-H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/hashtags/1
Response (204 No Content)
Error Responses
{ "detail": "Stream not found" } // 404
POST /api/v1/hashtags/{id}/fetch¶
Fetch posts for a specific stream from the Fediverse.
Request
curl -X POST \
-H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/hashtags/1/fetch?limit=20"
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Maximum posts to fetch (1-100) |
Response (200 OK)
{
"stream_id": 1,
"hashtag": "python",
"fetched_count": 18,
"added_count": 12,
"skipped_count": 6,
"errors": []
}
Response Fields
| Field | Type | Description |
|---|---|---|
stream_id |
integer | Stream that was fetched |
hashtag |
string | Hashtag name |
fetched_count |
integer | Posts retrieved from Fediverse |
added_count |
integer | New posts added to database |
skipped_count |
integer | Posts already in database |
errors |
array | Any errors during fetch |
Error Responses
{ "detail": "Stream not found" } // 404
{ "detail": "Instance unreachable" } // 503
POST /api/v1/hashtags/fetch-all¶
Fetch posts for all active streams simultaneously.
Request
curl -X POST \
-H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/hashtags/fetch-all?limit=20"
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Maximum posts per stream |
Response (200 OK)
{
"total_streams": 5,
"successful": 5,
"failed": 0,
"total_fetched": 87,
"total_added": 62,
"results": [
{
"stream_id": 1,
"hashtag": "python",
"fetched_count": 20,
"added_count": 15,
"status": "success"
},
{
"stream_id": 2,
"hashtag": "django",
"fetched_count": 18,
"added_count": 12,
"status": "success"
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
total_streams |
integer | Active streams processed |
successful |
integer | Streams fetched successfully |
failed |
integer | Streams that failed |
total_fetched |
integer | Total posts retrieved |
total_added |
integer | Total new posts added |
results |
array | Per-stream fetch results |
Examples¶
Python¶
import httpx
api_key = "your-api-key"
base_url = "http://localhost:8000/api/v1"
headers = {"X-API-Key": api_key}
# List all streams
async with httpx.AsyncClient() as client:
response = await client.get(f"{base_url}/hashtags", headers=headers)
streams = response.json()["items"]
# Create a new stream
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/hashtags",
json={
"hashtag": "python",
"instance": "mastodon.social",
"active": True
},
headers=headers
)
new_stream = response.json()
# Fetch posts for a stream
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/hashtags/1/fetch?limit=50",
headers=headers
)
result = response.json()
print(f"Added {result['added_count']} new posts")
JavaScript¶
const apiKey = "your-api-key";
const baseUrl = "http://localhost:8000/api/v1";
const headers = { "X-API-Key": apiKey };
// List streams
const streams = await fetch(`${baseUrl}/hashtags`, { headers })
.then(r => r.json());
// Create stream
const newStream = await fetch(`${baseUrl}/hashtags`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
hashtag: "python",
instance: "mastodon.social"
})
}).then(r => r.json());
// Fetch posts
const result = await fetch(
`${baseUrl}/hashtags/1/fetch?limit=50`,
{ method: "POST", headers }
).then(r => r.json());
cURL¶
export API_KEY="your-api-key"
export BASE_URL="http://localhost:8000/api/v1"
# List all streams
curl -H "X-API-Key: $API_KEY" "$BASE_URL/hashtags"
# Create stream
curl -X POST "$BASE_URL/hashtags" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"hashtag":"python","instance":"mastodon.social"}'
# Fetch posts
curl -X POST "$BASE_URL/hashtags/1/fetch?limit=50" \
-H "X-API-Key: $API_KEY"
# Fetch all streams
curl -X POST "$BASE_URL/hashtags/fetch-all?limit=20" \
-H "X-API-Key: $API_KEY"
Common Patterns¶
Monitor Multiple Instances¶
# Create streams for same hashtag on different instances
for instance in ["mastodon.social", "fosstodon.org", "pixelfed.social"]:
await client.post(
f"{base_url}/hashtags",
json={"hashtag": "python", "instance": instance},
headers=headers
)
Pause a Stream¶
# Disable fetching without deleting stream or posts
await client.patch(
f"{base_url}/hashtags/1",
json={"active": False},
headers=headers
)
Auto-Fetch Workflow¶
import asyncio
while True:
# Fetch all active streams
await client.post(f"{base_url}/hashtags/fetch-all?limit=20", headers=headers)
# Wait 1 hour before next fetch
await asyncio.sleep(3600)
Related APIs¶
- Posts API - Retrieve and manage fetched posts
- Review API - Approve/reject posts from streams
- Statistics API - Stream statistics and metrics
- Dashboard - Visual stream management
Best Practices¶
- Rate Limiting: Respect Fediverse instance rate limits; consider spacing out fetches
- Error Handling: Catch network errors and retry with exponential backoff
- Monitoring: Monitor
last_fetchtimestamp to detect stalled streams - Instance Selection: Use well-known, stable instances (mastodon.social, fosstodon.org)
- Hashtag Strategy: Monitor relevant hashtags; avoid ultra-popular ones (high volume, spam)