PostPost

Bulk Scheduling

Schedule multiple posts at once using batch operations, CSV imports, and content calendars.

Overview

Bulk scheduling allows you to:

  • Schedule a week or month of content in one script
  • Import posts from CSV or spreadsheets
  • Create content calendars with automated publishing
  • Batch update or delete multiple posts

Schedule Multiple Posts

JavaScript: Schedule a Week of Content

const PUBLORA_API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://api.postpost.dev/api/v1';

const headers = {
  'Content-Type': 'application/json',
  'x-api-key': PUBLORA_API_KEY
};

async function scheduleWeekOfContent(posts, platforms, startDate = new Date()) {
  const results = [];

  for (let i = 0; i < posts.length; i++) {
    // Schedule each post on a different day at 10 AM UTC
    const scheduledDate = new Date(startDate);
    scheduledDate.setDate(startDate.getDate() + i);
    scheduledDate.setUTCHours(10, 0, 0, 0);

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        content: posts[i].content,
        platforms,
        scheduledTime: scheduledDate.toISOString()
      })
    });

    const result = await response.json();
    results.push({
      day: i + 1,
      date: scheduledDate.toDateString(),
      postGroupId: result.postGroupId,
      success: response.ok
    });

    console.log(`Day ${i + 1}: ${response.ok ? '✓' : '✗'} ${scheduledDate.toDateString()}`);

    // Small delay to avoid rate limits
    await new Promise(r => setTimeout(r, 200));
  }

  return results;
}

// Example: 7 days of content
const weeklyPosts = [
  { content: 'Monday motivation: Start your week with intention!' },
  { content: 'Tech tip Tuesday: Always version your APIs.' },
  { content: 'Wednesday wisdom: Ship fast, iterate faster.' },
  { content: 'Throwback Thursday: How we grew from 0 to 10K users.' },
  { content: 'Feature Friday: Check out our new analytics dashboard!' },
  { content: 'Saturday reading: Top 5 dev articles this week.' },
  { content: 'Sunday reflection: What will you build next week?' }
];

scheduleWeekOfContent(weeklyPosts, ['twitter-123456', 'linkedin-ABC123']);

Python: Schedule a Month of Content

import requests
from datetime import datetime, timedelta
import time

PUBLORA_API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://api.postpost.dev/api/v1'

headers = {
    'Content-Type': 'application/json',
    'x-api-key': PUBLORA_API_KEY
}

def schedule_month_of_content(posts, platforms, start_date=None):
    if start_date is None:
        start_date = datetime.utcnow()

    results = []

    for i, post in enumerate(posts):
        scheduled_date = start_date + timedelta(days=i)
        scheduled_date = scheduled_date.replace(hour=10, minute=0, second=0, microsecond=0)

        response = requests.post(
            f'{BASE_URL}/create-post',
            headers=headers,
            json={
                'content': post['content'],
                'platforms': platforms,
                'scheduledTime': scheduled_date.isoformat() + 'Z'
            }
        )

        result = {
            'day': i + 1,
            'date': scheduled_date.strftime('%Y-%m-%d'),
            'success': response.ok
        }

        if response.ok:
            result['postGroupId'] = response.json()['postGroupId']

        results.append(result)
        print(f"Day {i + 1}: {'✓' if response.ok else '✗'} {scheduled_date.strftime('%Y-%m-%d')}")

        time.sleep(0.2)  # Rate limiting

    successful = sum(1 for r in results if r['success'])
    print(f"\nScheduled {successful}/{len(posts)} posts")

    return results

# Example: Generate 30 days of content
monthly_posts = [
    {'content': f'Day {i+1} of our 30-day challenge! #30daychallenge'}
    for i in range(30)
]

schedule_month_of_content(monthly_posts, ['twitter-123456'])

Import from CSV

CSV Format

Create a CSV file with your content:

content,platforms,scheduled_time
"Monday motivation: Start strong!",twitter-123456;linkedin-ABC123,2026-03-01T09:00:00Z
"Check out our new feature!",twitter-123456,2026-03-01T14:00:00Z
"Weekly roundup thread",twitter-123456,2026-03-02T10:00:00Z
"LinkedIn deep dive post",linkedin-ABC123,2026-03-02T12:00:00Z
"Behind the scenes",instagram-789012,2026-03-03T11:00:00Z

JavaScript CSV Import

const fs = require('fs');
const csv = require('csv-parse/sync');

async function importFromCSV(filePath) {
  const fileContent = fs.readFileSync(filePath, 'utf-8');
  const records = csv.parse(fileContent, {
    columns: true,
    skip_empty_lines: true
  });

  const results = [];

  for (const row of records) {
    const platforms = row.platforms.split(';').map(p => p.trim());

    const payload = {
      content: row.content,
      platforms,
      scheduledTime: row.scheduled_time
    };

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers,
      body: JSON.stringify(payload)
    });

    const result = await response.json();

    results.push({
      content: row.content.substring(0, 30) + '...',
      success: response.ok,
      postGroupId: result.postGroupId
    });

    console.log(`${response.ok ? '✓' : '✗'} ${row.content.substring(0, 40)}...`);

    await new Promise(r => setTimeout(r, 200));
  }

  const successful = results.filter(r => r.success).length;
  console.log(`\nImported ${successful}/${results.length} posts`);

  return results;
}

importFromCSV('./posts.csv');

Python CSV Import

import csv
import requests
import time

def import_from_csv(file_path):
    results = []

    with open(file_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)

        for row in reader:
            platforms = [p.strip() for p in row['platforms'].split(';')]

            payload = {
                'content': row['content'],
                'platforms': platforms,
                'scheduledTime': row['scheduled_time']
            }

            response = requests.post(
                f'{BASE_URL}/create-post',
                headers=headers,
                json=payload
            )

            result = {
                'content': row['content'][:30] + '...',
                'success': response.ok
            }

            if response.ok:
                result['postGroupId'] = response.json()['postGroupId']
            else:
                result['error'] = response.json().get('error', 'Unknown error')

            results.append(result)
            status = '✓' if response.ok else '✗'
            print(f"{status} {row['content'][:40]}...")

            time.sleep(0.2)

    successful = sum(1 for r in results if r['success'])
    print(f"\nImported {successful}/{len(results)} posts")

    return results

import_from_csv('posts.csv')

Content Calendar Patterns

Schedule at Optimal Times

// Optimal posting times by platform (UTC)
const OPTIMAL_TIMES = {
  twitter: [9, 12, 17],    // 9 AM, 12 PM, 5 PM
  linkedin: [7, 10, 17],   // 7 AM, 10 AM, 5 PM
  instagram: [11, 14, 19], // 11 AM, 2 PM, 7 PM
  facebook: [9, 13, 16]    // 9 AM, 1 PM, 4 PM
};

function getOptimalTime(platform, dayOffset = 0) {
  const times = OPTIMAL_TIMES[platform] || [10];
  const randomTime = times[Math.floor(Math.random() * times.length)];

  const date = new Date();
  date.setDate(date.getDate() + dayOffset);
  date.setUTCHours(randomTime, 0, 0, 0);

  return date;
}

async function scheduleAtOptimalTimes(posts) {
  for (let i = 0; i < posts.length; i++) {
    const post = posts[i];

    for (const platformId of post.platforms) {
      const platform = platformId.split('-')[0];
      const optimalTime = getOptimalTime(platform, i);

      await fetch(`${BASE_URL}/create-post`, {
        method: 'POST',
        headers,
        body: JSON.stringify({
          content: post.content,
          platforms: [platformId],
          scheduledTime: optimalTime.toISOString()
        })
      });

      console.log(`Scheduled for ${platform} at ${optimalTime.toISOString()}`);
    }
  }
}

Stagger Posts Across Platforms

async function staggerAcrossPlatforms(content, platforms, baseTime, intervalMinutes = 30) {
  const results = [];

  for (let i = 0; i < platforms.length; i++) {
    const scheduledTime = new Date(baseTime);
    scheduledTime.setMinutes(scheduledTime.getMinutes() + (i * intervalMinutes));

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        content,
        platforms: [platforms[i]],
        scheduledTime: scheduledTime.toISOString()
      })
    });

    results.push({
      platform: platforms[i],
      scheduledTime: scheduledTime.toISOString(),
      success: response.ok
    });
  }

  return results;
}

// Post to Twitter at 10:00, LinkedIn at 10:30, Threads at 11:00
staggerAcrossPlatforms(
  'Exciting announcement coming!',
  ['twitter-123456', 'linkedin-ABC123', 'threads-789012'],
  new Date('2026-03-01T10:00:00Z'),
  30
);

Weekly Recurring Schedule

async function createWeeklyRecurring(content, platforms, dayOfWeek, timeUTC, weeks = 4) {
  const results = [];

  // Find next occurrence of dayOfWeek (0 = Sunday, 1 = Monday, etc.)
  const startDate = new Date();
  while (startDate.getDay() !== dayOfWeek) {
    startDate.setDate(startDate.getDate() + 1);
  }
  startDate.setUTCHours(timeUTC, 0, 0, 0);

  for (let i = 0; i < weeks; i++) {
    const scheduledTime = new Date(startDate);
    scheduledTime.setDate(startDate.getDate() + (i * 7));

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        content,
        platforms,
        scheduledTime: scheduledTime.toISOString()
      })
    });

    results.push({
      week: i + 1,
      scheduledTime: scheduledTime.toISOString(),
      success: response.ok
    });

    console.log(`Week ${i + 1}: ${scheduledTime.toDateString()}`);
  }

  return results;
}

// Post every Monday at 9 AM UTC for 4 weeks
createWeeklyRecurring(
  'Happy Monday! What are your goals this week?',
  ['twitter-123456', 'linkedin-ABC123'],
  1, // Monday
  9, // 9 AM UTC
  4  // 4 weeks
);

Batch Operations

Retrieve Draft Post IDs

Before performing batch operations, you can retrieve your existing posts (including drafts) using the list-posts endpoint:

// List all draft posts
const response = await fetch(`${BASE_URL}/list-posts?status=draft`, {
  headers: { 'x-api-key': PUBLORA_API_KEY }
});

const { posts } = await response.json();
const draftIds = posts.map(pg => pg.postGroupId);
console.log(`Found ${draftIds.length} drafts:`, draftIds);
# List all draft posts
response = requests.get(
    f'{BASE_URL}/list-posts',
    headers={'x-api-key': PUBLORA_API_KEY},
    params={'status': 'draft'}
)

posts = response.json()['posts']
draft_ids = [pg['postGroupId'] for pg in posts]
print(f"Found {len(draft_ids)} drafts:", draft_ids)

Note: Post group IDs (postGroupId) are MongoDB ObjectIds (e.g., 6626a1f5e4b0c91a2d3f4567). The list-posts endpoint returns results in a posts array and supports the following query parameters:

ParameterTypeDescription
statusstringFilter by status: draft, scheduled, published, failed, partially_published
platformstringFilter by platform (e.g., twitter, linkedin)
pagenumberPage number (default: 1)
limitnumberResults per page (default: 20)
fromDatestringFilter posts scheduled on or after this ISO 8601 date
toDatestringFilter posts scheduled on or before this ISO 8601 date
sortBystringField to sort by: createdAt, updatedAt, or scheduledTime
sortOrderstringSort direction: asc or desc

The response includes a pagination object alongside the posts array:

{
  "success": true,
  "posts": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "totalItems": 47,
    "totalPages": 3,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

Batch Update Scheduled Times

async function batchReschedule(postGroupIds, newBaseTime, intervalHours = 2) {
  const results = [];

  for (let i = 0; i < postGroupIds.length; i++) {
    const newTime = new Date(newBaseTime);
    newTime.setHours(newTime.getHours() + (i * intervalHours));

    const response = await fetch(`${BASE_URL}/update-post/${postGroupIds[i]}`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        status: 'scheduled',
        scheduledTime: newTime.toISOString()
      })
    });

    results.push({
      postGroupId: postGroupIds[i],
      newTime: newTime.toISOString(),
      success: response.ok
    });
  }

  return results;
}

// Reschedule all posts starting from a new date
batchReschedule(
  ['6626a1f5e4b0c91a2d3f4567', '6626a1f5e4b0c91a2d3f4568', '6626a1f5e4b0c91a2d3f4569'],
  new Date('2026-03-15T10:00:00Z'),
  2 // 2 hours apart
);

Batch Delete

Note: The delete-post endpoint returns { "success": true } on success.

async function batchDelete(postGroupIds) {
  const results = [];

  for (const postGroupId of postGroupIds) {
    const response = await fetch(`${BASE_URL}/delete-post/${postGroupId}`, {
      method: 'DELETE',
      headers: { 'x-api-key': PUBLORA_API_KEY }
    });

    results.push({
      postGroupId,
      success: response.ok
    });

    await new Promise(r => setTimeout(r, 100));
  }

  const successful = results.filter(r => r.success).length;
  console.log(`Deleted ${successful}/${results.length} posts`);

  return results;
}

Convert Drafts to Scheduled

async function publishAllDrafts(draftIds, startTime, intervalMinutes = 60) {
  const results = [];

  for (let i = 0; i < draftIds.length; i++) {
    const scheduledTime = new Date(startTime);
    scheduledTime.setMinutes(scheduledTime.getMinutes() + (i * intervalMinutes));

    const response = await fetch(`${BASE_URL}/update-post/${draftIds[i]}`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        status: 'scheduled',
        scheduledTime: scheduledTime.toISOString()
      })
    });

    results.push({
      postGroupId: draftIds[i],
      scheduledTime: scheduledTime.toISOString(),
      success: response.ok
    });
  }

  return results;
}

Rate Limiting Best Practices

class RateLimitedScheduler {
  constructor(apiKey, requestsPerSecond = 5) {
    this.apiKey = apiKey;
    this.minInterval = 1000 / requestsPerSecond;
    this.lastRequest = 0;
  }

  async schedule(post) {
    // Wait if needed to respect rate limit
    const now = Date.now();
    const elapsed = now - this.lastRequest;

    if (elapsed < this.minInterval) {
      await new Promise(r => setTimeout(r, this.minInterval - elapsed));
    }

    this.lastRequest = Date.now();

    const response = await fetch(`${BASE_URL}/create-post`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': this.apiKey
      },
      body: JSON.stringify(post)
    });

    return response.json();
  }

  async scheduleBatch(posts) {
    const results = [];

    for (const post of posts) {
      const result = await this.schedule(post);
      results.push(result);
    }

    return results;
  }
}

// Usage
const scheduler = new RateLimitedScheduler(PUBLORA_API_KEY, 5);

const posts = [
  { content: 'Post 1', platforms: ['twitter-123456'], scheduledTime: '2026-03-01T10:00:00Z' },
  { content: 'Post 2', platforms: ['twitter-123456'], scheduledTime: '2026-03-01T12:00:00Z' },
  { content: 'Post 3', platforms: ['twitter-123456'], scheduledTime: '2026-03-01T14:00:00Z' }
];

scheduler.scheduleBatch(posts);

Error Handling

async function scheduleWithRetry(post, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(`${BASE_URL}/create-post`, {
        method: 'POST',
        headers,
        body: JSON.stringify(post)
      });

      if (response.ok) {
        return { success: true, data: await response.json() };
      }

      const error = await response.json();

      // Do not retry client errors (4xx) -- they will not succeed on retry
      // Note: rate limit errors use 403 with a LimitExceededError format, not 429
      if (response.status >= 400 && response.status < 500) {
        return { success: false, error: error.error || error.message };
      }

    } catch (error) {
      if (attempt === maxRetries) {
        return { success: false, error: error.error || error.message };
      }
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }

  return { success: false, error: 'Max retries exceeded' };
}

PostPost — Social media API with free Starter plan, paid plans from Pro ($2.99/mo billed yearly)

On this page