# Opal-Task API Reference REST API for the opal task manager. Built with Go and [chi](https://github.com/go-chi/chi) router, backed by SQLite. **Base URL:** `http://localhost:8080` (default) or behind a reverse proxy at `/api` ## Table of Contents - [Authentication](#authentication) - [Response Format](#response-format) - [Endpoints](#endpoints) - [Health](#health) - [OAuth](#oauth) - [Tasks](#tasks) - [Tags](#tags) - [Projects](#projects) - [Sync](#sync) - [API Keys](#api-keys) - [Data Models](#data-models) - [Reports](#reports) --- ## Authentication The API supports two authentication methods: ### API Key Generate a key with the CLI, then pass it as a Bearer token: ```bash opal server keygen --name "My Phone" # Output: oak_aBcDeFgH... (shown once, save it) ``` ``` Authorization: Bearer oak_aBcDeFgH... ``` Keys are bcrypt-hashed at rest. The `oak_` prefix identifies opal API keys. ### OAuth / JWT When OAuth is enabled, authenticate through the [login flow](#get-authlogin) to receive a JWT: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ``` JWTs are HS256-signed, issued by `opal-task`, and expire after 1 hour by default (configurable via `JWT_EXPIRY`). ### Public Endpoints These endpoints require no authentication: | Endpoint | Description | |---|---| | `GET /health` | Health check | | `GET /auth/login` | Get OAuth login URL | | `POST /auth/callback` | OAuth code exchange | | `POST /auth/refresh` | Refresh access token | | `POST /auth/logout` | Revoke refresh token | All other endpoints require a valid `Authorization: Bearer ` header. --- ## Response Format Every response follows this envelope: ### Success ```json { "success": true, "data": { ... } } ``` ### Error ```json { "success": false, "error": "description of what went wrong" } ``` ### Conventions - **JSON keys** are `snake_case` throughout. - **Timestamps** are Unix seconds (integers), not ISO 8601 strings. Nullable timestamps are `null`. - **Durations** (e.g., `recurrence_duration`) are in seconds. A 1-week recurrence is `604800`. - **Status** is a single-character string: `"P"`, `"C"`, `"D"`, or `"R"`. ### Status Codes | Code | Meaning | |---|---| | `200` | Success | | `201` | Resource created | | `400` | Invalid input (bad JSON, missing required fields, invalid UUID) | | `401` | Missing or invalid authentication | | `404` | Resource not found | | `500` | Server error | | `501` | Feature disabled (e.g., OAuth not configured) | --- ## Endpoints ### Health #### `GET /health` Returns server status. No authentication required. ```bash curl http://localhost:8080/health ``` ```json { "success": true, "data": { "status": "ok" } } ``` --- ### OAuth #### `GET /auth/login` Returns the OAuth authorization URL for redirecting the user to the identity provider. ```bash curl http://localhost:8080/auth/login ``` ```json { "success": true, "data": { "url": "https://auth.example.com/application/o/authorize/?client_id=...&state=abc123", "state": "abc123" } } ``` Returns `501` if OAuth is not enabled. --- #### `POST /auth/callback` Exchanges an OAuth authorization code for access and refresh tokens. The `code` parameter comes from the OAuth provider's redirect. **Query parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `code` | string | yes | Authorization code from OAuth provider | ```bash curl -X POST "http://localhost:8080/auth/callback?code=AUTH_CODE_HERE" ``` ```json { "success": true, "data": { "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "dGhpcyBpcyBhIHJlZnJl...", "expires_at": 1739700000, "token_type": "Bearer", "user": { "id": 1, "username": "alice", "email": "alice@example.com" } } } ``` --- #### `POST /auth/refresh` Exchanges a valid refresh token for a new access token. **Request body:** ```json { "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." } ``` ```json { "success": true, "data": { "access_token": "eyJhbGciOiJIUzI1NiIs...", "expires_at": 1739703600, "token_type": "Bearer" } } ``` Returns `401` if the refresh token is invalid or expired. --- #### `POST /auth/logout` Revokes a refresh token, preventing further use. **Request body:** ```json { "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." } ``` ```json { "success": true, "data": { "message": "logged out" } } ``` --- ### Tasks #### `GET /tasks` Lists tasks, either by named report or by filter parameters. **Query parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `report` | string | no | Named report (see [Reports](#reports)). Overrides filter params. | | `status` | string | no | Filter by status: `pending`, `completed`, `deleted`, `recurring` | | `project` | string | no | Filter by project name | | `priority` | string | no | Filter by priority: `L`, `D`, `M`, `H` | | `tag` | string[] | no | Filter by tags (repeat for multiple: `?tag=home&tag=urgent`) | **With report:** ```bash curl -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/tasks?report=overdue" ``` ```json { "success": true, "data": { "report": "overdue", "tasks": [ ... ], "count": 3 } } ``` **With filters:** ```bash curl -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/tasks?status=pending&tag=home&priority=H" ``` ```json { "success": true, "data": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "id": 42, "status": "P", "description": "Fix leaking faucet", "project": "house", "priority": 3, "created": 1739174400, "modified": 1739545800, "start": null, "end": null, "due": 1740009600, "scheduled": null, "wait": null, "until": null, "recurrence_duration": null, "parent_uuid": null, "tags": ["home", "urgent"], "urgency": 12.4 } ] } ``` --- #### `POST /tasks` Creates a new task using structured JSON fields. **Request body:** | Field | Type | Required | Description | |---|---|---|---| | `description` | string | yes | Task description | | `tags` | string[] | no | Tags to attach | | `project` | string | no | Project name | | `priority` | string | no | `L` (low), `D` (default), `M` (medium), `H` (high) | | `due` | int64 | no | Due date as Unix timestamp (seconds) | | `scheduled` | int64 | no | Scheduled date as Unix timestamp | | `wait` | int64 | no | Wait-until date as Unix timestamp | | `until` | int64 | no | Expiration date as Unix timestamp | | `recurrence` | string | no | Recurrence interval (e.g., `1d`, `2w`, `3m`, `1y`) | ```bash curl -X POST http://localhost:8080/tasks \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Buy groceries", "tags": ["personal", "errands"], "project": "household", "priority": "M", "due": 1739836800 }' ``` **Response** (`201 Created`): ```json { "success": true, "data": { "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "id": 43, "status": "P", "description": "Buy groceries", "project": "household", "priority": 2, "created": 1739750400, "modified": 1739750400, "due": 1739836800, "tags": ["personal", "errands"], "urgency": 6.1 } } ``` --- #### `POST /tasks/parse` Creates a task from a CLI-style input string. Supports the same syntax as the `opal add` command: words without special prefixes become the description, `+tag` adds tags, `-tag` removes tags, and `key:value` pairs set attributes. **Request body:** | Field | Type | Required | Description | |---|---|---|---| | `input` | string | yes | CLI-style task input | **Modifier syntax:** | Syntax | Meaning | Example | |---|---|---| | `+tag` | Add tag | `+home` | | `-tag` | Remove tag | `-garden` | | `project:name` | Set project | `project:household` | | `priority:X` | Set priority | `priority:H` | | `due:value` | Set due date | `due:tomorrow`, `due:monday`, `due:2026-03-01` | | `scheduled:value` | Set scheduled date | `scheduled:nextweek` | | `wait:value` | Set wait date | `wait:friday` | | `until:value` | Set expiration | `until:eom` | | `recur:interval` | Set recurrence | `recur:1w`, `recur:2d` | ```bash curl -X POST http://localhost:8080/tasks/parse \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"input": "Change bed sheets +home project:household due:sunday recur:1w"}' ``` **Response** (`201 Created`): ```json { "success": true, "data": { "task": { "uuid": "c9bf9e57-1685-4c89-bafb-ff5af830be8a", "id": 44, "status": "P", "description": "Change bed sheets", "project": "household", "priority": 1, "created": 1739750400, "modified": 1739750400, "due": 1739836800, "recurrence_duration": 604800, "parent_uuid": "d4e5f6a7-b8c9-0123-4567-890abcdef012", "tags": ["home"], "urgency": 5.3 } } } ``` For recurring tasks (those with `recur:`), the API creates a template task (status `"R"`) and returns the first instance. --- #### `GET /tasks/{uuid}` Returns a single task by its UUID. ```bash curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` ```json { "success": true, "data": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "id": 42, "status": "P", "description": "Fix leaking faucet", "project": "house", "priority": 3, "created": 1739174400, "modified": 1739545800, "due": 1740009600, "tags": ["home", "urgent"], "urgency": 12.4 } } ``` Returns `404` if the UUID does not match any task. --- #### `PUT /tasks/{uuid}` Updates one or more fields on an existing task. Only include the fields you want to change. **Request body:** | Field | Type | Description | |---|---|---| | `description` | string | New description | | `status` | string | New status: `pending`, `completed`, `deleted` | | `priority` | string | `L`, `D`, `M`, `H` | | `project` | string | Project name | | `due` | int64 | Unix timestamp (seconds) | | `scheduled` | int64 | Unix timestamp | | `wait` | int64 | Unix timestamp | | `until` | int64 | Unix timestamp | | `start` | int64 | Unix timestamp | | `recurrence` | string | Recurrence interval | All fields are optional. ```bash curl -X PUT http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"priority": "H", "due": 1739923200}' ``` **Response:** ```json { "success": true, "data": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "id": 42, "status": "P", "description": "Fix leaking faucet", "project": "house", "priority": 3, "created": 1739174400, "modified": 1739750400, "due": 1739923200, "tags": ["home", "urgent"], "urgency": 14.7 } } ``` --- #### `DELETE /tasks/{uuid}` Deletes a task. By default, sets the task status to `"D"` (soft delete). Pass `permanent=true` to remove it from the database entirely. **Query parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `permanent` | string | `false` | Set to `true` for permanent deletion | ```bash # Soft delete curl -X DELETE -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 # Permanent delete curl -X DELETE -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890?permanent=true" ``` ```json { "success": true, "data": { "message": "task deleted" } } ``` --- #### `POST /tasks/{uuid}/complete` Marks a task as completed. Sets the status to `"C"` and records the completion time. For recurring task instances, this may trigger creation of the next instance. ```bash curl -X POST -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/complete ``` **Response:** The updated task object with `"status": "C"`. --- #### `POST /tasks/{uuid}/start` Marks a task as actively being worked on by setting its `start` timestamp to now. ```bash curl -X POST -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/start ``` **Response:** The updated task object with `start` set to the current unix timestamp. --- #### `POST /tasks/{uuid}/stop` Clears the `start` timestamp, marking the task as no longer actively being worked on. ```bash curl -X POST -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/stop ``` **Response:** The updated task object with `start` cleared to `null`. --- ### Task Tags #### `GET /tasks/{uuid}/tags` Returns the tag list for a specific task. ```bash curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tags ``` ```json { "success": true, "data": ["home", "urgent"] } ``` --- #### `POST /tasks/{uuid}/tags` Adds a tag to a task. **Request body:** ```json { "tag": "important" } ``` **Response:** The updated task object. --- #### `DELETE /tasks/{uuid}/tags/{tag}` Removes a tag from a task. ```bash curl -X DELETE -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tags/urgent ``` **Response:** The updated task object. --- ### Tags #### `GET /tags` Returns all tags used across all tasks. ```bash curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/tags ``` ```json { "success": true, "data": ["errands", "home", "important", "personal", "urgent", "work"] } ``` --- ### Projects #### `GET /projects` Returns all project names used across all tasks. ```bash curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/projects ``` ```json { "success": true, "data": ["household", "work", "garden"] } ``` --- ### Sync These endpoints power the multi-device sync protocol. The CLI client uses them via `opal sync` commands. #### `POST /sync/changes` Returns all changes recorded since a given timestamp. Used by clients to pull updates from the server. **Request body:** | Field | Type | Required | Description | |---|---|---|---| | `since` | int64 | yes | Unix timestamp (seconds). Return changes after this time. Use `0` for initial sync. | | `client_id` | string | yes | Unique identifier for the syncing device | ```bash curl -X POST http://localhost:8080/sync/changes \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"since": 1739600000, "client_id": "phone-abc123"}' ``` ```json { "success": true, "data": [ { "id": 101, "task_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "change_type": "create", "changed_at": 1739650000, "data": "description:Buy groceries\nstatus:80\npriority:2" }, { "id": 102, "task_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "change_type": "update", "changed_at": 1739660000, "data": "status:67" } ] } ``` The `change_type` is one of `create`, `update`, or `delete`. The `data` field uses a `key:value` format with newline separators, recorded by database triggers on every task mutation. Note that change log data uses raw database values (integer status codes), not the API's serialized format. --- #### `POST /sync/push` Pushes local task changes to the server. Conflicts are resolved with **last-write-wins** based on the `modified` timestamp. **Request body:** | Field | Type | Required | Description | |---|---|---|---| | `tasks` | Task[] | yes | Array of full task objects to push | | `client_id` | string | yes | Unique identifier for the syncing device | ```bash curl -X POST http://localhost:8080/sync/push \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "phone-abc123", "tasks": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "P", "description": "Buy groceries", "priority": 2, "created": 1739600000, "modified": 1739650000, "tags": ["personal"] } ] }' ``` ```json { "success": true, "data": { "processed": 1, "conflicts": 0 } } ``` - **processed** — number of tasks successfully applied - **conflicts** — number of tasks where the server had a newer version (still applied via last-write-wins) --- ### API Keys #### `GET /auth/keys` Lists all API keys for the current user. The key value itself is not returned (it is only shown once at creation time). ```bash curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/auth/keys ``` ```json { "success": true, "data": [ { "id": 1, "name": "My Phone", "user_id": 1, "created_at": 1736935200, "last_used": 1739558400, "revoked": false } ] } ``` --- #### `DELETE /auth/keys/{id}` Revokes an API key. The key becomes immediately unusable. **URL parameters:** | Parameter | Type | Description | |---|---|---| | `id` | int | API key ID | ```bash curl -X DELETE -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/auth/keys/1 ``` ```json { "success": true, "data": { "message": "API key revoked" } } ``` --- ## Data Models ### Task | Field | Type | Description | |---|---|---| | `uuid` | string | Unique identifier (UUID v4) | | `id` | int | Auto-increment database ID | | `status` | string | `"P"` (pending), `"C"` (completed), `"D"` (deleted), `"R"` (recurring) | | `description` | string | Task description | | `project` | string \| null | Project name | | `priority` | int | `0` = Low, `1` = Default, `2` = Medium, `3` = High | | `created` | int | Unix timestamp (seconds) | | `modified` | int | Unix timestamp (seconds) | | `start` | int \| null | When the task was started (actively being worked on) | | `end` | int \| null | When the task was completed or deleted | | `due` | int \| null | Deadline | | `scheduled` | int \| null | Earliest date the task is actionable | | `wait` | int \| null | Task is hidden until this date | | `until` | int \| null | Task auto-deletes after this date | | `recurrence_duration` | int \| null | Recurrence interval in seconds (e.g., `604800` = 1 week) | | `parent_uuid` | string \| null | UUID of the recurring template task | | `tags` | string[] | Attached tags | | `urgency` | float | Computed urgency score (higher = more urgent) | #### Status Values | Value | Meaning | |---|---| | `"P"` | Pending — active, not yet completed | | `"C"` | Completed | | `"D"` | Deleted (soft delete) | | `"R"` | Recurring template | #### Priority Values | API Input | Numeric Value | Meaning | |---|---|---| | `L` | `0` | Low | | `D` | `1` | Default | | `M` | `2` | Medium | | `H` | `3` | High | Use the letter codes (`L`, `D`, `M`, `H`) when creating or updating tasks. Responses return the numeric value. --- ## Reports Named reports return pre-filtered, pre-sorted task lists. Pass the report name as `?report=` on `GET /tasks`. | Report | Description | |---|---| | `active` | Tasks that have been started (pending with a `start` time) | | `all` | All tasks including recurring templates | | `completed` | Completed tasks | | `list` | Pending tasks (default view) | | `minimal` | Minimal output view | | `newest` | Pending tasks sorted newest first | | `next` | Next task due | | `oldest` | Pending tasks sorted oldest first | | `overdue` | Tasks past their due date | | `ready` | Tasks ready to work on (past scheduled date, not waiting) | | `recurring` | Recurring template tasks | | `template` | Alias for `recurring` | | `waiting` | Tasks with a future `wait` date | --- ## CORS The API allows cross-origin requests: - **Origins:** `*` - **Methods:** `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS` - **Headers:** `Content-Type`, `Authorization` --- ## Running the Server ```bash # Build go build -o opal main.go # Generate an API key ./opal server keygen --name "My Device" --db /path/to/opal.db # Start the server ./opal server start --addr :8080 --db /path/to/opal.db ``` ### Environment Variables | Variable | Default | Description | |---|---|---| | `OAUTH_ENABLED` | `false` | Enable OAuth authentication | | `OAUTH_CLIENT_ID` | — | OAuth client ID | | `OAUTH_CLIENT_SECRET` | — | OAuth client secret | | `OAUTH_ISSUER` | — | OAuth issuer URL | | `OAUTH_REDIRECT_URI` | — | OAuth redirect URI | | `JWT_SECRET` | — | Secret for signing JWTs | | `JWT_EXPIRY` | `3600` | JWT lifetime in seconds | | `REFRESH_TOKEN_EXPIRY` | `604800` | Refresh token lifetime in seconds (default 7 days) | | `OPAL_DB_PATH` | XDG data dir | Override database file path | | `OPAL_CONFIG_DIR` | `~/.config/opal` | Config directory | | `OPAL_DATA_DIR` | `~/.local/share/opal` | Data directory |