Files
2026-02-15 14:58:01 +01:00

922 lines
20 KiB
Markdown

# 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 <token>` 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=<name>` 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 |