Skip to main content

REST API

Expose clips and tags over an authenticated HTTP API for external tools, scripts, and integrations.

Overview

The REST API runs a separate HTTP server with key-based authentication. Every request requires a Bearer token. Keys have roles that control access, and can optionally be scoped to a single tag.

Base URL: http://127.0.0.1:{port}/api/v1

Content types:

  • All JSON responses use Content-Type: application/json
  • Errors return { "error": "message" }
  • CORS headers are set on all responses (Access-Control-Allow-Origin: *)

Starting the Server

  1. Open the menu drawer and click API (or the API button in settings)
  2. Set a port (default: 8484)
  3. Optionally enable Network toggle to bind to 0.0.0.0 instead of 127.0.0.1
  4. Click Start Server

The status line shows the server URL and request count while running.

API settings

Stopping the Server

Click Stop Server in the API modal. The server shuts down gracefully with a 5-second timeout.

API Keys

Every request must include a valid API key. Keys are managed in the API modal.

Creating a Key

  1. Click + New Key
  2. Enter a name (e.g. "CI pipeline")
  3. Select a role: Viewer, Editor, or Admin
  4. Optionally select a tag scope to restrict the key to a single tag
  5. Click Create Key

The plaintext key is shown once. Copy it immediately -- it cannot be retrieved later. Keys are stored as SHA-256 hashes.

Keys are prefixed with mp_ (e.g. mp_a1b2c3d4e5f6...).

Revoking a Key

Click Revoke on any active key card. The key stops working immediately. Revoked keys remain visible in the list but are grayed out.

Roles

RoleClipsTagsScope
viewerList, get metadata, download dataList tagsRead-only
editorAll viewer permissions + create, delete, archive, unarchive, rename, expire, manage clip tags, metadata, clipboardList tagsRead-write clips
adminAll editor permissionsCreate, update, delete tags, manage watch folders, plugins, serve, backup, hidden tags, dedup-allFull access

Tag Scope

A key can be scoped to a single tag. When scoped, the key has access to the full subtree rooted at that tag -- the scoped tag itself plus all of its descendants.

Clip Access

  • List clips returns clips tagged with the scoped tag or any of its subtags
  • Get / download / delete a clip requires the clip to have the scoped tag or one of its subtags
  • Create clip auto-applies the scoped tag to the new clip

Tag Management (Admin Role)

Scoped admin keys can manage tags within their subtree:

  • Create tags -- can create new subtags under the scoped tag (e.g., a key scoped to work can create work/client2)
  • Update tags -- can rename or recolor tags within the subtree
  • Delete tags -- can delete tags within the subtree
  • List tags returns the scoped tag and all of its descendants

Tags outside the subtree are not visible or modifiable.

Clip-Tag Association

  • Add/remove tag on clip is restricted to tags within the scoped subtree
  • The clip must also belong to the scoped subtree

Serve Management

  • Scoped admin keys can start and stop serving for their scoped tag
  • Attempting to serve a different tag returns 403 Forbidden
  • Serve management is restricted to the exact scoped tag, not its subtags
  • List servers only returns servers for the scoped tag

Deduplication Restrictions

  • List duplicates and deduplicate all are not available for tag-scoped keys and return 403 Forbidden
  • Merge duplicates enforces tag scope on the survivor clip

Unscoped keys (scope: "All tags") have access to all clips and tags within their role.

Authentication

Include the key as a Bearer token:

Authorization: Bearer mp_a1b2c3d4e5f67890abcdef1234567890

Missing or invalid tokens return 401 Unauthorized. Insufficient role returns 403 Forbidden.

Endpoint Reference

All paths below are relative to /api/v1.

Clips

MethodPathMin RoleDescription
GET/clipsviewerList clips (paginated, filterable)
GET/clips/{id}viewerGet clip metadata
GET/clips/{id}/dataviewerDownload raw clip content
POST/clipseditorUpload a new clip
DELETE/clips/{id}editorDelete a clip
PUT/clips/{id}/archiveeditorArchive a clip
DELETE/clips/{id}/archiveeditorUnarchive a clip
PATCH/clips/{id}editorRename a clip
PUT/clips/{id}/expirationeditorSet expiration timer
DELETE/clips/{id}/expirationeditorCancel expiration

List clips

GET /api/v1/clips

Query parameters:

ParameterTypeDefaultDescription
limitint50Results per page (max 200)
offsetint0Pagination offset
tagint--Filter by tag ID
content_typestring--Filter by exact content type
archivedbool--Filter by archive status (true / false)
searchstring--Search filename and text content

Response:

{
"clips": [
{
"id": 1,
"filename": "screenshot.png",
"content_type": "image/png",
"size": 204800,
"is_archived": false,
"created_at": "2025-01-15T10:30:00Z",
"tags": [{ "id": 3, "name": "work", "color": "#e74c3c", "count": 5 }]
}
],
"total": 42,
"limit": 50,
"offset": 0
}

Get clip metadata

GET /api/v1/clips/{id}

Returns a single clip object with id, filename, content_type, size, created_at, is_archived, and tags.

Download clip data

GET /api/v1/clips/{id}/data

Returns the raw file bytes with the clip's original content type, Content-Length, and Content-Disposition headers. The Content-Disposition header is set to attachment with the clip's filename when available.

Upload a clip

POST /api/v1/clips
Content-Type: multipart/form-data

Send a file part in the multipart body (100 MB max). Optional ?filename= query parameter overrides the uploaded filename.

Returns 201 Created with the clip object on success.

Duplicate uploads (matching content hash) return the existing clip instead of creating a new one. When a tag-scoped key uploads a duplicate, the scoped tag is applied to the existing clip.

Delete a clip

DELETE /api/v1/clips/{id}

Returns 204 No Content on success.

Archive / Unarchive

PUT    /api/v1/clips/{id}/archive     # Archive
DELETE /api/v1/clips/{id}/archive # Unarchive

Returns 204 No Content on success.

Rename a clip

PATCH /api/v1/clips/{id}
Content-Type: application/json
{ "filename": "new-name.png" }

Returns 204 No Content on success.

Set / Cancel expiration

PUT /api/v1/clips/{id}/expiration
Content-Type: application/json
{ "minutes": 60 }

The clip is automatically deleted after the specified number of minutes. The minutes value must be positive.

DELETE /api/v1/clips/{id}/expiration

Cancels a pending expiration. Returns 204 No Content.


Clip Metadata

MethodPathMin RoleDescription
GET/clips/{id}/metadataviewerGet all metadata key-value pairs
PUT/clips/{id}/metadataeditorReplace all metadata
PUT/clips/{id}/metadata/{key}editorSet a single metadata value
DELETE/clips/{id}/metadata/{key}editorDelete a single metadata key

Get all metadata

GET /api/v1/clips/{id}/metadata

Returns a JSON object of key-value pairs: {"source": "screenshot", "project": "docs"}.

Replace all metadata

PUT /api/v1/clips/{id}/metadata
Content-Type: application/json
{ "source": "screenshot", "project": "docs" }

Atomically replaces all existing metadata with the provided pairs. Returns 204 No Content.

Set a single key

PUT /api/v1/clips/{id}/metadata/{key}
Content-Type: application/json
{ "value": "screenshot" }

Returns 204 No Content.

Delete a single key

DELETE /api/v1/clips/{id}/metadata/{key}

Returns 204 No Content.


Clip-Tag Association

MethodPathMin RoleDescription
PUT/clips/{id}/tags/{tagId}editorAssign a tag to a clip
DELETE/clips/{id}/tags/{tagId}editorRemove a tag from a clip

Returns 204 No Content on success.

Tree Exclusivity

Assigning a tag automatically removes any existing tags from the same root tree. For example, adding work/client2 removes work/client1 if both share the work root.


Bulk Operations

All bulk endpoints accept a JSON body with an ids array. Minimum role is editor unless noted.

MethodPathMin RoleDescription
POST/clips/bulk/deleteeditorDelete multiple clips
POST/clips/bulk/archiveeditorArchive multiple clips
POST/clips/bulk/unarchiveeditorUnarchive multiple clips
POST/clips/bulk/expireeditorSet expiration on multiple clips
POST/clips/bulk/cancel-expireeditorCancel expiration on multiple clips
POST/clips/bulk/tageditorAdd a tag to multiple clips
POST/clips/bulk/untageditorRemove a tag from multiple clips
POST/clips/bulk/downloadviewerDownload multiple clips as a ZIP
POST/clips/bulk/copyeditorCopy clip files to system clipboard

Request bodies

Delete / Archive / Unarchive / Cancel-expire:

{ "ids": [1, 2, 3] }

Expire:

{ "ids": [1, 2, 3], "minutes": 60 }

Tag / Untag:

{ "ids": [1, 2, 3], "tag_id": 5 }

Download:

{ "ids": [1, 2, 3] }

Returns an application/zip response containing the clip files.

Copy:

{ "ids": [1, 2, 3] }

Copies the clip files to the system clipboard. Returns 204 No Content.

Response bodies

Most bulk operations return a JSON object with the count of affected items:

EndpointResponse
bulk/delete{ "deleted": 3 }
bulk/archive{ "archived": 3 }
bulk/unarchive{ "unarchived": 3 }
bulk/expire{ "updated": 3 }
bulk/cancel-expire{ "updated": 3 }
bulk/tag{ "tagged": 3 }
bulk/untag{ "untagged": 3 }
bulk/downloadBinary ZIP file
bulk/copy204 No Content

Tags

MethodPathMin RoleDescription
GET/tagsviewerList all tags
POST/tagsadminCreate a tag
PUT/tags/{id}adminUpdate tag name and color
DELETE/tags/{id}adminDelete a tag
GET/tags/{id}/childrenviewerList direct child tags
GET/tags/{id}/clipsviewerList clips with this tag
GET/tags/hiddenviewerGet hidden tag IDs
PUT/tags/hiddenadminSet hidden tag IDs

List tags

GET /api/v1/tags

Returns an array of tag objects, each with id, name, color, and count (number of clips using the tag). Tag-scoped keys only see their scoped tag and its descendants.

Create a tag

POST /api/v1/tags
Content-Type: application/json
{ "name": "work/client1" }

Use / separators to create hierarchical tags. Parent tags are created automatically if they don't exist. Returns 201 Created with the new tag object.

Reserved Names

Tag names cannot contain _api as a path segment. See Tag Serve for details.

Update a tag

PUT /api/v1/tags/{id}
Content-Type: application/json
{ "name": "work/client2", "color": "#e74c3c" }

Returns 204 No Content.

Delete a tag

DELETE /api/v1/tags/{id}

Returns 204 No Content. Deleting a parent tag does not delete its children.

List child tags

GET /api/v1/tags/{id}/children

Returns an array of direct child tags (one level deep).

List clips for a tag

GET /api/v1/tags/{id}/clips

Query parameters:

ParameterTypeDefaultDescription
limitint50Results per page (max 200)
offsetint0Pagination offset

Returns a paginated clip list in the same envelope format as List clips:

{ "clips": [...], "total": 12, "limit": 50, "offset": 0 }

Get / Set hidden tags

GET /api/v1/tags/hidden

Returns { "ids": [1, 2] }.

PUT /api/v1/tags/hidden
Content-Type: application/json
{ "ids": [1, 2] }

Replaces the full list of hidden tag IDs. Returns 204 No Content.


Deduplication

MethodPathMin RoleDescription
GET/dedupviewerList duplicate groups
POST/dedup/{clipId}/mergeeditorMerge duplicates (keeps specified clip)
POST/dedup/alladminDeduplicate all groups at once
Tag-Scoped Keys

Listing duplicates and deduplicating all are not available for tag-scoped keys. These endpoints return 403 Forbidden when called with a scoped key.

List duplicates

GET /api/v1/dedup

Returns duplicate groups. Each group contains clips that share the same content hash.

{
"groups": [
{
"content_hash": "abc123...",
"filename": "screenshot.png",
"content_type": "image/png",
"count": 3,
"oldest_id": 1
}
],
"total": 2
}

Merge duplicates

POST /api/v1/dedup/{clipId}/merge

Keeps the specified clip as the survivor. Tags from all duplicates are merged onto it, then the duplicates are deleted. Returns 204 No Content.

Deduplicate all

POST /api/v1/dedup/all

Iterates over all duplicate groups and merges each one, keeping the oldest clip in each group.

{ "removed": 5 }

Watch Folders

MethodPathMin RoleDescription
GET/watchviewerList watch folders
GET/watch/statusviewerGet watch system status
GET/watch/{id}viewerGet a specific folder
POST/watchadminAdd a watch folder
PUT/watch/{id}adminUpdate folder config
DELETE/watch/{id}adminRemove a folder
PUT/watch/{id}/pauseadminPause a folder
DELETE/watch/{id}/pauseadminResume a folder
POST/watch/{id}/processadminProcess existing files
PUT/watch/global-pauseadminPause all watchers
DELETE/watch/global-pauseadminResume all watchers

List watch folders

GET /api/v1/watch

Returns all configured watch folders:

{
"folders": [
{
"id": 1,
"path": "/Users/me/Screenshots",
"filter_mode": "presets",
"filter_presets": ["images"],
"filter_regex": "",
"process_existing": false,
"auto_archive": false,
"auto_tag_id": 5,
"is_paused": false,
"created_at": "2025-01-15T10:30:00Z",
"exists": true
}
],
"total": 1
}

Add a watch folder

POST /api/v1/watch
Content-Type: application/json
{
"path": "/Users/me/Screenshots",
"filter_mode": "presets",
"filter_presets": ["images"],
"filter_regex": "",
"auto_tag_id": 5,
"auto_archive": false,
"process_existing": false
}
FieldTypeDescription
pathstringAbsolute path to the folder (required)
filter_modestring"all", "presets", or "custom"
filter_presetsstring[]Preset names when filter_mode is "presets" (e.g. ["images", "videos", "documents"])
filter_regexstringRegex pattern when filter_mode is "custom"
auto_tag_idintTag ID to auto-apply to imports (null or 0 for none)
auto_archiveboolAuto-archive imported files
process_existingboolImport existing files in the folder immediately

Returns 201 Created with the folder object. When process_existing is true, the import runs in the background after the folder is created.

Update a watch folder

PUT /api/v1/watch/{id}
Content-Type: application/json

Supports partial updates -- only include the fields you want to change. Omitted fields keep their current values.

{ "filter_mode": "all", "auto_archive": true }

Returns 204 No Content.

Pause / Resume a folder

PUT    /api/v1/watch/{id}/pause    # Pause
DELETE /api/v1/watch/{id}/pause # Resume

Returns 204 No Content.

Global pause / resume

PUT    /api/v1/watch/global-pause    # Pause all
DELETE /api/v1/watch/global-pause # Resume all

Returns 204 No Content.

Process existing files

POST /api/v1/watch/{id}/process

Triggers a one-time scan of all existing files in the folder. Returns 204 No Content.


Plugins

MethodPathMin RoleDescription
GET/pluginsviewerList installed plugins
GET/plugins/actionsviewerList all plugin UI actions
POST/pluginsadminInstall a plugin
POST/plugins/check-updatesadminCheck for available updates
DELETE/plugins/{id}adminRemove a plugin
PUT/plugins/{id}/enableadminEnable a plugin
PUT/plugins/{id}/disableadminDisable a plugin
GET/plugins/{id}/storageadminGet all plugin storage
GET/plugins/{id}/storage/{key}adminGet a storage value
PUT/plugins/{id}/storage/{key}adminSet a storage value
POST/plugins/{id}/actions/{actionId}editorExecute a plugin action
POST/plugins/{id}/updateadminUpdate a plugin

List plugins

GET /api/v1/plugins

Returns all installed plugins:

{
"plugins": [
{
"id": 1,
"name": "fal-ai",
"version": "1.0.0",
"description": "FAL.AI image processing",
"author": "mahpastes",
"enabled": true,
"status": "loaded",
"events": ["clip:created"],
"settings": []
}
],
"total": 1
}

Install a plugin

POST /api/v1/plugins
Content-Type: application/json

Pass a URL or local file path in the source field:

{ "source": "https://example.com/plugin.lua" }
{ "source": "/Users/me/plugins/my-plugin.lua" }

Returns 201 Created with the plugin info object.

Remove a plugin

DELETE /api/v1/plugins/{id}

Returns 204 No Content.

Enable / Disable

PUT /api/v1/plugins/{id}/enable
PUT /api/v1/plugins/{id}/disable

Returns 204 No Content.

Plugin storage

GET /api/v1/plugins/{id}/storage

Returns all key-value pairs for the plugin.

GET /api/v1/plugins/{id}/storage/{key}

Returns { "key": "...", "value": "..." }.

PUT /api/v1/plugins/{id}/storage/{key}
Content-Type: application/json
{ "value": "my-api-key-here" }

Returns 204 No Content.

Execute a plugin action

POST /api/v1/plugins/{id}/actions/{actionId}
Content-Type: application/json
{ "clip_ids": [42], "options": { "style": "watercolor" } }

Pass clip_ids to identify the target clip(s) and options for any form fields defined in the action manifest. Both fields are optional.

Check for updates

POST /api/v1/plugins/check-updates

Returns update availability info for all installed plugins:

{ "updates": [...] }

Update a plugin

POST /api/v1/plugins/{id}/update

Downloads and installs the latest version from the plugin's source URL.


Serve (Tag Hosting)

MethodPathMin RoleDescription
GET/serveviewerList running tag servers
POST/serveadminStart serving a tag
DELETE/serve/{tagId}adminStop serving a tag

List servers

GET /api/v1/serve

Returns all running tag servers. Tag-scoped keys only see servers for their scoped tag.

{
"servers": [
{
"tag_id": 5,
"tag_name": "my-site",
"port": 44557,
"bind_all": false,
"url": "http://127.0.0.1:44557",
"running": true,
"request_count": 42,
"api_access": "readwrite"
}
]
}

Start serving

POST /api/v1/serve
Content-Type: application/json
{
"tag_id": 5,
"port": 0,
"bind_all": false,
"api_access": "none"
}
FieldTypeDescription
tag_idintID of the tag to serve (required)
portintPort number, or 0 to auto-assign
bind_allboolBind to 0.0.0.0 instead of 127.0.0.1
api_accessstring"none" (default), "read", or "readwrite" -- controls the JSON API
  • Returns 201 Created with server info on success
  • Returns 409 Conflict if the tag is already being served or the port is unavailable
Scoped Keys

Tag-scoped admin keys can start and stop servers for their own scoped tag. They receive 403 Forbidden only when attempting to serve a different tag.

Stop serving

DELETE /api/v1/serve/{tagId}

Returns 204 No Content. Returns 404 if no server is running for that tag.


Backup

MethodPathMin RoleDescription
GET/backupadminDownload a backup ZIP
POST/backup/restoreadminRestore from a backup ZIP

Download backup

GET /api/v1/backup

Returns an application/zip file containing the full database and all clip data.

Restore from backup

POST /api/v1/backup/restore
Content-Type: multipart/form-data

Upload a backup ZIP file as a multipart form. This replaces the current database and clip data.

Returns { "status": "restored" } on success.


Clipboard

MethodPathMin RoleDescription
POST/clipboard/copyeditorCopy clip content to system clipboard
POST/clipboard/copy-fileeditorCopy clip as a file reference

Copy content

POST /api/v1/clipboard/copy
Content-Type: application/json
{ "clip_id": 1 }

Copies the clip's raw content (text or image data) to the system clipboard. Returns 204 No Content.

Copy as file

POST /api/v1/clipboard/copy-file
Content-Type: application/json
{ "clip_id": 1 }

Places a file reference on the system clipboard (macOS: NSPasteboard file URL, Windows: PowerShell SetFileDropList). You can then paste the clip as a file into Finder, Explorer, or other apps. Returns 204 No Content.

Error Responses

All errors return JSON with an error field:

{ "error": "clip not found" }

Common status codes:

StatusMeaning
400Bad request (invalid JSON, missing fields)
401Missing or invalid API key
403Insufficient role or tag scope violation
404Resource not found
409Conflict (duplicate, port in use)
413Request body too large
500Internal server error
  • Tag Serve -- unauthenticated file serving for quick sharing, plus JSON API and file upload for served HTML apps
  • Tags -- create and manage tags used for scoping
  • CLI (mp) -- command-line interface that wraps this REST API