Language versions: 简体中文
Related documents: Overview · Quick Start · API Reference
The Local API is not only for scripts. It also works well for locally hosted plugin front ends such as:
- document editor add-ins
- Obsidian companion panels
- local web dashboards
- automation configuration pages
- any plugin UI that needs local access to Lattice paper data
In addition to /api/v1/..., Lattice also exposes:
/plugins/{name}/...
That means you can deploy a plugin as a static website into Lattice's plugin directory, and Lattice will serve it over the same local HTTP service.
Examples:
http://127.0.0.1:29467/plugins/my-plugin/index.html
http://127.0.0.1:29467/plugins/my-plugin/app.js
http://127.0.0.1:29467/plugins/my-plugin/assets/icon.png
The advantages are straightforward:
- your plugin page and
/api/v1stay on the same origin - you do not need to run a separate local web server
- you avoid extra cross-origin complexity
- packaging and distribution stay simple
- it is easy to turn into a "user installs it and it works" desktop plugin
For the sandboxed Lattice app, plugins are placed under the app container's Application Support directory. A typical path looks like:
~/Library/Containers/com.aurelian.Lattice/Data/Library/Application Support/Plugins/<plugin-name>/
After deployment, the corresponding public URL becomes:
http://127.0.0.1:<port>/plugins/<plugin-name>/...
Lattice does not require a specific framework. You only need static assets.
A minimal plugin can look like this:
my-plugin/
index.html
app.js
styles.css
assets/
icon.png
If your source repository also contains build scripts, templates, or source code, the installation step should copy only the final static output into the plugin directory.
The Local API already serves the asset types most plugins need, including:
.html.js.css.json.png.svg.xml.csl
That means a plugin with UI, styles, icons, and CSL templates can be hosted directly.
If the plugin page itself is loaded from /plugins/{name}/..., the recommended pattern is to always call the API with relative paths:
async function requestJson(path, init = {}) {
const headers = {
Accept: "application/json",
...(init.headers ?? {})
};
const response = await fetch(`/api/v1${path}`, {
...init,
method: init.method ?? "GET",
headers
});
if (!response.ok) {
const payload = await response.json().catch(() => null);
throw new Error(payload?.error || `${response.status} ${response.statusText}`);
}
return await response.json();
}
export async function getBridgeStatus() {
return requestJson("/status");
}
export async function searchLattice(query, limit = 10) {
return requestJson(`/search?q=${encodeURIComponent(query ?? "")}&limit=${encodeURIComponent(String(limit))}`);
}
export async function fetchPaperSnapshot(id) {
return requestJson(`/papers/${encodeURIComponent(id)}`);
}
export async function getCollections() {
return requestJson("/collections");
}
export async function getTags() {
return requestJson("/tags");
}
export async function lookupMetadata(params) {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(params ?? {})) {
if (value) {
query.set(key, value);
}
}
return requestJson(`/metadata?${query.toString()}`);
}
export async function createPaper(body) {
return requestJson("/papers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
}
export async function uploadPaperPdf(id, pdfBytes, { force = false } = {}) {
const suffix = force ? "?force=true" : "";
const response = await fetch(`/api/v1/papers/${encodeURIComponent(id)}/pdf${suffix}`, {
method: "PUT",
headers: { "Content-Type": "application/pdf" },
body: pdfBytes
});
if (!response.ok) {
const payload = await response.json().catch(() => null);
throw new Error(payload?.error || payload?.reason || `${response.status} ${response.statusText}`);
}
return await response.json();
}This is also the recommended default integration pattern.
Do not assume write routes are always available.
Recommended pattern:
- call
/api/v1/status - inspect
capabilities - only show create UI or send create requests if
create-paperis present - only show raw PDF upload UI if
pdf-uploadis present - if you depend on duplicate strategy support and rich success payloads, check for
create-paper-v2
Example:
export async function getCapabilities() {
const status = await getBridgeStatus();
return new Set(status.capabilities ?? []);
}
export async function canCreatePapers() {
return (await getCapabilities()).has("create-paper");
}
export async function canUploadPdfBytes() {
return (await getCapabilities()).has("pdf-upload");
}For write-oriented plugins, the public Local API already exposes the primitives you usually need:
- call
/api/v1/collectionsto populate collection pickers - call
/api/v1/tagsto populate tag pickers or autosuggest - call
/api/v1/metadatawithdoi,arxivId,isbn, ortitleto prefill a create or replace form before writing
Start with curl or a browser check:
curl http://127.0.0.1:29467/api/v1/statusUse any stack you want:
- plain HTML/CSS/JS
- built static output from React / Vue / Svelte
- Office Add-in front ends
- any other setup that produces static files
Deploy the final output to:
.../Application Support/Plugins/<plugin-name>/
http://127.0.0.1:29467/plugins/<plugin-name>/index.html
Examples:
- some hosts require a manifest or registration file
- some desktop tools may require a custom protocol or WebView entrypoint
A robust pattern looks like this:
- the UI page is hosted by Lattice under
/plugins/<name>/... - the page calls
/api/v1/...using relative paths - the host application only needs to open that page
- the plugin keeps its own cache for paper snapshots and rendering settings
This keeps responsibilities simple:
- Lattice provides local data and static asset hosting
- the plugin owns UI, state, rendering, and host integration
The current read endpoints are citation-oriented and intentionally smaller than the full internal Lattice paper model.
Read payloads currently do not expose:
abstractcollectionstagspdfPathpdfURLlatticeURL
Important implications:
- if your plugin only needs to open a paper in Lattice, synthesize
lattice://paper/{id}from the paperid - if your plugin needs the actual filesystem PDF path, the current Local API does not expose it
pdfPathis currently an input field accepted byPOST /api/v1/papers, not a field returned byGET /api/v1/papers/{id}- raw PDF byte upload is a separate
PUT /api/v1/papers/{id}/pdfflow gated bypdf-upload; it is not part of the paper-detail payload
This is not the intended default model. The Local API is designed around local-origin, local-integration usage.
It is optional. The user must explicitly turn Read-Only Mode off, and clients should gate write behavior on /status.capabilities.
They are not. The Local API is currently optimized for citation workflows, lightweight paper selection, and controlled paper creation.
- Start with
/api/v1/statusto distinguish availability problems from data problems - If search results look wrong, inspect
/api/v1/searchdirectly - If a plugin page does not open, first verify that the plugin assets were actually copied into the plugin directory
- If static files load but API requests fail, verify that the configured port in Lattice still matches the URL being used
- If
POST /api/v1/papersreturns403, ask the user to turnRead-Only Modeoff - If paper creation succeeds but
pdfAttachedisfalse, inspect thewarningsarray for Trusted Folder or PDF validation failures - If
PUT /api/v1/papers/{id}/pdfreturns409, decide whether your UI should retry with?force=true - If
PUT /api/v1/papers/{id}/pdfreturns503, ask the user to configure a usable PDF upload destination first - If the host application caches an entry URL, changing the Local API port usually requires re-registration or reinstallation
- For a first-time integration flow, start with Quick Start
- For exact endpoint fields and error handling details, use API Reference