List Posts
Retrieve a paginated list of all your scheduled, draft, published, and failed posts.
Endpoint
GET https://api.postpost.dev/api/v1/list-postsHeaders
| Header | Required | Description |
|---|---|---|
x-api-key | Yes | Your API key |
x-postpost-user-id | No | Managed user ID (workspace only) |
x-postpost-client | No | Client identifier (e.g., mcp for MCP integrations). Affects which access controls are checked. |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (1-indexed). Values less than 1 are silently clamped to 1. |
limit | number | 20 | Items per page. Values are clamped to the range 1–100, with falsy values (0, NaN, empty) defaulting to 20. |
status | string | all | Filter by status: draft, scheduled, published, failed, partially_published. Note: pending and processing are valid per-platform ScheduledPost statuses (not ScheduledPostGroup statuses) and cannot be used as filter values here. |
platform | string | all | Filter by platform: twitter, linkedin, instagram, threads, tiktok, youtube, facebook, bluesky, mastodon, telegram, pinterest. Internally, the filter applies a case-insensitive regex against stored compound platform IDs (e.g., twitter-123). The filter uses a regex anchored to the start of the string (^platform-), so passing a bare platform name like twitter matches all Twitter connections. Passing a full compound ID like twitter-123 would generate regex ^twitter-123- which requires a trailing dash and will likely return no results — use bare platform names instead. No validation is performed on the value — invalid names (e.g., ?platform=foobar) silently return an empty result set rather than an error. |
sortBy | string | createdAt | Sort field: createdAt, scheduledTime, updatedAt |
sortOrder | string | desc | Sort order: asc or desc. Invalid values silently default to desc. |
fromDate | string | - | Filter posts scheduled on or after this date (inclusive, uses $gte). Accepts any date string parseable by JavaScript's new Date(), though ISO 8601 is recommended for consistency. Filters on scheduledTime only — drafts without a scheduledTime are excluded when this parameter is used. |
toDate | string | - | Filter posts scheduled on or before this date (inclusive, uses $lte). Accepts any date string parseable by JavaScript's new Date(), though ISO 8601 is recommended for consistency. Filters on scheduledTime only — drafts without a scheduledTime are excluded when this parameter is used. |
Response
{
"success": true,
"posts": [
{
"postGroupId": "507f1f77bcf86cd799439011",
"content": "Excited to share our new product launch!",
"status": "scheduled",
"scheduledTime": "2026-03-15T14:30:00.000Z",
"createdAt": "2026-03-10T09:00:00.000Z",
"updatedAt": "2026-03-10T09:00:00.000Z",
"platforms": [
{
"platformId": "twitter-123456789",
"platform": "twitter",
"status": "scheduled"
},
{
"platformId": "linkedin-ABC123",
"platform": "linkedin",
"status": "scheduled"
}
],
"mediaUrls": ["https://your-media-url.example.com/images/abc123.jpg"]
}
],
"pagination": {
"page": 1,
"limit": 20,
"totalItems": 47,
"totalPages": 3,
"hasNextPage": true,
"hasPrevPage": false
}
}Note: The
platformIdfield in list-posts uses a compound format (e.g.,"twitter-123456789"), combining the platform name with the raw ID. This differs from the get-post endpoint, which returns only the raw platform ID (e.g.,"123456789").
Note: Filtering by
?status=publishedreturns only posts where all platforms published successfully. It does not includepartially_publishedposts. This is an exact match and differs from dashboard behavior, which may group these statuses together.
Pagination Fields
| Field | Type | Description |
|---|---|---|
page | number | Current page number |
limit | number | Items per page |
totalItems | number | Total number of posts matching filters |
totalPages | number | Total number of pages |
hasNextPage | boolean | Whether more pages exist after current |
hasPrevPage | boolean | Whether pages exist before current |
Examples
JavaScript - Fetch All Scheduled Posts with Pagination
async function fetchAllScheduledPosts(apiKey) {
const posts = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const url = new URL('https://api.postpost.dev/api/v1/list-posts');
url.searchParams.set('page', page);
url.searchParams.set('limit', 100);
url.searchParams.set('status', 'scheduled');
url.searchParams.set('sortBy', 'scheduledTime');
url.searchParams.set('sortOrder', 'asc');
const response = await fetch(url, {
headers: { 'x-api-key': apiKey }
});
const data = await response.json();
posts.push(...data.posts);
hasMore = data.pagination.hasNextPage;
page++;
console.log(`Fetched page ${data.pagination.page} of ${data.pagination.totalPages}`);
}
console.log(`Total scheduled posts: ${posts.length}`);
return posts;
}
// Usage
const scheduledPosts = await fetchAllScheduledPosts('YOUR_API_KEY');JavaScript - Paginated List with UI Controls
class PostListPaginator {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.postpost.dev/api/v1/list-posts';
this.currentPage = 1;
this.limit = 20;
this.filters = {};
}
setFilters({ status, platform, fromDate, toDate }) {
this.filters = { status, platform, fromDate, toDate };
this.currentPage = 1; // Reset to first page when filters change
}
async fetchPage(page = this.currentPage) {
const url = new URL(this.baseUrl);
url.searchParams.set('page', page);
url.searchParams.set('limit', this.limit);
// Apply filters
if (this.filters.status) url.searchParams.set('status', this.filters.status);
if (this.filters.platform) url.searchParams.set('platform', this.filters.platform);
if (this.filters.fromDate) url.searchParams.set('fromDate', this.filters.fromDate);
if (this.filters.toDate) url.searchParams.set('toDate', this.filters.toDate);
const response = await fetch(url, {
headers: { 'x-api-key': this.apiKey }
});
const data = await response.json();
this.currentPage = data.pagination.page;
return data;
}
async nextPage() {
return this.fetchPage(this.currentPage + 1);
}
async prevPage() {
return this.fetchPage(Math.max(1, this.currentPage - 1));
}
async goToPage(page) {
return this.fetchPage(page);
}
}
// Usage
const paginator = new PostListPaginator('YOUR_API_KEY');
// Get first page of scheduled posts
paginator.setFilters({ status: 'scheduled' });
const firstPage = await paginator.fetchPage();
console.log(`Showing ${firstPage.posts.length} of ${firstPage.pagination.totalItems} posts`);
// Navigate pages
if (firstPage.pagination.hasNextPage) {
const secondPage = await paginator.nextPage();
}Python - Fetch All Posts with Pagination
import requests
from typing import Generator, Dict, Any, Optional
def fetch_posts_paginated(
api_key: str,
status: Optional[str] = None,
platform: Optional[str] = None,
limit: int = 100
) -> Generator[Dict[str, Any], None, None]:
"""
Generator that yields posts one by one, handling pagination automatically.
Args:
api_key: Your PostPost API key
status: Filter by status (draft, scheduled, published, failed, partially_published)
platform: Filter by platform (twitter, linkedin, etc.)
limit: Items per page (max 100)
Yields:
Individual post objects
"""
base_url = 'https://api.postpost.dev/api/v1/list-posts'
headers = {'x-api-key': api_key}
page = 1
while True:
params = {
'page': page,
'limit': limit,
'sortBy': 'scheduledTime',
'sortOrder': 'asc'
}
if status:
params['status'] = status
if platform:
params['platform'] = platform
response = requests.get(base_url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
for post in data['posts']:
yield post
if not data['pagination']['hasNextPage']:
break
page += 1
def fetch_all_scheduled_posts(api_key: str) -> list:
"""Fetch all scheduled posts into a list."""
posts = list(fetch_posts_paginated(api_key, status='scheduled'))
print(f"Fetched {len(posts)} scheduled posts")
return posts
# Usage
api_key = 'YOUR_API_KEY'
# Iterate through all scheduled posts
for post in fetch_posts_paginated(api_key, status='scheduled'):
print(f"{post['postGroupId']}: {post['content'][:50]}...")
print(f" Scheduled for: {post['scheduledTime']}")
print(f" Platforms: {[p['platform'] for p in post['platforms']]}")
# Or fetch all at once
all_posts = fetch_all_scheduled_posts(api_key)Python - Filter Posts by Date Range
import requests
from datetime import datetime, timedelta, timezone
def get_posts_for_week(api_key: str, start_date: datetime) -> list:
"""Get all posts scheduled for a specific week."""
end_date = start_date + timedelta(days=7)
params = {
'status': 'scheduled',
'fromDate': start_date.isoformat(),
'toDate': end_date.isoformat(),
'sortBy': 'scheduledTime',
'sortOrder': 'asc',
'limit': 100
}
response = requests.get(
'https://api.postpost.dev/api/v1/list-posts',
headers={'x-api-key': api_key},
params=params
)
return response.json()['posts']
# Usage: Get posts scheduled for next week
next_monday = datetime.now(timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
# Adjust to next Monday
days_until_monday = (7 - next_monday.weekday()) % 7
next_monday += timedelta(days=days_until_monday)
posts = get_posts_for_week('YOUR_API_KEY', next_monday)
print(f"Posts scheduled for next week: {len(posts)}")Node.js (axios) - Paginated Fetch with Async Iterator
const axios = require('axios');
async function* paginatedPosts(apiKey, options = {}) {
const { status, platform, limit = 100 } = options;
let page = 1;
let hasMore = true;
const api = axios.create({
baseURL: 'https://api.postpost.dev/api/v1',
headers: { 'x-api-key': apiKey }
});
while (hasMore) {
const params = { page, limit, sortBy: 'scheduledTime', sortOrder: 'asc' };
if (status) params.status = status;
if (platform) params.platform = platform;
const { data } = await api.get('/list-posts', { params });
for (const post of data.posts) {
yield post;
}
hasMore = data.pagination.hasNextPage;
page++;
}
}
// Usage
(async () => {
// Iterate through all scheduled posts
for await (const post of paginatedPosts('YOUR_API_KEY', { status: 'scheduled' })) {
console.log(`${post.postGroupId}: ${post.content.slice(0, 50)}...`);
console.log(` Scheduled: ${post.scheduledTime}`);
}
})();cURL - Basic Pagination
# Get first page of scheduled posts
curl "https://api.postpost.dev/api/v1/list-posts?page=1&limit=20&status=scheduled" \
-H "x-api-key: YOUR_API_KEY"
# Get second page
curl "https://api.postpost.dev/api/v1/list-posts?page=2&limit=20&status=scheduled" \
-H "x-api-key: YOUR_API_KEY"
# Filter by platform and date range
curl "https://api.postpost.dev/api/v1/list-posts?status=scheduled&platform=twitter&fromDate=2026-03-01T00:00:00Z&toDate=2026-03-31T23:59:59Z" \
-H "x-api-key: YOUR_API_KEY"
# Get all posts sorted by scheduled time
curl "https://api.postpost.dev/api/v1/list-posts?sortBy=scheduledTime&sortOrder=asc&limit=100" \
-H "x-api-key: YOUR_API_KEY"Bash - Fetch All Pages
#!/bin/bash
API_KEY="YOUR_API_KEY"
BASE_URL="https://api.postpost.dev/api/v1/list-posts"
PAGE=1
LIMIT=100
ALL_POSTS="[]"
while true; do
RESPONSE=$(curl -s "${BASE_URL}?page=${PAGE}&limit=${LIMIT}&status=scheduled" \
-H "x-api-key: ${API_KEY}")
POSTS=$(echo "$RESPONSE" | jq '.posts')
HAS_NEXT=$(echo "$RESPONSE" | jq '.pagination.hasNextPage')
TOTAL=$(echo "$RESPONSE" | jq '.pagination.totalItems')
# Merge posts
ALL_POSTS=$(echo "$ALL_POSTS" "$POSTS" | jq -s 'add')
echo "Fetched page $PAGE (Total items: $TOTAL)"
if [ "$HAS_NEXT" = "false" ]; then
break
fi
PAGE=$((PAGE + 1))
done
echo "Total posts fetched: $(echo "$ALL_POSTS" | jq 'length')"
echo "$ALL_POSTS" > scheduled_posts.jsonErrors
| Status | Error | Cause |
|---|---|---|
| 400 | "Invalid status. Must be one of: draft, scheduled, published, failed, partially_published" | Invalid status filter value |
| 400 | "Invalid sortBy. Must be one of: createdAt, updatedAt, scheduledTime" | Invalid sortBy field |
| 400 | "Invalid fromDate format" | fromDate is not a valid date string (not parseable by new Date()) |
| 400 | "Invalid toDate format" | toDate is not a valid date string (not parseable by new Date()) |
| 400 | "Invalid x-postpost-user-id" | The x-postpost-user-id header value is not a valid ObjectId format |
| 401 | "API key is required" | Missing x-api-key header |
| 401 | "Invalid API key" | The provided API key is not valid |
| 401 | "Invalid API key owner" | API key exists but the associated workspace/user could not be resolved |
| 403 | "API access is not enabled for this account" | The account does not have API access enabled |
| 403 | "MCP access is not enabled for this account" | Returned when x-postpost-client: mcp is set but MCP access is not enabled |
| 403 | "Workspace access is not enabled for this key" | The API key does not have workspace/managed-user permissions |
| 403 | "User is not managed by key" | The x-postpost-user-id references a user not managed by this API key |
| 500 | "Failed to list posts" | Internal server error |
Note: The
pageandlimitparameters do not return errors for out-of-range values. Instead, they are silently clamped to valid ranges:pageis clamped to a minimum of 1, andlimitis clamped to the range 1–100 (with falsy values like 0 defaulting to 20).
Best Practices
-
Use reasonable page sizes. For UI pagination, 20-50 items is typical. For bulk exports, use the max of 100.
-
Always handle pagination. Don't assume all posts fit on one page. Check
hasNextPageor loop untilpage >= totalPages. -
Cache total counts. The
totalItemscount is useful for UI but may be expensive. Cache it for paginated UIs. -
Filter server-side. Use query parameters to filter by status and platform rather than fetching all posts and filtering client-side.
-
Use date ranges for calendars. When building calendar views, use
fromDateandtoDateto fetch only the visible range.