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 type | Limit | Window |
|---|---|---|
| Read (GET) | 60 requests | Per hour, per user |
| Write (POST, DELETE) | 30 requests | Per 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 rate limit window resets at the top of each hour. It is not a rolling 60-minute window — the counter resets on the clock hour regardless of when you started making requests. For example, if the window opens at 14:00 UTC, all requests made between 14:00 and 14:59:59 count toward that window; the counter resets at 15:00.
What Counts as a Read Request
The following endpoints consume one unit from the read bucket per call:
GET /developers/v1/accountsGET /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/uploadDELETE /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
When you receive a 429, wait until the top of the next hour before retrying. The following JavaScript example shows a simple retry helper with exponential backoff as a fallback:
async function fetchWithRateLimitRetry(url, options = {}, maxRetries = 3) {
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.`);
}
// Calculate milliseconds until the top of the next hour
const now = new Date();
const nextHour = new Date(now);
nextHour.setHours(now.getHours() + 1, 0, 0, 0);
const msUntilReset = nextHour - now;
// For the first retry, wait until the window resets.
// For subsequent retries (unexpected), use exponential backoff.
const waitMs = attempt === 0
? msUntilReset
: Math.min(msUntilReset, 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.