Skip to main content

Rate Limits

SchedPilot enforces per-user rate limits to ensure fair API access for everyone. Limits are tracked separately for read and write operations.

Limits at a Glance

Request typeLimitWindow
Read (GET)60 requestsPer hour, per user
Write (POST, DELETE)30 requestsPer hour, per user

Limits are tracked per user account, not per IP address. Whether you are calling from a server, a local script, or an MCP agent, all requests authenticated with the same API key or OAuth token count toward the same bucket.

Window Behavior

The window is rolling, not aligned to the clock hour. Your counter starts on your first request and clears roughly 60 minutes later — but only after a quiet period: each new request of the same type (read or write) extends the window. In practice, once you reach the limit you must pause requests of that type for up to an hour before the counter frees up. There is no fixed reset at the top of the hour.

What Counts as a Read Request

The following endpoints consume one unit from the read bucket per call:

  • GET /developers/v1/accounts
  • GET /developers/v1/posts (list posts)
  • GET /developers/v1/analytics/{id}
  • GET /developers/v1/media/list

What Counts as a Write Request

The following endpoints consume one unit from the write bucket per call:

  • POST /developers/v1/post (create post)
  • DELETE /developers/v1/posts/{id}
  • POST /developers/v1/media/upload
  • DELETE /developers/v1/media/delete

429 Response

When you exceed a limit, the API returns:

HTTP 429 Too Many Requests

{
"code": 429,
"message": "Rate limit exceeded. Max 60 read requests/hour."
}

Or for write requests:

{
"code": 429,
"message": "Rate limit exceeded. Max 30 write requests/hour."
}

Handling 429 Errors

Because the window is rolling, the safest response to a 429 is to back off with increasing delays rather than retrying immediately. The following JavaScript example shows a simple retry helper with exponential backoff:

async function fetchWithRateLimitRetry(url, options = {}, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);

if (response.status !== 429) {
return response;
}

if (attempt === maxRetries) {
throw new Error(`Rate limit exceeded after ${maxRetries} retries.`);
}

// Exponential backoff, capped at 60s (the longest the window can hold).
const waitMs = Math.min(60_000, 1000 * 2 ** attempt);

console.warn(`Rate limited. Retrying in ${Math.round(waitMs / 1000)}s...`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}

// Usage
const response = await fetchWithRateLimitRetry(
'https://api.schedpilot.com/developers/v1/accounts',
{ headers: { 'X-API-KEY': 'smm_your_key_here' } }
);
const accounts = await response.json();

Best Practices

Cache the accounts list. GET /accounts rarely changes. Fetch it once at startup and cache the result locally rather than calling it before every post creation.

Batch posts when possible. If you need to schedule posts for multiple accounts on the same content, pass all target accounts in a single POST /post call using the accounts array. This uses one write unit instead of N.

Use webhooks instead of polling. Calling GET /posts in a loop to detect when a post has published will exhaust your read quota quickly. Register a webhook for post.published and post.failed events to get push notifications instead.

Spread automated scheduling over time. If you're bulk-importing a content calendar, introduce a small delay between POST /post calls to avoid hitting the write limit partway through the batch.