Posts

Schedule Post

Schedule posts across multiple platforms

Endpoint

POST /posts

Request

curl -X POST https://api.postcore.dev/posts \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileKey": "prof_abc123",
    "content": "Excited to announce our new feature! 🚀",
    "platforms": ["linkedin", "bluesky"],
    "scheduledFor": "2025-01-15T14:00:00Z"
  }'
const response = await fetch('https://api.postcore.dev/posts', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.POSTCORE_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    profileKey: 'prof_abc123',
    content: 'Excited to announce our new feature! 🚀',
    platforms: ['linkedin', 'bluesky'],
    scheduledFor: '2025-01-15T14:00:00Z',
  }),
});

const data = await response.json();
console.log(`Scheduled ${data.posts.length} posts`);
import os
import requests

response = requests.post(
    'https://api.postcore.dev/posts',
    headers={
        'x-api-key': os.getenv('POSTCORE_API_KEY'),
        'Content-Type': 'application/json'
    },
    json={
        'profileKey': 'prof_abc123',
        'content': 'Excited to announce our new feature! 🚀',
        'platforms': ['linkedin', 'bluesky'],
        'scheduledFor': '2025-01-15T14:00:00Z'
    }
)

data = response.json()
print(f"Scheduled {len(data['posts'])} posts")

Request Body

FieldTypeRequiredDescription
profileKeystringYesProfile to post from
contentstringYesPost content (max 3000 chars)
platformsarrayYesPlatforms to post to: ["linkedin"], ["bluesky"], or both
scheduledForstringYesISO 8601 datetime in UTC (must be in future)

Response

{
  "posts": [
    {
      "postId": "post_abc123",
      "platform": "linkedin",
      "scheduledFor": "2025-01-15T14:00:00Z",
      "createdAt": "2025-01-08T12:00:00Z",
      "status": "pending"
    },
    {
      "postId": "post_def456",
      "platform": "bluesky",
      "scheduledFor": "2025-01-15T14:00:00Z",
      "createdAt": "2025-01-08T12:00:00Z",
      "status": "pending"
    }
  ],
  "usage": {
    "month": "January 2025",
    "used": 2,
    "limit": 10,
    "remaining": 8
  }
}

Status Code: 201 Created

Response Fields

Posts Array:

Each platform gets its own post object:

FieldTypeDescription
postIdstringUnique post identifier
platformstringPlatform name
scheduledForstringWhen the post will publish
createdAtstringWhen the post was created
statusstringAlways "pending" for new posts

Usage Object:

Tracks your monthly post quota:

FieldTypeDescription
monthstringCurrent month
usednumberPosts created this month
limitnumberMonthly limit (10 for Free, 3000 for Pro)
remainingnumberPosts remaining this month

Scheduling Rules

Time Format

All times must be in UTC using ISO 8601 format:

Correct:

"scheduledFor": "2025-01-15T14:00:00Z"

Incorrect:

"scheduledFor": "2025-01-15 14:00:00"    // Missing 'T' and 'Z'
"scheduledFor": "January 15, 2025"       // Not ISO 8601

Future Time Required

The scheduled time must be in the future:

// Must be any time after now
const scheduledFor = new Date();
scheduledFor.setSeconds(scheduledFor.getSeconds() + 5); // Even 5 seconds works
const isoString = scheduledFor.toISOString();

Posts are checked every second, so they'll publish as soon as the scheduled time is reached.


Platform Rate Limits

postcore enforces platform-specific rate limits to comply with each platform's posting rules:

PlatformMinimum Time Between Posts
LinkedIn60 seconds
Bluesky1 second

These limits apply per profile, per platform. You can schedule to multiple platforms simultaneously, but each platform has its own rate limit.

Rate Limit Example

// Allowed: Different platforms
await schedulePost({
  platforms: ["linkedin"],
  scheduledFor: "2025-01-15T14:00:00Z",
});
await schedulePost({
  platforms: ["bluesky"],
  scheduledFor: "2025-01-15T14:00:00Z",
}); // ✅ OK - different platform

// Not allowed: Same platform too close
await schedulePost({
  platforms: ["linkedin"],
  scheduledFor: "2025-01-15T14:00:00Z",
});
await schedulePost({
  platforms: ["linkedin"],
  scheduledFor: "2025-01-15T14:00:30Z",
}); // ❌ Error - only 30 seconds apart

Handling Rate Limit Errors

{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "LinkedIn requires 60 seconds between posts. Next available: 2025-01-15T14:01:00Z",
  "platform": "linkedin",
  "nextAvailable": "2025-01-15T14:01:00Z"
}

Status: 429 Too Many Requests

Solution: Use the nextAvailable time for your next LinkedIn post.


Monthly Limits

Your plan determines how many posts you can create per month:

  • Free Plan: 10 posts/month
  • Pro Plan: 3,000 posts/month

Important: Limits are based on creation date, not publish date. A post created in January counts toward January's limit, even if scheduled to publish in February.

The usage object in every response shows your current usage.

Monthly Limit Error

{
  "error": "MONTHLY_LIMIT_EXCEEDED",
  "message": "Monthly limit of 10 posts reached",
  "limit": 10,
  "used": 10
}

Status: 429 Too Many Requests

Solution: Wait until next month or upgrade to Pro.


Character Limits

Each platform has its own character limits:

PlatformCharacter Limit
LinkedIn3,000 characters
Bluesky300 characters

If your content exceeds a platform's limit, the API will reject the entire request before creating any posts.

Content Too Long Error

{
  "error": "CONTENT_TOO_LONG",
  "message": "Content exceeds Bluesky's limit of 300 characters (current: 350)",
  "platform": "bluesky",
  "limit": 300,
  "current": 350
}

Status: 400 Bad Request

Solution: Shorten your content or remove that platform from the request.


Error Responses

Profile Not Found

{
  "error": "PROFILE_NOT_FOUND",
  "message": "Profile not found"
}

Status: 404 Not Found

Causes:

  • Invalid profileKey
  • Profile was deleted
  • Profile belongs to different account

Platforms Not Connected

{
  "error": "PLATFORMS_NOT_CONNECTED",
  "message": "The following platforms are not connected: linkedin",
  "missingPlatforms": ["linkedin"]
}

Status: 400 Bad Request

Solution: Connect the missing platforms first. See Connect OAuth Platforms or Connect Credential Platforms.

Invalid Scheduled Time

{
  "error": "INVALID_SCHEDULED_TIME",
  "message": "Scheduled time must be in the future"
}

Status: 400 Bad Request

Solution: Schedule any time in the future (even a few seconds works).


Best Practices

Respect Rate Limits

Track next available posting times for each platform:

const rateLimits = {
  linkedin: 60000, // 60 seconds in ms
  bluesky: 1000, // 1 second in ms
};

let nextAvailable = {
  linkedin: new Date(),
  bluesky: new Date(),
};

async function schedulePost(profileKey, content, platforms) {
  // Find the latest required time across requested platforms
  const earliestTime = platforms.reduce((latest, platform) => {
    const platformTime = nextAvailable[platform];
    return platformTime > latest ? platformTime : latest;
  }, new Date());

  // Ensure it's in the future
  if (earliestTime <= new Date()) {
    earliestTime.setSeconds(earliestTime.getSeconds() + 1);
  }

  const response = await fetch("https://api.postcore.dev/posts", {
    method: "POST",
    headers: {
      "x-api-key": process.env.POSTCORE_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      profileKey,
      content,
      platforms,
      scheduledFor: earliestTime.toISOString(),
    }),
  });

  if (response.ok) {
    // Update next available times
    platforms.forEach((platform) => {
      nextAvailable[platform] = new Date(
        earliestTime.getTime() + rateLimits[platform]
      );
    });
  }

  return response.json();
}

Monitor Monthly Usage

Check usage before scheduling:

const response = await fetch("https://api.postcore.dev/posts", {
  method: "POST",
  headers: {
    "x-api-key": apiKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    /* ... */
  }),
});

const { usage } = await response.json();

if (usage.remaining < 5) {
  console.warn(`Only ${usage.remaining} posts remaining this month!`);
}

Validate Content Length

Check content length before sending:

function validateContent(content, platforms) {
  const limits = { linkedin: 3000, bluesky: 300 };

  for (const platform of platforms) {
    if (content.length > limits[platform]) {
      throw new Error(
        `Content too long for ${platform}: ${content.length}/${limits[platform]} chars`
      );
    }
  }
}

// Usage
try {
  validateContent(myContent, ["linkedin", "bluesky"]);
  await schedulePost(/* ... */);
} catch (error) {
  console.error(error.message);
}