Skip to main content

Error Handling

All SchedPilot API errors return a JSON body with at least two fields:

FieldTypeDescription
codeintegerMatches the HTTP status code.
messagestringHuman-readable explanation of what went wrong. For 400 errors, the message will identify the specific field that failed validation.

HTTP Status Codes

CodeNameWhen it occurs
200OKSuccessful GET or DELETE request.
201CreatedResource created successfully (POST /post, POST /media/upload).
400Bad RequestA required parameter is missing, has the wrong type, or fails validation. The message will name the offending field.
401UnauthorizedNo API key or token was included in the request.
403ForbiddenThe API key or OAuth token was found but is invalid, expired, or has been revoked.
404Not FoundThe requested resource does not exist, or exists but belongs to a different user.
409ConflictThe operation is not allowed given the resource's current state (e.g. deleting a published post, or deleting media attached to a scheduled post).
413Payload Too LargeThe uploaded file exceeds the 5 GB limit.
415Unsupported Media TypeThe file format is not supported.
422Unprocessable EntityThe file upload multipart/form-data body is missing the file field or is otherwise malformed.
429Too Many RequestsThe rate limit has been exceeded (60 read or 30 write requests per hour).
500Internal Server ErrorAn 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

StatusAction
429 Too Many RequestsWait until the top of the next hour, then retry. See Rate Limits for a code example.
500 Internal Server ErrorRetry 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 RequestDo not retry. Fix the request parameters first. Retrying the same malformed request will produce the same error.
401 UnauthorizedDo not retry. Add the X-API-KEY or Authorization header.
403 ForbiddenDo not retry the same credential. Rotate your API key or re-run the OAuth flow.
404 Not FoundDo not retry. Verify the resource ID exists and belongs to your account.
409 ConflictDo not retry without changing the state. Resolve the conflict described in message first.
413 / 415 / 422Do 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));
}
}