Error Handling
All SchedPilot API errors return a JSON body with at least two fields:
| Field | Type | Description |
|---|---|---|
code | integer | Matches the HTTP status code. |
message | string | Human-readable explanation of what went wrong. For 400 errors, the message will identify the specific field that failed validation. |
HTTP Status Codes
| Code | Name | When it occurs |
|---|---|---|
200 | OK | Successful GET or DELETE request. |
201 | Created | Resource created successfully (POST /post, POST /media/upload). |
400 | Bad Request | A required parameter is missing, has the wrong type, or fails validation. The message will name the offending field. |
401 | Unauthorized | No API key or token was included in the request. |
403 | Forbidden | The API key or OAuth token was found but is invalid, expired, or has been revoked. |
404 | Not Found | The requested resource does not exist, or exists but belongs to a different user. |
409 | Conflict | The operation is not allowed given the resource's current state (e.g. deleting a published post, or deleting media attached to a scheduled post). |
413 | Payload Too Large | The uploaded file exceeds the 5 GB limit. |
415 | Unsupported Media Type | The file format is not supported. |
422 | Unprocessable Entity | The file upload multipart/form-data body is missing the file field or is otherwise malformed. |
429 | Too Many Requests | The rate limit has been exceeded (60 read or 30 write requests per hour). |
500 | Internal Server Error | An unexpected error occurred on the server. |
Error Response Examples
401 Unauthorized
{
"code": 401,
"message": "No API key provided."
}
This happens when the X-API-KEY header or Authorization header is absent entirely.
403 Forbidden
{
"code": 403,
"message": "Invalid or revoked API key."
}
The credential was recognized but is no longer valid. Verify the key in your dashboard, or re-run the OAuth flow if using a bearer token.
404 Not Found
{
"code": 404,
"message": "Post not found."
}
The post ID does not exist or belongs to a different user account. SchedPilot returns 404 (rather than 403) for resources that belong to other users to avoid leaking information about their existence.
409 Conflict
{
"code": 409,
"message": "Cannot delete a post that has already been published."
}
The operation is logically invalid in the resource's current state. Read the message field — it will explain what condition is blocking the operation.
429 Too Many Requests
{
"code": 429,
"message": "Rate limit exceeded. Max 30 write requests/hour."
}
See the Rate Limits guide for how to handle and avoid this error.
400 Bad Request (validation)
{
"code": 400,
"message": "Missing required field: scheduled_date."
}
Fix the identified field in your request and retry immediately.
Retry Guidance
| Status | Action |
|---|---|
429 Too Many Requests | Wait until the top of the next hour, then retry. See Rate Limits for a code example. |
500 Internal Server Error | Retry with exponential backoff (e.g. 1s, 2s, 4s, 8s). If the error persists across multiple attempts, it may indicate a service outage — check the SchedPilot status page. |
400 Bad Request | Do not retry. Fix the request parameters first. Retrying the same malformed request will produce the same error. |
401 Unauthorized | Do not retry. Add the X-API-KEY or Authorization header. |
403 Forbidden | Do not retry the same credential. Rotate your API key or re-run the OAuth flow. |
404 Not Found | Do not retry. Verify the resource ID exists and belongs to your account. |
409 Conflict | Do not retry without changing the state. Resolve the conflict described in message first. |
413 / 415 / 422 | Do not retry. Fix the file (size, format, or multipart structure) and upload again. |
Exponential backoff example (JavaScript)
async function fetchWithBackoff(url, options = {}, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
// Success or a 4xx error that won't benefit from a retry
if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 429)) {
return response;
}
// 429 or 5xx — worth retrying
if (attempt === maxRetries) {
throw new Error(`Request failed after ${maxRetries} retries: ${response.status}`);
}
const delayMs = response.status === 429
? getMillisecondsUntilNextHour() // defined in the Rate Limits guide
: 1000 * 2 ** attempt; // 1s, 2s, 4s, 8s ...
console.warn(`HTTP ${response.status}. Retrying in ${delayMs}ms (attempt ${attempt + 1})...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}