Skip to main content

Tags, Categories, Queries, MRQL & More

This page covers Tags, Categories, Resource Categories, Template Partials, Queries, MRQL, Search, Logs, Download Queue, and Users, Accounts & Authentication endpoints.


Personal saved searches

Saved list searches are available to every authenticated role, including guests. When authentication is disabled, the root account owns them. Session requests that create, update, or delete searches require the usual CSRF token; bearer authentication follows the existing account API rules.

MethodEndpointBehavior
GET/v1/account/saved-searches?family=resourcesList your searches alphabetically; omit family to list all
POST/v1/account/saved-searchesCreate from {"name":"Recent files","url":"/resources/details?SortBy=CreatedAt%20desc"}; returns 201
PATCH/v1/account/saved-searches/{id}Update name, url, or both; returns the updated record
DELETE/v1/account/saved-searches/{id}Delete your search; returns 204

Records contain id, name, family, url, layout, createdAt, and updatedAt. Ownership is derived from the request and cannot be supplied. Another user's record, or a missing record, returns 404.

Names are trimmed, required, and limited to 200 UTF-8 bytes; duplicate names are allowed. URLs are limited to 64 KiB and must point to a supported relative list path. External URLs, fragments, detail pages, API routes, and .json/.body destinations are rejected. Replacements must stay within the same list family. Invalid input returns 400.

Supported families are resources, notes, groups, tags, categories, resourceCategories, noteTypes, relations, relationTypes, queries, templatePartials, downloads, logs, and reductions. Alternate list layouts share their base family.

Saving preserves repeated filter values, sort precedence, metadata filters, and MRQL text. Pagination and transient navigation/error state are removed; Downloads' Error filter is preserved. Timeline URLs additionally use timelineMode (created or updated), timelineGranularity (week, month, or year), and timelineAnchor (YYYY-MM-DD). Opening the stored URL uses normal list authorization and reruns the search against current data.

Tags API

Tags are labels that can be applied to resources, notes, and groups for organization.

List Tags

GET /v1/tags

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name (partial match)
DescriptionstringFilter by description
CreatedBeforestringFilter by creation date
CreatedAfterstringFilter by creation date
SortBystring[]Sort order

Example

curl http://localhost:8181/v1/tags

# Search for tags
curl "http://localhost:8181/v1/tags?Name=project"

Response

[
{
"ID": 1,
"Name": "important",
"Description": "High priority items",
"CreatedAt": "2024-01-01T00:00:00Z"
}
]

Suggest Tags

Typeahead listing. Accepts the same filters as GET /v1/tags, but skips the pagination COUNT, so it returns no X-Total-Count header.

GET /v1/tags/suggest

Example

curl "http://localhost:8181/v1/tags/suggest?Name=proj"

Create or Update Tag

POST /v1/tag

Parameters

ParameterTypeDescription
IDintegerTag ID (include to update)
NamestringTag name
DescriptionstringDescription

Example

# Create
curl -X POST http://localhost:8181/v1/tag \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"Name": "urgent", "Description": "Requires immediate attention"}'

# Update
curl -X POST http://localhost:8181/v1/tag \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"ID": 1, "Name": "critical", "Description": "Updated description"}'

Delete Tag

POST /v1/tag/delete?Id={id}

Bulk Delete Tags

Delete multiple tags at once.

POST /v1/tags/delete

Parameters

ParameterTypeDescription
IDinteger[]Tag IDs to delete

Merge Tags

Merge multiple tags into one, transferring all associations.

POST /v1/tags/merge

Parameters

ParameterTypeDescription
WinnerintegerTag ID to keep
Losersinteger[]Tag IDs to merge and delete

Inline Editing

POST /v1/tag/editName?id={id}
POST /v1/tag/editDescription?id={id}

Timeline

GET /v1/tags/timeline

See Timeline Endpoints below for details.


Categories API

Categories define types for groups with optional display customizations.

List Categories

GET /v1/categories

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name
DescriptionstringFilter by description

Example

curl http://localhost:8181/v1/categories

Response

[
{
"ID": 1,
"Name": "Person",
"Description": "Individual people",
"CustomHeader": "...",
"CustomSidebar": "...",
"CustomSummary": "...",
"CustomAvatar": "...",
"MetaSchema": "..."
}
]

Create or Update Category

POST /v1/category

Parameters

ParameterTypeDescription
IDintegerCategory ID (include to update)
NamestringCategory name
DescriptionstringDescription
CustomHeaderstringCustom header template
CustomSidebarstringCustom sidebar template
CustomSummarystringCustom summary template
CustomAvatarstringCustom avatar template
CustomListHeaderstringCustom list-page header template
CustomDetailFooterstringTemplate rendered at the bottom of the detail page
CustomListFooterstringCustom list-page footer template (carrier-bound, like CustomListHeader)
CustomHoverCardstringHover-card template; falls back to CustomSummary when empty
CustomOwnEntitiesstringReplaces the body of the group detail page's Own Entities section
CustomMRQLResultstringCustom MRQL result-card template
CustomCSSstringCustom CSS injected on category pages
SectionConfigstringJSON section layout configuration
MetaSchemastringJSON schema for metadata validation

Example

curl -X POST http://localhost:8181/v1/category \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Name": "Company",
"Description": "Business organizations"
}'

Delete Category

POST /v1/category/delete?Id={id}

Inline Editing

POST /v1/category/editName?id={id}
POST /v1/category/editDescription?id={id}

Resource Categories API

Resource categories define types for resources with optional display customizations and metadata schemas.

List Resource Categories

GET /v1/resourceCategories

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name
DescriptionstringFilter by description

Example

curl http://localhost:8181/v1/resourceCategories

Response

[
{
"ID": 1,
"Name": "Photo",
"Description": "Photograph files",
"CustomHeader": "...",
"CustomSidebar": "...",
"CustomSummary": "...",
"CustomAvatar": "...",
"MetaSchema": "..."
}
]

Create or Update Resource Category

POST /v1/resourceCategory

Parameters

ParameterTypeDescription
IDintegerResource category ID (include to update)
NamestringResource category name
DescriptionstringDescription
CustomHeaderstringCustom header template
CustomSidebarstringCustom sidebar template
CustomSummarystringCustom summary template
CustomAvatarstringCustom avatar template
CustomListHeaderstringCustom list-page header template
CustomDetailFooterstringTemplate rendered at the bottom of the detail page
CustomListFooterstringCustom list-page footer template (carrier-bound, like CustomListHeader)
CustomHoverCardstringHover-card template; falls back to CustomSummary when empty
CustomPreviewstringTemplate rendered above the built-in preview image
CustomLightboxstringLightbox details-panel template; falls back to CustomSidebar when empty
CustomCellstringExtra table-cell template for the resources details table
CustomMRQLResultstringCustom MRQL result-card template
CustomCSSstringCustom CSS injected on resource category pages
SectionConfigstringJSON section layout configuration
AutoDetectRulesstringJSON rules for auto-assigning this category to resources
MetaSchemastringJSON schema for metadata validation

Example

curl -X POST http://localhost:8181/v1/resourceCategory \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Name": "Photo",
"Description": "Photograph files"
}'

Delete Resource Category

POST /v1/resourceCategory/delete?Id={id}

Inline Editing

POST /v1/resourceCategory/editName?id={id}
POST /v1/resourceCategory/editDescription?id={id}

Template Partials API

Template partials are named, reusable template fragments that any carrier's Custom* slot can include. See Custom Templates.

With -auth enabled, create, edit and delete require the admin role. Reads are open to any authenticated principal.

List Template Partials

GET /v1/templatePartials

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name
DescriptionstringFilter by description

Example

curl http://localhost:8181/v1/templatePartials

Create or Update Template Partial

POST /v1/templatePartial
POST /v1/templatePartial/edit

Parameters

ParameterTypeDescription
IDintegerPartial ID (include to update)
NamestringPartial name (kebab-case)
DescriptionstringDescription
ContentstringTemplate body

Example

curl -X POST http://localhost:8181/v1/templatePartial \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"Name": "person-card", "Description": "Shared card markup", "Content": "<div class=\"card\"></div>"}'

Delete Template Partial

POST /v1/templatePartial/delete?Id={id}

Inline Editing

POST /v1/templatePartial/editDescription?id={id}

There is no editName route: a partial's name must stay kebab-case, so renames go through the create/update path, which validates it.

List Template Presets

Return the built-in starter template bundles. Read-only and open.

GET /v1/templatePresets

Queries API

Queries are saved SQL queries that can be executed to generate custom reports. For database-level write protection, configure DB_READONLY_DSN as a truly read-only connection; otherwise query execution is not database-enforced read-only.

List Queries

GET /v1/queries

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
NamestringFilter by name
TextstringFilter by query text

Example

curl http://localhost:8181/v1/queries

Get Single Query

GET /v1/query?id={id}

Create or Update Query

POST /v1/query

Parameters

ParameterTypeDescription
IDintegerQuery ID (include to update)
NamestringQuery name
TextstringSQL query text
TemplatestringDisplay template
DescriptionstringQuery description

Example

curl -X POST http://localhost:8181/v1/query \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Name": "Recent Resources",
"Text": "SELECT * FROM resources ORDER BY created_at DESC LIMIT 10"
}'

Delete Query

POST /v1/query/delete?Id={id}

Run Query

Execute a saved query and get results.

POST /v1/query/run

Query Parameters

ParameterTypeDescription
idintegerQuery ID to run
namestringQuery name to run (alternative to id)

Example

# Run by ID
curl -X POST "http://localhost:8181/v1/query/run?id=1" \
-H "Accept: application/json"

# Run by name
curl -X POST "http://localhost:8181/v1/query/run?name=Recent%20Resources" \
-H "Accept: application/json"

The response is {"columns": [...], "rows": [[...], ...]} -- the SELECT list in its own order, and one array of values per row, index-aligned with it. See Saved Queries for the repeated-column and column-type rules, and for what changed from the previous array-of-objects shape.

Get Database Schema

Return the database table and column schema. Useful for writing saved queries.

GET /v1/query/schema

Example

curl http://localhost:8181/v1/query/schema

The response carries a Cache-Control: max-age=300 header, so HTTP clients and proxies may cache it for 5 minutes. This is an HTTP caching hint, not a server-side cache.

Inline Editing

POST /v1/query/editName?id={id}
POST /v1/query/editDescription?id={id}

Global Search API

Search across entity types: resources, notes, groups, tags, categories, queries, relation types, note types, resource categories, and saved MRQL queries.

GET /v1/search

Query Parameters

ParameterTypeDescription
qstringRequired. Search query
limitintegerMaximum results (default: 20, effective max: 50 -- larger values are clamped)
typesstringEntity types to search (comma-separated: resource, note, group, tag, category, query, relationType, noteType, resourceCategory, mrqlQuery)

Example

# Search everything
curl "http://localhost:8181/v1/search?q=project"

# Search with limit
curl "http://localhost:8181/v1/search?q=project&limit=100"

# Search specific types
curl "http://localhost:8181/v1/search?q=project&types=resource,note"

Response

{
"query": "project",
"total": 15,
"results": [
{
"id": 1,
"type": "group",
"name": "Project Alpha",
"description": "Main project group",
"score": 100,
"url": "/group?id=1",
"extra": {"category": "Project"}
},
{
"id": 5,
"type": "resource",
"name": "project-plan.pdf",
"description": "Project planning document",
"score": 85,
"url": "/resource?id=5"
}
]
}

The search stops collecting at a ceiling that is never below limit and never above 50, and total is counted after that trim, so it saturates at the ceiling. When it does, the response also carries "totalCapped": true, meaning total is a floor rather than an exact count; the field is omitted otherwise.


Logs API

Query the audit log of system events and entity changes. With -auth enabled these endpoints are admin-only.

List Log Entries

GET /v1/logs

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
LevelstringFilter by level (info, warning, error)
ActionstringFilter by action (create, update, delete, system, progress, plugin, reset)
EntityTypestringFilter by entity type
EntityIDintegerFilter by entity ID
MessagestringSearch in message (partial match)
RequestPathstringSearch in request path
CreatedBeforestringFilter by date
CreatedAfterstringFilter by date
SortBystring[]Sort order

Example

# List recent logs
curl http://localhost:8181/v1/logs

# Filter by action
curl "http://localhost:8181/v1/logs?Action=create"

# Filter by entity type
curl "http://localhost:8181/v1/logs?EntityType=resource"

# Filter errors only
curl "http://localhost:8181/v1/logs?Level=error"

Response

{
"logs": [
{
"id": 100,
"createdAt": "2024-01-15T10:30:00Z",
"level": "info",
"action": "create",
"entityType": "resource",
"entityId": 456,
"entityName": "photo.jpg",
"message": "Resource created: photo.jpg",
"requestPath": "/v1/resource"
}
],
"totalCount": 1500,
"page": 1,
"perPage": 50
}

Get Single Log Entry

GET /v1/log?id={id}

Get Entity History

Get all log entries for a specific entity.

GET /v1/logs/entity

Query Parameters

ParameterTypeDescription
entityTypestringRequired. Entity type (tag, note, resource, group)
entityIdintegerRequired. Entity ID
pageintegerPage number (default: 1)

Example

# Get history for a specific resource
curl "http://localhost:8181/v1/logs/entity?entityType=resource&entityId=123"

Download Queue API

Queue background downloads for remote resources. The queue holds up to 100 jobs and runs as many concurrently as -max-job-concurrency allows (default 6), a budget shared with group export and import jobs. In the in-memory queue, completed, failed, and cancelled jobs are retained for 1 hour before eviction; paused jobs are retained for 24 hours. A finished download also leaves a durable row behind, which the Download History endpoints below read.

The canonical route family is /v1/jobs/*. Legacy /v1/download/* aliases remain available for compatibility.

With -auth enabled, the queue listing, GET /v1/jobs/get, the job control endpoints and the SSE stream show only the jobs the caller submitted; admins see all of them, and a job belonging to someone else answers 404 rather than 403.

Submit Download

Add URLs to the download queue.

POST /v1/jobs/download/submit

Parameters

Same as POST /v1/resource/remote, but always queues for background download.

Submit multiple URLs by separating them with newlines in the URL field. Each URL becomes a separate job.

Example

curl -X POST http://localhost:8181/v1/jobs/download/submit \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"URL": "https://example.com/file1.zip\nhttps://example.com/file2.zip",
"OwnerId": 5
}'

Response

202 Accepted, with one job object per URL, so the caller learns the job ids to poll:

{
"queued": true,
"jobs": [
{
"id": "job-123",
"url": "https://example.com/file1.zip",
"status": "pending",
"source": "download"
}
]
}

The request answers 400 when the URL field is empty or no URL in it parses, 403 when a group-limited caller names an owner, group or note outside its subtree, and 503 when the queue is full.

Legacy alias: POST /v1/download/submit

Get Download Queue

Get all download jobs.

GET /v1/jobs/queue

Example

curl http://localhost:8181/v1/jobs/queue

Legacy alias: GET /v1/download/queue

Response

{
"jobs": [
{
"id": "job-123",
"url": "https://example.com/file.zip",
"status": "downloading",
"progress": 471859,
"totalSize": 1048576,
"progressPercent": 45.0,
"createdAt": "2024-01-15T10:00:00Z",
"source": "download"
}
]
}

progress is the number of bytes downloaded and progressPercent is progress / totalSize * 100, or -1 when the total size is unknown.

Job Operations

EndpointDescription
POST /v1/jobs/cancel?id={job_id}Cancel a job that has not finished -- pending, downloading, processing or paused. A finished job answers 409 Conflict; an unknown id answers 404.
POST /v1/jobs/pause?id={job_id}Pause a download job. A job in a state that cannot be paused answers 409 Conflict.
POST /v1/jobs/resume?id={job_id}Resume a paused download (restarts from the beginning)
POST /v1/jobs/retry?id={job_id}Retry a failed or cancelled download
GET /v1/jobs/get?id={job_id}Return one job snapshot by id. Answers 404 for an unknown id or a job the caller may not see. No legacy alias.
POST /v1/jobs/clearCompletedDismiss every finished job the caller can see; active and paused jobs are kept. Answers {"cleared": N, "ids": ["<job id>", ...]}, naming the jobs that were removed -- which rows are finished is decided when the request is handled, so a caller cannot work it out from its own earlier view of the queue.

Downloads can fail due to network errors, connection timeouts (default 30s), idle timeouts (default 60s), or exceeding the overall timeout (default 30m). Configure these with the -remote-connect-timeout, -remote-idle-timeout, and -remote-overall-timeout flags.

Legacy aliases remain available under /v1/download/cancel, /v1/download/pause, /v1/download/resume, and /v1/download/retry.

Download Events (SSE)

Stream real-time download status updates via Server-Sent Events.

GET /v1/jobs/events

Example

The server emits named events, so you must use addEventListener (not onmessage):

EventDescription
initFull initial state with all current jobs ({ jobs: [...], actionJobs: [...] })
addedA new download job was queued
updatedA download job changed status or progress
removedA download job was removed from the queue
action_addedA plugin action job was created
action_updatedA plugin action job changed status
action_removedA plugin action job was removed
const eventSource = new EventSource('http://localhost:8181/v1/jobs/events');

// Receive full initial state (all current jobs)
eventSource.addEventListener('init', (event) => {
const { jobs, actionJobs } = JSON.parse(event.data);
console.log('Initial download jobs:', jobs);
console.log('Initial action jobs:', actionJobs);
});

// Download job updates
eventSource.addEventListener('added', (event) => {
const { type, job } = JSON.parse(event.data);
console.log('New download job:', job);
});

eventSource.addEventListener('updated', (event) => {
const { type, job } = JSON.parse(event.data);
console.log('Download job updated:', job.id, job.status, job.progressPercent + '%');
});

eventSource.addEventListener('removed', (event) => {
const { type, job } = JSON.parse(event.data);
console.log('Download job removed:', job.id);
});

// Plugin action job updates (prefixed with "action_")
eventSource.addEventListener('action_updated', (event) => {
const { job } = JSON.parse(event.data);
console.log('Action job updated:', job);
});

Legacy alias: GET /v1/download/events

Download History

The durable record behind the /downloads page. Every job whose source is download is written here when it reaches a terminal state, so it outlives the in-memory queue's 100-job cap, its eviction sweep and a restart. Group export, import and plugin action jobs are not recorded. Rows are scoped to the caller unless the caller is an admin.

GET /v1/downloads

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
Statusstring[]Filter by status (completed, failed, cancelled); empty matches all
URLstringPartial match over URL and resource name
Retriedstringyes keeps only downloads that were run again (retried in place, or resubmitted from the stored payload); no keeps only those that were not; empty matches all
CreatedBeforestringFilter by when the download was submitted
CreatedAfterstringFilter by when the download was submitted
CompletedBeforestringOnly downloads that finished at or before this instant. Bounds the finish time, not the submission time; a download that never finished is outside every such window. A bare YYYY-MM-DD is midnight at the start of that day, so it excludes the day itself -- pass the next day, or an RFC 3339 instant, to include it
CompletedAfterstringOnly downloads that finished on or after this date. A bare YYYY-MM-DD names a calendar day in the database's own time zone; an RFC 3339 value names an instant. Use the second when the distinction matters
ReasonstringOnly downloads whose stored error falls in the named failure bucket: http, timeout, blocked, unsupported, limit, storage, cancelled, or other for a failure none of those claim. The buckets match the error text, so they are best-effort and they overlap — an HTTP 504: Gateway Timeout is in both http and timeout, which is the useful answer to either question. A download that carries no error is in no bucket, other included. Empty matches all
ErrorstringSubstring match over the stored error text, case-insensitive (ASCII-folded on SQLite, as every text filter here is). AND-ed with Reason
SortBystring[]Sort order

Response

{
"downloads": [],
"count": 0,
"page": 1,
"pageSize": 50
}
POST /v1/downloads/retry
POST /v1/downloads/delete

Both take a list of history row ids and report an outcome per id:

curl -X POST http://localhost:8181/v1/downloads/retry \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"ids": [12, 13]}'

A retry re-runs the row in place when its job is still in the queue and otherwise resubmits it from the stored payload, which is re-validated against the retrying principal's own scope. It is refused for a completed row, for a row whose retry is already claiming or running, and while any queued or running job is already fetching the same URL. Delete removes the queue entry along with the row. When no id could be acted on, the response is 409 with the per-id outcomes still attached.


Series API

A series groups related resources into an ordered sequence (e.g., pages of a scanned document, frames of an animation).

List Series

GET /v1/seriesList

Query Parameters

ParameterTypeDescription
pageintegerPage number for pagination
NamestringFilter by name (partial match)
SlugstringFilter by slug
CreatedBeforestringFilter by creation date (ISO 8601)
CreatedAfterstringFilter by creation date (ISO 8601)
SortBystring[]Sort order

Get Single Series

GET /v1/series?id={id}

Create Series

POST /v1/series/create

Parameters

ParameterTypeDescription
NamestringRequired. Series name
SlugstringOptional slug; defaults to the trimmed name

Update Series

POST /v1/series

Parameters

ParameterTypeDescription
IDintegerRequired. Series ID
NamestringNew name
MetastringJSON metadata

Delete Series

POST /v1/series/delete?Id={id}

Inline Editing

POST /v1/series/editName?id={id}

Remove Resource from Series

POST /v1/resource/removeSeries?id={resourceId}

Removes a resource from its series without deleting the series itself.


Meta Keys Endpoints

Get all unique metadata keys used across entities.

EntityEndpoint
ResourcesGET /v1/resources/meta/keys
NotesGET /v1/notes/meta/keys
GroupsGET /v1/groups/meta/keys

Each returns an array of objects, one per metadata key, with the key under a key field:

[{"key": "author"}, {"key": "source"}, {"key": "date_created"}, {"key": "location"}]

MRQL API

Execute, validate, and manage MRQL queries. See the MRQL feature page for the full query language reference.

Execute Query

POST /v1/mrql

Request Body

FieldTypeDescription
querystringMRQL query expression (required)
limitintegerMax results (flat) or items per bucket (grouped)
pageintegerPage number
bucketsintegerBuckets per page (grouped mode only)
offsetintegerDirect offset for cursor-based bucket paging
paramsobjectBindings for the query's $name placeholders; JSON body only

The same placeholders can also be bound with param.<name> form fields or query-string parameters, which win over params on a key collision.

Query Parameters

ParameterDescription
render=1Process CustomMRQLResult templates server-side and populate renderedHTML on each entity

Example

curl -X POST http://localhost:8181/v1/mrql \
-H "Content-Type: application/json" \
-d '{"query": "type = resource AND tags = \"photo\" ORDER BY created DESC", "limit": 20}'

Returns an MRQLResult for flat queries (with resources, notes, groups arrays) or an MRQLGroupedResult for GROUP BY queries (with mode, buckets, and rows fields).

Validate Query

POST /v1/mrql/validate

Request Body

FieldTypeDescription
querystringMRQL query expression

Response

{
"valid": true,
"errors": []
}

When invalid, errors contains objects with position and message details.

Autocomplete

POST /v1/mrql/complete

Request Body

FieldTypeDescription
querystringPartial MRQL query
cursorintegerCursor position in the query string

Returns context-aware completion suggestions for the cursor position.

Explain Query

Return the SQL a query would run, without executing it. See MRQL.

POST /v1/mrql/explain

Request Body

FieldTypeDescription
querystringMRQL query expression
idintegerSaved query ID (alternative to query)
namestringSaved query name (alternative to query)
paramsobjectBindings for the query's $name placeholders
nativePlanbooleanAlso run the database's own query planner on the generated SQL. Admin-only under -auth; a non-admin request answers 403

Export Results

Stream query results as a downloadable file. See MRQL.

GET|POST /v1/mrql/export
ParameterDescription
queryMRQL query expression
id / nameSaved query to export instead of inline text
formatcsv (default) or json; a ?format= query parameter wins over the body

Generate Query

Draft an MRQL query from a natural-language prompt. Requires DEEPSEEK_API_KEY.

POST /v1/mrql/generate

Request Body

FieldTypeDescription
promptstringRequired. Natural-language description of the query

List Saved Queries

GET /v1/mrql/saved
GET /v1/mrql/saved?id=N

Without id, returns a paginated list of all saved MRQL queries. With id, returns a single saved query.

ParameterDescription
idSaved query ID (returns single query)
all=1Return all saved queries without pagination
pagePage number for paginated listing

Create Saved Query

POST /v1/mrql/saved
FieldTypeRequiredDescription
namestringYesQuery name
querystringYesMRQL query expression
descriptionstringNoQuery description

Returns 201 with the created saved query object.

Update Saved Query

PUT /v1/mrql/saved?id=N

Same body fields as create. Returns the updated saved query.

Delete Saved Query

POST /v1/mrql/saved/delete?id=N

Returns {"id": N} on success.

Run Saved Query

POST /v1/mrql/saved/run
ParameterDescription
idSaved query ID
nameSaved query name (fallback if id not found)
limitMax results
pagePage number
bucketsBuckets per page (grouped mode)
offsetDirect offset
paramsBindings for the query's $name placeholders; JSON body only (param.<name> fields and query-string parameters bind the same placeholders and win on collision)
render=1Process CustomMRQLResult templates server-side

Looks up the saved query by ID first, then by name. Revalidates the saved query before execution (schema changes may have invalidated it since save time). Returns the same response format as the execute endpoint.


Admin Stats API

Server and data statistics for monitoring. With -auth enabled these endpoints are admin-only.

Server Stats

GET /v1/admin/server-stats

Returns server runtime information (memory usage, goroutines, uptime, etc.).

Data Stats

GET /v1/admin/data-stats

Returns entity counts and storage statistics.

Expensive Data Stats

GET /v1/admin/data-stats/expensive

Returns statistics that require heavier queries (e.g., orphan counts, duplicate detection). Separated from the main stats endpoint to avoid blocking.

Similarity Maintenance

Rebuild image-similarity data. Both are admin-only under -auth.

POST /v1/admin/similarity/recompute
POST /v1/admin/similarity/retry-failed

recompute submits a background job that rebuilds the stored similarity pairs and answers {"jobId": "..."}, or 409 Conflict when one is already running. retry-failed resets failed hash rows so the backfill worker retries them, and answers {"reset": N}.

Runtime Settings

GET /v1/admin/settings and PUT|DELETE /v1/admin/settings/{key} read and change the settings that can be edited without a restart. See Runtime Settings.


Users, Accounts & Authentication API

Session and token authentication, admin user management, and self-service account endpoints. These are the whole programmatic surface of the authentication feature.

Log In

POST /v1/auth/login

Parameters

ParameterTypeDescription
usernamestringRequired. Account username
passwordstringRequired. Account password

Accepts a JSON body or a form body. On success, sets the session cookie and returns the user. Answers 401 for a bad credential or a disabled account, 429 when login rate-limiting is enabled and the limit is hit, and 503 with Retry-After when the session could not be minted because of contention.

Example

curl -X POST http://localhost:8181/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "hunter2hunter2"}'

Log Out

POST /v1/auth/logout

Revokes the current session and clears the cookie. Returns {"ok": true}.

Current Principal

GET /v1/auth/me

Response

{
"authEnabled": true,
"userId": 3,
"username": "alice",
"role": "editor",
"scopeGroupId": null,
"isAdmin": false,
"canWrite": true,
"superUser": false,
"csrfToken": "..."
}

csrfToken is empty for Bearer-authenticated requests, which are exempt from the CSRF check.

User Administration

Admin-only under -auth.

GET  /v1/users
POST /v1/users
GET /v1/user?id={id}
POST /v1/user
POST /v1/user/delete

Parameters

ParameterTypeDescription
idintegerUser ID (required on update and delete)
usernamestringAccount username
displayNamestringDisplay name
passwordstringPassword (at least 8 characters, at most 72 bytes)
rolestringadmin, editor, user or guest
scopeGroupIdintegerGroup subtree the account is confined to
disabledbooleanLock the account

GET /v1/users accepts offset and limit; a limit of 0 returns every account. Deleting, demoting or disabling the last enabled administrator is refused with 409 Conflict.

Example

curl -X POST http://localhost:8181/v1/users \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username": "bob", "password": "correcthorse", "role": "user"}'

Self-Service Account

These act on the caller's own record and are open to any authenticated principal, including guests.

EndpointDescription
POST /v1/account/passwordChange own password. Takes currentPassword and newPassword; revokes the caller's other sessions.
GET /v1/account/tokensList the caller's API tokens.
POST /v1/account/tokensMint an API token. Takes name and an optional expiresIn Go duration; the raw token is returned exactly once.
POST /v1/account/tokens/deleteRevoke one of the caller's tokens by id.
GET /v1/account/settingsRead the caller's UI settings.
PUT /v1/account/settings/{key}Upsert one setting.
DELETE /v1/account/settings/{key}Remove one setting.

Example

curl -X POST http://localhost:8181/v1/account/tokens \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "laptop", "expiresIn": "720h"}'

Timeline Endpoints

Each major entity type has a timeline endpoint that returns entities grouped by time period, suitable for timeline/calendar views.

EntityEndpoint
ResourcesGET /v1/resources/timeline
NotesGET /v1/notes/timeline
GroupsGET /v1/groups/timeline
TagsGET /v1/tags/timeline
CategoriesGET /v1/categories/timeline
QueriesGET /v1/queries/timeline

Each accepts the same query parameters as the corresponding list endpoint, plus three timeline-specific ones: granularity (yearly, monthly or weekly, default monthly), anchor (YYYY-MM-DD, default today) and columns (default 15; a value outside 1 to 60 is ignored rather than clamped). See Timeline Parameters.