Files
gems/opal-task/docs/api.md
T
2026-02-15 14:58:01 +01:00

20 KiB

Opal-Task API Reference

REST API for the opal task manager. Built with Go and chi router, backed by SQLite.

Base URL: http://localhost:8080 (default) or behind a reverse proxy at /api

Table of Contents


Authentication

The API supports two authentication methods:

API Key

Generate a key with the CLI, then pass it as a Bearer token:

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 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

{
  "success": true,
  "data": { ... }
}

Error

{
  "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.

curl http://localhost:8080/health
{
  "success": true,
  "data": {
    "status": "ok"
  }
}

OAuth

GET /auth/login

Returns the OAuth authorization URL for redirecting the user to the identity provider.

curl http://localhost:8080/auth/login
{
  "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
curl -X POST "http://localhost:8080/auth/callback?code=AUTH_CODE_HERE"
{
  "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:

{
  "refresh_token": "dGhpcyBpcyBhIHJlZnJl..."
}
{
  "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:

{
  "refresh_token": "dGhpcyBpcyBhIHJlZnJl..."
}
{
  "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). 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:

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/tasks?report=overdue"
{
  "success": true,
  "data": {
    "report": "overdue",
    "tasks": [ ... ],
    "count": 3
  }
}

With filters:

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/tasks?status=pending&tag=home&priority=H"
{
  "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)
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):

{
  "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
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):

{
  "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.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890
{
  "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.

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:

{
  "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
# 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"
{
  "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.

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.

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.

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.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tags
{
  "success": true,
  "data": ["home", "urgent"]
}

POST /tasks/{uuid}/tags

Adds a tag to a task.

Request body:

{
  "tag": "important"
}

Response: The updated task object.


DELETE /tasks/{uuid}/tags/{tag}

Removes a tag from a task.

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.

curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/tags
{
  "success": true,
  "data": ["errands", "home", "important", "personal", "urgent", "work"]
}

Projects

GET /projects

Returns all project names used across all tasks.

curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/projects
{
  "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
curl -X POST http://localhost:8080/sync/changes \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"since": 1739600000, "client_id": "phone-abc123"}'
{
  "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
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"]
      }
    ]
  }'
{
  "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).

curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/auth/keys
{
  "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
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/auth/keys/1
{
  "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

# 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