Skip to content

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

  1. Create: Define a hashtag and instance to monitor
  2. Active: Fetch posts periodically or on-demand
  3. Update: Modify instance or active status
  4. 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)


Best Practices

  1. Rate Limiting: Respect Fediverse instance rate limits; consider spacing out fetches
  2. Error Handling: Catch network errors and retry with exponential backoff
  3. Monitoring: Monitor last_fetch timestamp to detect stalled streams
  4. Instance Selection: Use well-known, stable instances (mastodon.social, fosstodon.org)
  5. Hashtag Strategy: Monitor relevant hashtags; avoid ultra-popular ones (high volume, spam)