API Reference
All REST endpoints under /api/. The server binds to 127.0.0.1 only -- no network exposure, no CORS, no session auth.
Overview
Base URL: http://localhost:24680/api
Content Type: All request and response bodies are application/json.
Response envelope
Every response uses a consistent envelope:
// Success
{
"success": true,
"data": T,
"meta"?: { "total": number, "page": number, "pageSize": number, "hasMore": boolean }
}
// Error
{
"success": false,
"error": {
"code": string, // Machine-readable: "JOB_NOT_FOUND"
"message": string, // Human-readable description
"details"?: any // Validation errors, etc.
}
}
HTTP status codes
| Status | Meaning |
|---|---|
200 | OK -- successful GET, PUT, POST (non-creation) |
201 | Created -- successful resource creation |
204 | No Content -- successful DELETE |
400 | Bad Request -- validation error or malformed input |
401 | Unauthorized -- Touch ID required but not provided |
403 | Forbidden -- attempt to modify a read-only job |
404 | Not Found -- resource does not exist |
409 | Conflict -- duplicate label, conflicting state |
422 | Unprocessable -- semantically invalid (valid JSON, bad values) |
500 | Server Error -- launchctl failure, DB error |
Authentication
Endpoints requiring Touch ID include an X-Auth-Token header with a signed WebAuthn assertion:
X-Auth-Token: <base64-encoded-webauthn-assertion>
Pagination
List endpoints accept page (1-indexed, default 1) and pageSize (default 50, max 200) query parameters.
Endpoint Map
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/jobs | -- | List jobs with filters, sorting, pagination |
| POST | /api/jobs | Touch ID | Create a new managed job |
| GET | /api/jobs/:id | -- | Get single job with full config |
| PUT | /api/jobs/:id | Touch ID | Update job config |
| DEL | /api/jobs/:id | Touch ID | Soft-delete (archive) a job |
| POST | /api/jobs/:id/run | -- | Execute job immediately |
| POST | /api/jobs/:id/pause | Touch ID | Unload job from launchd |
| POST | /api/jobs/:id/resume | Touch ID | Reload job into launchd |
| POST | /api/jobs/:id/undo | -- | Restore deleted job (30s window) |
| POST | /api/jobs/bulk | Touch ID | Batch action on multiple jobs |
| GET | /api/jobs/:id/logs | -- | Execution history for a job |
| POST | /api/discover | -- | Trigger system scan |
| GET | /api/folders | -- | List all folders |
| POST | /api/folders | -- | Create folder |
| GET | /api/tags | -- | List all tags |
| POST | /api/tags | -- | Create tag |
| GET | /api/alerts | -- | List alerts |
| GET | /api/settings | -- | Get all settings |
| PUT | /api/settings | -- | Update settings |
| GET | /api/status | -- | System health (menubar) |
Jobs
GET /api/jobs -- List Jobs
Retrieve all jobs with optional filtering, searching, and sorting.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
folderId | string | -- | Filter by folder ID |
tagId | string | -- | Filter by tag ID |
status | string | -- | running, idle, failed, disabled |
source | string | -- | dashboard, system, user |
search | string | -- | Full-text across label, name, command, tags |
sort | string | name | name, lastRun, nextRun, status, created |
order | string | asc | asc or desc |
Response
{
"success": true,
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"label": "com.launchpad.daily-backup",
"name": "Daily Backup",
"command": "/Users/alex/scripts/backup.sh",
"arguments": ["-v", "--incremental"],
"isEnabled": true,
"lastExitCode": 0,
"lastRunAt": "2026-02-27T09:00:03.000Z",
"nextRunAt": "2026-02-28T09:00:00.000Z",
"source": "dashboard",
"tags": [
{ "id": "t1", "name": "critical", "color": "#EF4444" }
],
"stats": {
"totalRuns": 42,
"successRate": 97.6,
"avgDurationMs": 12340
}
}
],
"meta": { "total": 63, "page": 1, "pageSize": 50, "hasMore": true }
}
POST /api/jobs -- Create Job
Create a new LaunchAgent. Generates a plist, writes it to ~/Library/LaunchAgents/, and registers with launchctl.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Reverse-DNS identifier (must match ^[a-zA-Z][a-zA-Z0-9._-]*$) |
name | string | Yes | Human-readable display name |
command | string | Yes | Executable path |
arguments | string[] | No | Additional arguments |
workingDirectory | string | No | Working directory for execution |
environmentVariables | object | No | Key-value env vars |
startInterval | integer | No | Interval in seconds (>= 10) |
startCalendarInterval | object or array | No | Calendar schedule (mutually exclusive with startInterval) |
watchPaths | string[] | No | File paths to watch |
runAtLoad | boolean | No | Run when loaded (default: false) |
keepAlive | boolean or object | No | Keep-alive configuration |
folderId | string | No | Folder to place job in |
tagIds | string[] | No | Tags to assign |
Error codes
| Status | Code | Message |
|---|---|---|
400 | VALIDATION_ERROR | Validation failed (with field-level details) |
409 | DUPLICATE_LABEL | A job with this label already exists |
422 | SCHEDULE_CONFLICT | Cannot set both startInterval and startCalendarInterval |
500 | PLIST_WRITE_ERROR | Failed to write plist file |
500 | LAUNCHCTL_ERROR | Failed to register job with launchctl |
POST /api/jobs/:id/run -- Execute Job
Trigger an immediate one-shot execution. Does not affect the schedule.
// Response: 200 OK
{
"success": true,
"data": {
"logId": "log-uuid-here",
"pid": 12345,
"startedAt": "2026-02-27T15:45:00.000Z",
"status": "running"
}
}
Discovery
POST /api/discover -- Scan System
Triggers a full filesystem scan of LaunchAgent directories, parses plists, queries launchctl, and reconciles with the database.
{
"success": true,
"data": {
"scannedDirectories": [
"~/Library/LaunchAgents/",
"/Library/LaunchAgents/"
],
"totalFound": 47,
"newlyDiscovered": 3,
"updated": 12,
"removed": 1,
"errors": [
{
"path": "~/Library/LaunchAgents/com.broken.plist",
"error": "Malformed XML: unexpected end of input"
}
],
"scanDuration": 1823,
"completedAt": "2026-02-27T14:30:02.000Z"
}
}
Execution Logs
GET /api/jobs/:id/logs -- List Executions
Paginated execution history for a specific job.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | running, success, failed, timeout, cancelled |
from | ISO date | Start of date range |
to | ISO date | End of date range |
{
"success": true,
"data": [
{
"id": "log-uuid-1",
"jobId": "job-uuid",
"startedAt": "2026-02-27T09:00:03.000Z",
"finishedAt": "2026-02-27T09:00:15.000Z",
"durationMs": 12340,
"exitCode": 0,
"pid": 12345,
"status": "success",
"triggerType": "schedule",
"stdoutPreview": "Backup complete: 142 files synced",
"hasFullLogs": true
}
],
"meta": { "total": 42, "page": 1, "pageSize": 50, "hasMore": false }
}
System Status
GET /api/status -- Health Summary
Used by the menubar process to determine icon color and display summary.
{
"success": true,
"data": {
"total": 47,
"running": 3,
"idle": 38,
"failed24h": 1,
"disabled": 5,
"recentFailures": [
{
"jobId": "job-uuid",
"label": "com.launchpad.api-healthcheck",
"exitCode": 1,
"failedAt": "2026-02-27T02:37:00.000Z",
"stderrExcerpt": "curl: (7) Failed to connect..."
}
],
"statusColor": "red"
}
}
Error Reference
| Code | HTTP | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Input validation failure with field-level details |
INVALID_PARAMETER | 400 | Invalid query parameter value |
AUTH_REQUIRED | 401 | Touch ID verification required but not provided |
READ_ONLY_JOB | 403 | Cannot modify a system/external job |
JOB_NOT_FOUND | 404 | Job ID does not exist |
FOLDER_NOT_FOUND | 404 | Folder ID does not exist |
TAG_NOT_FOUND | 404 | Tag ID does not exist |
DUPLICATE_LABEL | 409 | Job label already in use |
DUPLICATE_TAG | 409 | Tag name already exists |
JOB_ALREADY_RUNNING | 409 | Cannot start a job that is already executing |
JOB_ALREADY_PAUSED | 409 | Job is already unloaded |
SCHEDULE_CONFLICT | 422 | Cannot use both startInterval and startCalendarInterval |
MAX_DEPTH_EXCEEDED | 422 | Folder nesting exceeds 3 levels |
UNDO_EXPIRED | 410 | 30-second undo window has elapsed |
LAUNCHCTL_ERROR | 500 | launchctl command returned non-zero exit |
PLIST_WRITE_ERROR | 500 | Failed to write plist to disk |
SCAN_ERROR | 500 | Discovery scan failed |