PostPost

List Posts

Retrieve a paginated list of all your scheduled, draft, published, and failed posts.

Endpoint

GET https://api.postpost.dev/api/v1/list-posts

Headers

HeaderRequiredDescription
x-api-keyYesYour API key
x-postpost-user-idNoManaged user ID (workspace only)
x-postpost-clientNoClient identifier (e.g., mcp for MCP integrations). Affects which access controls are checked.

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed). Values less than 1 are silently clamped to 1.
limitnumber20Items per page. Values are clamped to the range 1–100, with falsy values (0, NaN, empty) defaulting to 20.
statusstringallFilter 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.
platformstringallFilter 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.
sortBystringcreatedAtSort field: createdAt, scheduledTime, updatedAt
sortOrderstringdescSort order: asc or desc. Invalid values silently default to desc.
fromDatestring-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.
toDatestring-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 platformId field 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=published returns only posts where all platforms published successfully. It does not include partially_published posts. This is an exact match and differs from dashboard behavior, which may group these statuses together.

Pagination Fields

FieldTypeDescription
pagenumberCurrent page number
limitnumberItems per page
totalItemsnumberTotal number of posts matching filters
totalPagesnumberTotal number of pages
hasNextPagebooleanWhether more pages exist after current
hasPrevPagebooleanWhether 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.json

Errors

StatusErrorCause
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 page and limit parameters do not return errors for out-of-range values. Instead, they are silently clamped to valid ranges: page is clamped to a minimum of 1, and limit is clamped to the range 1–100 (with falsy values like 0 defaulting to 20).

Best Practices

  1. Use reasonable page sizes. For UI pagination, 20-50 items is typical. For bulk exports, use the max of 100.

  2. Always handle pagination. Don't assume all posts fit on one page. Check hasNextPage or loop until page >= totalPages.

  3. Cache total counts. The totalItems count is useful for UI but may be expensive. Cache it for paginated UIs.

  4. Filter server-side. Use query parameters to filter by status and platform rather than fetching all posts and filtering client-side.

  5. Use date ranges for calendars. When building calendar views, use fromDate and toDate to fetch only the visible range.


On this page