Error Codes
Every error response uses a consistent envelope:
{ "success": false, "message": "Human-readable description", "code": "machine_readable_code", "errors": []}The code field is stable. Use it for programmatic error handling. The message field may change between versions.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning | Retryable? |
|---|---|---|
| 400 | Bad request. Invalid input | No |
| 401 | Unauthorized. Missing or invalid API key | No |
| 403 | Forbidden. Access denied, plan limit exceeded, or CSRF failure (session auth only) | No (check code) |
| 404 | Not found. Resource doesn’t exist | No |
| 409 | Conflict. Resource state prevents the action | No (fix state first) |
| 422 | Validation error. Request body/query failed schema validation | No (fix input) |
| 429 | Rate limited. Too many requests | Yes (after Retry-After seconds) |
| 500 | Internal error. Unexpected server failure | Yes |
Global errors
Section titled “Global errors”These can occur on any endpoint.
| Code | Status | Description |
|---|---|---|
bad_request |
400 | Request is malformed or semantically invalid. |
unauthorized |
401 | No valid API key provided, or key has been revoked. |
forbidden |
403 | Access denied. |
not_found |
404 | Resource does not exist or URL does not match any route. |
conflict |
409 | Resource state prevents the requested action. |
validation_error |
422 | Request body, query, or path params failed schema validation. See errors array for details. |
rate_limit_exceeded |
429 | Exceeded the rate limit (approximately 100 requests per 10 seconds per client IP). The Retry-After header tells you when to retry. |
internal_error |
500 | Unexpected server error. |
error |
500 | Fallback code when no specific code applies. |
Schedule errors
Section titled “Schedule errors”| Code | Status | When |
|---|---|---|
schedule_not_found |
404 | Schedule ID doesn’t exist or doesn’t belong to your account. |
schedule_archived |
409 | Attempting to update, trigger, pause, resume, or re-archive an archived schedule. |
schedule_completed |
409 | Attempting to update, trigger, pause, or resume a completed schedule (reached runs limit or stop_at). |
schedule_already_paused |
409 | Pausing a schedule that is already paused. |
schedule_already_active |
409 | Resuming a schedule that is already active. |
schedule_cadence_invalid |
422 | Resuming a schedule whose stored cron, interval, or timezone can no longer be computed. Update the schedule, then resume. |
handler_required_for_pull |
400 | Updating a schedule to pull delivery without setting a handler. |
schedule_status_changed |
409 | Concurrent update detected. The schedule’s status changed between your read and write. Retry the operation. |
plan_limit_exceeded |
403 | The request exceeds a plan limit: the active-schedule count (create, resume), the timeout cap (create, update, resume, trigger), or the minimum interval (create, update, resume). The message names the limit and your tier; see Pricing & Limits. |
Job errors
Section titled “Job errors”| Code | Status | When |
|---|---|---|
job_not_found |
404 | Job ID doesn’t exist or doesn’t belong to your account. |
idempotency_key_conflict |
409 | A job with the same idempotency_key already exists on your account. |
job_not_cancellable |
409 | Cancelling a job that isn’t pending, admitting, queued, ready, or retrying. |
job_not_scheduled |
409 | Skipping a one-off job. Only schedule-spawned jobs can be skipped. |
job_not_skippable |
409 | Skipping a job that isn’t pending or queued. |
job_not_failed |
409 | Retrying a job that hasn’t failed. |
job_status_changed |
409 | Concurrent update detected. The job’s status changed between your read and write. Retry the operation. |
plan_limit_exceeded |
403 | timeout exceeds your plan’s cap. The message names the cap and your tier; see Pricing & Limits. |
Execution errors
Section titled “Execution errors”| Code | Status | When |
|---|---|---|
execution_not_found |
404 | Execution ID doesn’t exist or doesn’t belong to your account. |
execution_not_running |
409 | Reporting a result for an execution that isn’t in running status. The execution may have already timed out or been reported by another process. Reporting the status the execution already has returns 200 instead. |
plan_limit_exceeded |
403 | Claiming a job after your plan’s monthly execution quota is used up. See Pricing & Limits. |
Monitor errors
Section titled “Monitor errors”| Code | Status | When |
|---|---|---|
monitor_not_found |
404 | Monitor ID doesn’t exist or doesn’t belong to your account. |
monitor_already_paused |
409 | Pausing a monitor that is already paused. |
monitor_not_paused |
409 | Resuming a monitor that isn’t paused. |
plan_limit_exceeded |
403 | Creating or resuming a monitor when you already have as many active monitors as your plan allows. See Pricing & Limits. |
The Ping API returns plain-text responses, not these codes.
Signing key errors
Section titled “Signing key errors”| Code | Status | When |
|---|---|---|
signing_key_not_found |
404 | No signing key exists for your account. Create one in the dashboard. |
signing_key_grace_window_active |
409 | A key rotation is already in progress. Wait 24 hours for the grace window to close before rotating again. |
Validation error details
Section titled “Validation error details”When the code is validation_error, the errors array contains per-field details:
{ "success": false, "message": "request body failed validation", "code": "validation_error", "errors": [ { "field": "cron", "message": "cron must be a valid cron expression" }, { "field": "payload", "message": "payload must be 64KB or smaller" } ]}Common validation messages:
| Message | Cause |
|---|---|
provide either cron or interval, not both |
Schedule creation/update with both timing fields |
provide either delay or scheduled_for, not both |
Job creation with both timing options |
payload must be 64KB or smaller |
Payload exceeds size limit |
cron must be a valid cron expression |
Unparseable cron syntax |
must be a valid IANA timezone |
Invalid timezone string |
url must use https in production |
HTTP URL (HTTPS required in production) |
url must not point to a private or reserved IP address |
Localhost or private network URL |
stop_at must be a future date |
Past date for schedule termination |
scheduled_for must be a future date |
Past date for job execution |
size must be between 1 and 100 |
Pagination page size out of range |
at least one field must be provided |
Empty update body |
cron must not be more frequent than every 60 seconds |
Monitor cron expression that fires more often than once a minute |
interval must not be more frequent than every 60 seconds |
Monitor interval under 60 seconds |
cron must fire at least twice in the next 2 years |
Monitor cron expression that fires too rarely |
interval must not be longer than 365 days |
Monitor interval over 365 days |
grace_seconds must be between 60 and 604800 |
Monitor grace period out of range |
Handling errors in code
Section titled “Handling errors in code”With the SDK
Section titled “With the SDK”The SDK throws API errors as typed error classes. Resource methods throw ChronosApiError, so branch on its code:
import { Chronos, ChronosApiError } from '@chronos.sh/sdk';
const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });
try { await chronos.jobs.create({ name: 'Charge invoice inv_789', handler: 'charge-invoice', idempotency_key: 'charge-inv_789', });} catch (err) { if (!(err instanceof ChronosApiError)) throw err; switch (err.code) { case 'idempotency_key_conflict': break; // The job already exists. case 'validation_error': console.error('Invalid input:', err.fieldErrors); break; default: throw err; }}Reads retry network errors, timeouts, 429 and 5xx responses before throwing, and writes retry only on 429. See Retries for the full rule.
The worker retries 429 and 5xx responses itself, so the one rejection you catch from start() is a 401:
import { Chronos, ChronosAuthenticationError } from '@chronos.sh/sdk';
const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });
try { await chronos.worker.start();} catch (err) { if (err instanceof ChronosAuthenticationError) { console.error('Chronos rejected the API key; rotate CHRONOS_API_KEY', { requestId: err.requestId }); process.exit(1); } throw err;}See Node SDK reference for every error class.
With direct API calls
Section titled “With direct API calls”Check the code field for programmatic handling:
const res = await fetch('https://api.chronos.sh/v1/schedules', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(schedule),});
if (!res.ok) { const error = await res.json(); switch (error.code) { case 'validation_error': console.error('Invalid input:', error.errors); break; case 'rate_limit_exceeded': const retryAfter = res.headers.get('Retry-After'); await sleep(Number(retryAfter) * 1000); break; case 'unauthorized': throw new Error('Check your API key'); break; }}