Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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_casethroughout. - 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 is604800. - 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 |