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

StatusMeaning
200OK -- successful GET, PUT, POST (non-creation)
201Created -- successful resource creation
204No Content -- successful DELETE
400Bad Request -- validation error or malformed input
401Unauthorized -- Touch ID required but not provided
403Forbidden -- attempt to modify a read-only job
404Not Found -- resource does not exist
409Conflict -- duplicate label, conflicting state
422Unprocessable -- semantically invalid (valid JSON, bad values)
500Server 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

MethodEndpointAuthDescription
GET/api/jobs--List jobs with filters, sorting, pagination
POST/api/jobsTouch IDCreate a new managed job
GET/api/jobs/:id--Get single job with full config
PUT/api/jobs/:idTouch IDUpdate job config
DEL/api/jobs/:idTouch IDSoft-delete (archive) a job
POST/api/jobs/:id/run--Execute job immediately
POST/api/jobs/:id/pauseTouch IDUnload job from launchd
POST/api/jobs/:id/resumeTouch IDReload job into launchd
POST/api/jobs/:id/undo--Restore deleted job (30s window)
POST/api/jobs/bulkTouch IDBatch 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

ParameterTypeDefaultDescription
folderIdstring--Filter by folder ID
tagIdstring--Filter by tag ID
statusstring--running, idle, failed, disabled
sourcestring--dashboard, system, user
searchstring--Full-text across label, name, command, tags
sortstringnamename, lastRun, nextRun, status, created
orderstringascasc 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

FieldTypeRequiredDescription
labelstringYesReverse-DNS identifier (must match ^[a-zA-Z][a-zA-Z0-9._-]*$)
namestringYesHuman-readable display name
commandstringYesExecutable path
argumentsstring[]NoAdditional arguments
workingDirectorystringNoWorking directory for execution
environmentVariablesobjectNoKey-value env vars
startIntervalintegerNoInterval in seconds (>= 10)
startCalendarIntervalobject or arrayNoCalendar schedule (mutually exclusive with startInterval)
watchPathsstring[]NoFile paths to watch
runAtLoadbooleanNoRun when loaded (default: false)
keepAliveboolean or objectNoKeep-alive configuration
folderIdstringNoFolder to place job in
tagIdsstring[]NoTags to assign

Error codes

StatusCodeMessage
400VALIDATION_ERRORValidation failed (with field-level details)
409DUPLICATE_LABELA job with this label already exists
422SCHEDULE_CONFLICTCannot set both startInterval and startCalendarInterval
500PLIST_WRITE_ERRORFailed to write plist file
500LAUNCHCTL_ERRORFailed 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

ParameterTypeDescription
statusstringrunning, success, failed, timeout, cancelled
fromISO dateStart of date range
toISO dateEnd 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

CodeHTTPDescription
VALIDATION_ERROR400Input validation failure with field-level details
INVALID_PARAMETER400Invalid query parameter value
AUTH_REQUIRED401Touch ID verification required but not provided
READ_ONLY_JOB403Cannot modify a system/external job
JOB_NOT_FOUND404Job ID does not exist
FOLDER_NOT_FOUND404Folder ID does not exist
TAG_NOT_FOUND404Tag ID does not exist
DUPLICATE_LABEL409Job label already in use
DUPLICATE_TAG409Tag name already exists
JOB_ALREADY_RUNNING409Cannot start a job that is already executing
JOB_ALREADY_PAUSED409Job is already unloaded
SCHEDULE_CONFLICT422Cannot use both startInterval and startCalendarInterval
MAX_DEPTH_EXCEEDED422Folder nesting exceeds 3 levels
UNDO_EXPIRED41030-second undo window has elapsed
LAUNCHCTL_ERROR500launchctl command returned non-zero exit
PLIST_WRITE_ERROR500Failed to write plist to disk
SCAN_ERROR500Discovery scan failed