Python API
Quick Start
from plaid_client import PlaidClient
client = PlaidClient.login("http://localhost:8080", "user@example.com", "password")
# Create a project
project = client.projects.create("My Project")
# Create a document
doc = client.documents.create(project["id"], "Document 1")
# List a project's documents
docs = client.projects.list_documents(project["id"])
# Batch multiple operations atomically
with client.batched() as b:
b.tokens.create(token_layer_id, text_id, 0, 5)
b.tokens.create(token_layer_id, text_id, 6, 11)
results = b.results
Client
check_constraints(layer_id, constraints)The violations the given constraints would meet in this layer's stored data, as {'violations', 'violation_count'}. Writes nothing.
| Parameter | Type | Description |
|---|---|---|
layer_id |
Any |
The layer ID |
constraints |
Any |
The list to check |
id() @propertyset_message([message])| Parameter | Type | Description |
|---|---|---|
message optional |
Any |
server_now()The server's time now (UTC), as its last response's Date header put it (to the second), else this machine's. Judge a time the server stamped, such as an audit entry's ts, against this rather than the machine's own clock, which can be minutes off.
query(body)Run a query over every project you can read.
body is the query AST. Its keys follow the usual client convention (snake_case, e.g. scope['project_ids']) and are converted to the wire format automatically; clause heads and variables are plain strings you write literally (e.g. 'span', '?s1', 'vocab-link').
Example:
client.query({
'find': ['?s1', '?s2'],
'where': [
['span', '?s1', {'layer': pos_layer_id, 'value': 'NOUN'}],
['span', '?s2', {'layer': pos_layer_id, 'value': 'VERB'}],
['covers', '?s1', '?t1'], ['covers', '?s2', '?t2'],
['precedes', '?t1', '?t2'],
],
'return': 'entities', # 'ids' (default) | 'entities' | 'count'
'limit': 100,
})A layer is referenced by its id (its UUID) only — not by name or path. To match a layer by name, bind it with a *-layer clause (e.g. ['span-layer', '?sl', {'name': 'pos'}]) and use the variable.
Optional keys: scope (restrict to projects by id, {'project_ids': [...]}), order_by (sort rows), and bindings (substitute ?name placeholders with literals). return may also be an aggregate spec {group, aggregates}. See the query language reference.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The query AST ({find, where, scope?, limit?, order_by?, return?, bindings?}). |
enter_strict_mode(document_id)Enter strict mode for a specific document.
Enables strict mode, requiring document version headers on writes.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The ID of the document to track versions for |
exit_strict_mode()Exit strict mode and stop tracking document versions for writes.
key_seed()A seed for the Idempotency-Keys of a logical operation that may be run again from the top (see begin_operation's keys). Keep it with the work it belongs to and pass it to every run.
end_operation([message])End the current logical operation. With no argument this is purely local (no request). Pass a refined message to relabel the group now that the outcome is known (end_operation('Merged 3 morphemes')) — that sends one PATCH, skipped if the operation never wrote anything. A refine from a nested (flattened) end_operation is ignored; the outer label wins.
| Parameter | Type | Description |
|---|---|---|
message optional |
Any |
Optional. A refined label for the finished operation. |
batch()Open a batch: a view of this client with the same resources, on which every write of project data queues instead of going out. submit() sends the queued operations as ONE atomic request (larger than the server's cap, as consecutive requests with the results concatenated in queue order, each atomic on its own, so a failure in a later one leaves the earlier ones committed, and the error it raises carries committed, the count saved, and committed_results, their results) and returns one result per operation; abort() drops them. A call made on the client itself is never touched by an open batch, and a read or an out-of-band signal made on the batch goes over the wire now (see the note at the top of http.py):
b = client.batch() b.tokens.bulk_create(sentence_ops) b.tokens.bulk_create(word_ops) sentence_results, word_results = b.submit()
Server-side a batch runs sequentially in one transaction: a child op sees parents created earlier in the same batch, and any op's failure rolls the whole batch back. A batch is not nestable. Prefer :meth:batched, which submits or aborts for you.
claim(doc_id, [pin])| Parameter | Type | Description |
|---|---|---|
doc_id |
Any |
|
pin optional |
Any |
attempt([data], [request_headers])| Parameter | Type | Description |
|---|---|---|
data optional |
Any |
|
request_headers optional |
Any |
batched() @contextmanagerRun the block with a batch, then submit all queued ops as :meth:PlaidBatch.submit does (ONE atomic request up to MAX_BATCH_OPS operations, consecutive requests past it), or abort the batch if the block raises. The block makes its writes on the yielded batch; the results land on its .results (a context manager can't return a value):
with client.batched() as b:
b.tokens.bulk_create(sentence_ops)
b.tokens.bulk_create(word_ops)
sentence_results, word_results = b.resultsAn empty block submits nothing and leaves .results == []. A write made on client inside the block is not part of the batch: it goes over the wire at once, as it would anywhere else. See :meth:batch.
close()Close the underlying HTTP session.
invite_url(app_url, code) @staticmethodBuild the link to hand someone for an invite code.
The server never sees an app URL, so the app that minted the invite is the one that names it. Both SPAs use hash routing, so the code rides in the fragment — which also keeps it out of server access logs.
| Parameter | Type | Description |
|---|---|---|
app_url |
Any |
Where the SPA lives, e.g. "https://plaid.example.org/igt/" |
code |
Any |
The code returned by invites.create() |
health() @classmethodLiveness, version and database size, with NO authentication and no client instance: for a launcher or status page checking whether a server is up before anyone logs in. client.server.health() is the same read from a client.
info() @classmethodThe version and the limits this server enforces, with NO authentication and no client instance. client.server.info() is the same read from a client, cached.
batch()batched()Send the queued operations and return one result per operation, in order (also kept on .results). Up to MAX_BATCH_OPS operations go as one atomic request. A larger batch goes as consecutive requests, each atomic on its own, so a failure in a later one leaves the earlier ones committed. The error then carries committed, how many operations were saved (0 when none were), and committed_results, their results in queue order.
submit()Send the queued operations and return one result per operation, in order (also kept on .results). Up to MAX_BATCH_OPS operations go as one atomic request. A larger batch goes as consecutive requests, each atomic on its own, so a failure in a later one leaves the earlier ones committed. The error then carries committed, how many operations were saved (0 when none were), and committed_results, their results in queue order.
abort()Drop the queued operations without sending them.
ref()A stand-in for the id a queued operation will create, to put in a later operation's body on this batch, so a create and the write that uses it go in one transaction: op n's id, or with index the k-th of the ids a bulk create answers. op_index counts from 0, or from the end when negative (-1, the default, is the op queued last), and is fixed when ref is called. It goes only in the body of a later write on this batch, at any depth. Anywhere else (a path, another batch, a call made on the client) the client refuses it:
with client.batched() as b:
b.vocab_items.create(vocab_id, 'dog')
b.vocab_links.create(b.ref(), [token_id])Admin
admin.server()Everything about the server in one read.
Version and JVM uptime, database size and per-table row counts, media directory usage, backup configuration and the backups on disk, and the settings an operator gets asked to confirm. Carries no secrets. Runs a count per table and walks the media directory, so call it when someone asks, not on a timer.
admin.backup()Take a database backup right now, outside the nightly schedule.
Returns the backup block, with ok reporting whether the snapshot succeeded. Uses VACUUM INTO, which only reads, so it is safe while people are working.
out_of_band, as every admin action on the server itself is: none of them writes project data (see the note at the top of http.py).
admin.locks()Documents currently held by an editing lock, with who holds each and when it expires on its own.
admin.release_lock(document_id)Drop the lock on a document whoever holds it. Idempotent.
For a client that went away without releasing one, which otherwise leaves the document unwritable until the lock expires.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document to unlock |
admin.rate_limits()Live login and invite rate-limit buckets: the address, the account where there is one, failures inside the window, the limit, and whether it is currently blocking.
admin.log_file(lines)The tail of the configured log file, as text lines.
The only place third-party library messages (connection pool, SQLite driver, HTTP server) and anything from before the last restart can be read. Returns an error string instead of lines when no log file is configured or it does not exist yet. The server also logs to stdout, where a file is not required.
| Parameter | Type | Description |
|---|---|---|
lines |
Any |
How many lines (default 200, max 2000) |
Api Tokens
api_tokens.list(user_id)List a user's named API tokens.
Never includes the signed token string itself — that is only returned once, by create(). Transparently follows server-side pagination cursors and returns the full flat list.
| Parameter | Type | Description |
|---|---|---|
user_id |
Any |
The user ID who owns the tokens |
api_tokens.iter_pages(user_id, page_size)Iterate over pages of a user's API tokens, yielding each page's entries.
| Parameter | Type | Description |
|---|---|---|
user_id |
Any |
The user ID who owns the tokens |
page_size |
Any |
Page size (1..1000) |
api_tokens.create(user_id, name)Mint a named API token for a user.
The returned token is the signed credential and is shown ONLY here — store it immediately. API tokens do not expire and survive password changes / logout; revoke to kill. Returns a dict with id, name and token.
| Parameter | Type | Description |
|---|---|---|
user_id |
Any |
The user ID who will own the token |
name |
Any |
A human label, e.g. "Stanza parser" |
api_tokens.revoke(user_id, token_id)Revoke a named API token (soft-revoke; idempotent).
| Parameter | Type | Description |
|---|---|---|
user_id |
Any |
The user ID who owns the token |
token_id |
Any |
The token ID to revoke |
Auth
auth.logout_everywhere()End every sign-in of this user on every device: each browser tab, script and service holding one of the user's sign-in tokens is refused from now on, this client included. Named API tokens are not affected (revoke one with api_tokens.revoke). To sign out of one tab only, discard its token instead. A signal rather than project data: made on a batch it still goes out at once, and it never joins an operation.
Documents
documents.check_lock(document_id)Get information about a document lock.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
documents.acquire_lock(document_id, [new_lock_id])Acquire a document lock as a new holder.
The answer's lock_id names the holder: :meth:renew_lock and :meth:release_lock take it. While the lock is held, a second acquire is refused with HTTP 423, whoever makes it, this user included.
new_lock_id names the holder from the client's side (a fresh UUID). An acquire whose answer never arrived can then be sent again, which answers 200 while that holder has the lock, or released. Without it the server mints the id, and a lost answer leaves a lock nobody can release until it expires.
out_of_band: the lock is a signal, not project data (see the note at the top of http.py). Queued on a batch it would be taken only at submit, after every write it was meant to guard, and until then answer success to a caller that does not hold it and cannot see the 423 saying somebody else does.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
new_lock_id optional |
Any |
Optional holder id this client minted |
documents.renew_lock(document_id, lock_id)Renew the lock lock_id holds while it is live. HTTP 423 if another holder has it, and also once it has expired or been dropped, even when nobody holds the document now: a renewal never takes a free document.
out_of_band, for the same reason as :meth:acquire_lock.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
lock_id |
Any |
The lock_id :meth:acquire_lock answered with |
documents.release_lock(document_id, lock_id)Release the lock lock_id holds. Idempotent: a lock that holder no longer has is left alone.
out_of_band, for the same reason as :meth:acquire_lock: queued, the lock would be held until the batch submits, and not released at all if it aborts.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
lock_id |
Any |
The lock_id :meth:acquire_lock answered with |
documents.locked(document_id, keep_alive) @contextmanagerHold this document's server-enforced lock for a with block, releasing it on exit (including on error).
Wrap any multi-step, server-side mutation of a document that must not interleave with a human editor or another service — e.g. a parser or tokenizer that deletes and recreates a document's tokens/spans/relations (a single atomic call doesn't need this). While the lock is held, writes to the document by ANOTHER user are rejected by the server with HTTP 423; the holder's own writes pass and refresh the lock. If anyone already holds it, another block of this same user included, this raises :class:PlaidAPIError (status == 423, the same Locked code the server returns when rejecting another user's write) with a readable message and the block does NOT run:
with client.documents.locked(doc_id):
...delete + recreate tokens...The block is renewed for as long as it runs, so work that computes for minutes before it writes holds the lock the whole time rather than only for its first minute. If a renewal fails the lock is gone: every later write from this client raises :class:DocumentLockLost, and a block that got to the end anyway ends with that error rather than reporting success. The block may also read lock.lost to give up sooner:
with client.documents.locked(doc_id) as lock:
for sentence in sentences:
lock.raise_if_lost()
...| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document to hold. |
keep_alive |
Any |
Renew the lock on a timer while the block runs (default). Pass False for a block that writes as it goes and wants no background thread. |
documents.get_media(document_id)Get the media file for a document.
Media is not versioned, so there is no as-of form: the route refuses the parameter. Prefer the document's own media URL, which carries the file's version for caching.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
documents.delete_media(document_id)Delete media file for a document.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
documents.delete(document_id)Delete a document and all data contained.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
documents.update(document_id, name)Update a document's name.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
name |
Any |
The name |
documents.set_metadata(document_id, body)Replace all metadata for a document.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
body |
Any |
The request body |
documents.delete_metadata(document_id)Remove all metadata from a document.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
documents.patch_metadata(document_id, body)Edit metadata for a document with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
document_id |
Any |
The document ID |
body |
Any |
The metadata ops |
Events
events.create(project_id, [events])Record events in a project. Requires write access.
The server stamps the user and its own time. All or nothing: one bad event refuses the request with a 400 naming it.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The project the events happened in |
events optional |
Any |
At most 500 dicts, each with type and optional document_id, target_id, data (an object) and client_ts (an ISO-8601 instant) |
Guidelines
guidelines.get()Read one guideline, Markdown body included.
guidelines.delete()Delete a guideline.
guidelines.list(project_id, include_bodies)List a project's guidelines, by title.
Transparently follows server-side pagination cursors and returns the full flat list.
Without include_bodies each entry carries body_chars, the length of its body, so a caller can budget before fetching any. Pinned guidelines are NOT sorted first: pinned is on every entry and grouping is the caller's.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The project to read |
include_bodies |
Any |
Return each body instead of its length |
Invites
invites.list(project_id, all)List invites you minted, oldest first.
With project_id, lists that project's invites instead (including ones minted by co-maintainers), which requires maintainer or admin on that project. With all, lists every invite on the server, which requires admin. Never includes invite codes — a code is returned once, by create(), and is not recoverable afterward. Transparently follows server-side pagination cursors and returns the full flat list.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
List this project's invites rather than your own |
all |
Any |
List every invite on the server (admin only) |
invites.revoke(invite_id)Revoke an invite, killing the link immediately.
Idempotent. Allowed for the invite's creator, an admin, or a maintainer of the project the invite grants access to.
| Parameter | Type | Description |
|---|---|---|
invite_id |
Any |
The invite ID |
Messages
messages.listen(project_id, on_event, path)Open a Server-Sent Events stream for a project.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The UUID of the project to listen to |
on_event |
Any |
Callback function that receives (event_type, data). If it returns true, listening will stop. |
path |
Any |
Stream path under the base URL. Defaults to the project /listen bus (audit-log + broadcast messages); service request channels pass their own path. |
messages.send_message(project_id, data)Send a message to project listeners.
data may be any JSON value and is sent VERBATIM (raw_body): a message payload is opaque application data, like metadata and config, so its keys must not be re-cased on the way out. Without this a key such as case-marker would reach listeners as case_marker in Python and caseMarker in JavaScript. :meth:listen restores it verbatim on the way in.
A message is not saved and writes nothing to History, so it never joins an open operation (:meth:PlaidClient.operation). Made on a batch it still queues, so a message queued after the writes goes out after them.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The UUID of the project to send to |
data |
Any |
The message data to send |
messages.discover_services(project_id)Discover the services seen on a project.
Reads the server-side service registry synchronously. Returns every service ever registered on the project: currently connected ones carry online: True; previously-seen offline ones carry online: False plus a last_seen_at stamp. Goes over the wire even while a batch is open on the client.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The UUID of the project to query |
messages.discard_service(project_id, service_id)Forget a previously-seen (offline) service.
Removes the service's row from the project's persistent registry. Maintainer-only; 409 if the service is currently connected.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The UUID of the project |
service_id |
Any |
The ID of the service to forget |
messages.cancel_service_request(project_id, request_id)Ask the service to stop a request made earlier. The request still ends with whatever the service then reports, on the stream of whoever is awaiting it. 404 if unknown or expired, 409 once finished.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The UUID of the project |
request_id |
Any |
The request id |
Operation Groups
operation_groups.get(id)Get a logical-operation group (its label + creator).
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The group id |
operation_groups.update(id, message)Relabel a logical-operation group after the fact. Owner or admin only.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The group id |
message |
Any |
The new label |
Projects
projects.create(name, [id])Create a new project.
Also registers the current user as a maintainer.
| Parameter | Type | Description |
|---|---|---|
name |
Any |
The name |
id optional |
Any |
Optional. The id to create it under, a UUIDv7 this client minted (plaid_client.uuid7()), so a create sent again after its answer was lost lands once (409 with id_taken when the id was used before). |
projects.list()List all projects accessible to the current user.
Transparently follows server-side pagination cursors and returns the full flat list.
projects.list_page(limit, cursor)List one page of projects.
| Parameter | Type | Description |
|---|---|---|
limit |
Any |
Page size (1..1000) |
cursor |
Any |
Opaque cursor from a previous page's next_cursor |
projects.iter_pages(page_size)Iterate over pages of projects, yielding each page's entries list.
| Parameter | Type | Description |
|---|---|---|
page_size |
Any |
Page size (1..1000) |
projects.list_documents(id)List all documents (IDs and names) in a project.
Transparently follows server-side pagination cursors and returns the full flat list. Replaces the removed include_documents param on get.
Note: this endpoint does not support temporal (as-of) queries; the server rejects ?as-of= on the documents-list route with a 400.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The project ID |
projects.iter_documents(id, page_size)Iterate over pages of a project's documents, yielding each page's entries.
Note: this endpoint does not support temporal (as-of) queries; the server rejects ?as-of= on the documents-list route with a 400.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The project ID |
page_size |
Any |
Page size (1..1000) |
projects.get(id)Get a project by ID.
To fetch the project's document IDs and names, use list_documents (the former include_documents param has been removed server-side).
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
projects.delete(id, audit_message, timeout)Delete a project and everything in it. This is irrecoverable. The project is gone when this returns, and what it holds is removed on the server afterwards.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
audit_message |
Any |
Custom audit-log message. |
timeout |
Any |
Per-request timeout in seconds, the client's own by default. None disables it. |
projects.update(id, name)Update a project's name.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
name |
Any |
The name |
projects.add_writer(id, user_id)Set a user's access level to read and write for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.remove_writer(id, user_id)Remove a user's writer privileges for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.add_reader(id, user_id)Set a user's access level to read-only for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.remove_reader(id, user_id)Remove a user's reader privileges for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.add_maintainer(id, user_id)Assign a user as a maintainer for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.remove_maintainer(id, user_id)Remove a user's maintainer privileges for this project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
projects.my_last_edits(project_id)When you last wrote to each document in a project, as a {document_id: timestamp} dict.
Documents you have never written to are absent. One request covers a whole document list.
The response is NOT key-transformed: its keys are document ids, and recasing would mangle the hyphens in a UUID.
| Parameter | Type | Description |
|---|---|---|
project_id |
Any |
The project ID |
projects.link_vocab(id, vocab_id)Link a vocabulary to a project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_id |
Any |
The vocab layer ID |
projects.unlink_vocab(id, vocab_id)Unlink a vocabulary from a project.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_id |
Any |
The vocab layer ID |
Relation Layers
relation_layers.get(relation_layer_id)Get a relation layer by ID.
| Parameter | Type | Description |
|---|---|---|
relation_layer_id |
Any |
The relation layer ID |
relation_layers.delete(relation_layer_id)Delete a relation layer.
| Parameter | Type | Description |
|---|---|---|
relation_layer_id |
Any |
The relation layer ID |
relation_layers.update(relation_layer_id, name)Update a relation layer's name.
| Parameter | Type | Description |
|---|---|---|
relation_layer_id |
Any |
The relation layer ID |
name |
Any |
The name |
relation_layers.shift(relation_layer_id, direction)Shift a relation layer's display order.
| Parameter | Type | Description |
|---|---|---|
relation_layer_id |
Any |
The relation layer ID |
direction |
Any |
The direction ("up" or "down") |
Relations
relations.set_metadata(relation_id, body)Replace all metadata for a relation.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
body |
Any |
The request body |
relations.delete_metadata(relation_id)Remove all metadata from a relation.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
relations.patch_metadata(relation_id, body)Edit metadata for a relation with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
body |
Any |
The metadata ops |
relations.set_target(relation_id, span_id)Update the target span of a relation.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
span_id |
Any |
The span ID |
relations.set_source(relation_id, span_id)Update the source span of a relation.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
span_id |
Any |
The span ID |
relations.get(relation_id)Get a relation by ID.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
relations.delete(relation_id)Delete a relation.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
relations.update(relation_id, value)Update a relation's value.
| Parameter | Type | Description |
|---|---|---|
relation_id |
Any |
The relation ID |
value |
Any |
The value |
relations.bulk_create(body)Create multiple relations in a single operation.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
relations.bulk_delete(body)Delete multiple relations in a single operation. Provide a list of IDs.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
relations.bulk_update(body)Update many relations in a single operation: set values and/or patch metadata.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
A list of {"id": ..., "value": ..., "metadata": [...]} objects. value is set only when the key is present (None sends JSON null); metadata is a list of metadata ops, as for patch_metadata. The relations may lie in several documents of one project. Every document touched has its version bumped, and every new version comes back in X-Document-Versions (past fifty documents, only their number, in X-Document-Versions-Omitted). A document-version precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update. |
Server
server.info()This server's version and the limits it enforces. Sizes are in bytes. Unauthenticated.
server.limits()Just the limits, which is what a caller almost always wants.
server.health()Liveness, version and database size.
Unauthenticated, and served outside the REST router at /health, so it answers even when the API is refusing requests. Never cached — the point is that it is current.
Span Layers
span_layers.get(span_layer_id)Get a span layer by ID.
| Parameter | Type | Description |
|---|---|---|
span_layer_id |
Any |
The span layer ID |
span_layers.delete(span_layer_id)Delete a span layer.
| Parameter | Type | Description |
|---|---|---|
span_layer_id |
Any |
The span layer ID |
span_layers.update(span_layer_id, name)Update a span layer's name.
| Parameter | Type | Description |
|---|---|---|
span_layer_id |
Any |
The span layer ID |
name |
Any |
The name |
span_layers.shift(span_layer_id, direction)Shift a span layer's display order.
| Parameter | Type | Description |
|---|---|---|
span_layer_id |
Any |
The span layer ID |
direction |
Any |
The direction ("up" or "down") |
Spans
spans.set_metadata(span_id, body)Replace all metadata for a span.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
body |
Any |
The request body |
spans.delete_metadata(span_id)Remove all metadata from a span.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
spans.patch_metadata(span_id, body)Edit metadata for a span with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
body |
Any |
The metadata ops |
spans.set_tokens(span_id, tokens)Replace the tokens associated with a span.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
tokens |
Any |
The tokens |
spans.get(span_id)Get a span by ID.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
spans.delete(span_id)Delete a span.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
spans.update(span_id, value)Update a span's value.
| Parameter | Type | Description |
|---|---|---|
span_id |
Any |
The span ID |
value |
Any |
The value |
spans.bulk_create(body)Create multiple spans in a single operation.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
spans.bulk_delete(body)Delete multiple spans in a single operation. Provide a list of IDs.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
spans.bulk_update(body)Update many spans in a single operation: set values and/or patch metadata.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
A list of {"id": ..., "value": ..., "metadata": [...]} objects. value is set only when the key is present (None sends JSON null); metadata is a list of metadata ops, as for patch_metadata. The spans may lie in several documents of one project. Every document touched has its version bumped, and every new version comes back in X-Document-Versions (past fifty documents, only their number, in X-Document-Versions-Omitted). A document-version precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update. |
Text Layers
text_layers.get(text_layer_id)Get a text layer by ID.
| Parameter | Type | Description |
|---|---|---|
text_layer_id |
Any |
The text layer ID |
text_layers.delete(text_layer_id)Delete a text layer.
| Parameter | Type | Description |
|---|---|---|
text_layer_id |
Any |
The text layer ID |
text_layers.update(text_layer_id, name)Update a text layer's name.
| Parameter | Type | Description |
|---|---|---|
text_layer_id |
Any |
The text layer ID |
name |
Any |
The name |
text_layers.shift(text_layer_id, direction)Shift a text layer's order within the project.
| Parameter | Type | Description |
|---|---|---|
text_layer_id |
Any |
The text layer ID |
direction |
Any |
The direction ("up" or "down") |
Texts
texts.get(text_id)Get a text.
| Parameter | Type | Description |
|---|---|---|
text_id |
Any |
The text ID |
texts.delete(text_id)Delete a text and all dependent data.
| Parameter | Type | Description |
|---|---|---|
text_id |
Any |
The text ID |
texts.set_metadata(text_id, body)Replace all metadata for a text.
| Parameter | Type | Description |
|---|---|---|
text_id |
Any |
The text ID |
body |
Any |
The request body |
texts.delete_metadata(text_id)Remove all metadata from a text.
| Parameter | Type | Description |
|---|---|---|
text_id |
Any |
The text ID |
texts.patch_metadata(text_id, body)Edit metadata for a text with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
text_id |
Any |
The text ID |
body |
Any |
The metadata ops |
Token Layers
token_layers.get(token_layer_id)Get a token layer by ID.
| Parameter | Type | Description |
|---|---|---|
token_layer_id |
Any |
The token layer ID |
token_layers.delete(token_layer_id)Delete a token layer.
| Parameter | Type | Description |
|---|---|---|
token_layer_id |
Any |
The token layer ID |
token_layers.update(token_layer_id, name)Update a token layer's name.
| Parameter | Type | Description |
|---|---|---|
token_layer_id |
Any |
The token layer ID |
name |
Any |
The name |
token_layers.shift(token_layer_id, direction)Shift a token layer's display order.
| Parameter | Type | Description |
|---|---|---|
token_layer_id |
Any |
The token layer ID |
direction |
Any |
The direction ("up" or "down") |
Tokens
tokens.set_metadata(token_id, body)Replace all metadata for a token.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
body |
Any |
The request body |
tokens.delete_metadata(token_id)Remove all metadata from a token.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
tokens.patch_metadata(token_id, body)Edit metadata for a token with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
body |
Any |
The metadata ops |
tokens.get(token_id)Get a token.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
tokens.delete(token_id)Delete a token and remove it from any spans.
If this causes a span to have no remaining tokens, the span will also be deleted.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
tokens.bulk_create(body)Create multiple tokens in a single operation.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
tokens.bulk_delete(body)Delete multiple tokens in a single operation. Provide a list of IDs.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
tokens.bulk_update(body)Patch the metadata of many tokens in a single operation.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
A list of {"id": ..., "metadata": [...]} objects; each metadata is a list of metadata ops, as for patch_metadata. The tokens may lie in several documents of one project. Every document touched has its version bumped, and every new version comes back in X-Document-Versions (past fifty documents, only their number, in X-Document-Versions-Omitted). A document-version precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update. |
tokens.merge(token_id, other_token_id)Merge two tokens.
The left token (smaller begin) survives with the combined extent; the right is deleted and its spans and vocab-links are reparented to the left. On partitioning layers the two tokens must be adjacent; on non-overlapping layers the merged extent must not engulf a third token.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The anchor token ID |
other_token_id |
Any |
The other token to merge in |
tokens.shift(token_id, begin, end)Shift a token's boundary.
On partitioning layers the adjacent token is auto-adjusted to preserve the partition; on non-overlapping layers the shift is rejected if it would create an overlap.
| Parameter | Type | Description |
|---|---|---|
token_id |
Any |
The token ID |
begin |
Any |
New start offset, inclusive (Unicode code points). Omit to leave unchanged. |
end |
Any |
New end offset, exclusive (Unicode code points). Omit to leave unchanged. |
User Data
user_data.get()Read one entry ({key, updated_at, value}); 404 if absent.
user_data.put()Create or replace one entry. value is any JSON (up to 1 MB).
The server stores it verbatim, but this client recases object keys on the way out and back like any other body (my_key <-> my-key), so a value whose keys are snake_case round-trips unchanged while one keyed by arbitrary strings does not. Put such a map under a metadata key, which both clients pass through untouched.
user_data.delete()Delete one entry; 404 if absent.
Users
users.list(q)List (or search) users. Admin-or-maintainer only.
Transparently follows server-side pagination cursors and returns the full flat list.
| Parameter | Type | Description |
|---|---|---|
q |
Any |
Filter to users whose display name or email contains this text (case-insensitive) |
users.iter_pages(q, page_size)Iterate over pages of users, yielding each page's entries list.
| Parameter | Type | Description |
|---|---|---|
q |
Any |
Filter to users whose display name or email contains this text (case-insensitive) |
page_size |
Any |
Page size (1..1000) |
users.get(id)Get a user by ID.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
users.delete(id)Deactivate a user.
Users are never hard-deleted: deactivation rejects their logins and tokens, strips their project memberships and vocab maintainerships, and revokes their API tokens. The user stays visible in listings with a deactivated-at timestamp. Reversible via :meth:activate, which restores login only.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
users.activate(id)Reactivate a deactivated user, restoring their ability to log in.
Project memberships, vocab maintainerships, and API tokens removed at deactivation are NOT restored — re-grant them deliberately.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
users.get_avatar(id)Get a user's profile picture as raw bytes.
Raises for 404 when the user has no picture, so check the user record's avatar_hash first if that is not an error for you.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The user ID |
users.set_avatar(id, file)Upload a profile picture. Your own, or anyone's if you are an admin.
The server center-crops to a square, scales to the configured edge length, and re-encodes, so no client-side resizing is needed. Accepts PNG, JPEG, WebP, and GIF. Returns the updated user record, whose avatar_hash is the new picture's cache key.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The user ID |
file |
Any |
The image to upload (an open binary file object or bytes) |
users.delete_avatar(id)Remove a profile picture. Your own, or anyone's if you are an admin.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The user ID |
users.avatar_url(id, avatar_hash)URL for a user's profile picture, with the session token in the query string so it works in contexts that cannot set an Authorization header (an HTML image element, say).
Pass the user record's avatar_hash whenever you have it: the URL then addresses that exact picture, so it can be cached indefinitely and still picks up a replacement the moment the user changes it.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The user ID |
avatar_hash |
Any |
The user record's avatar_hash |
Vocab Items
vocab_items.bulk_create(body)Create multiple vocab items in a single operation.
Entries may target different vocab layers; the user must have write access to each. Each entry is a dict with keys vocab_layer_id, form, and optional metadata.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The vocab items to create |
vocab_items.bulk_update(body)Update many vocab items in a single operation: set forms and/or patch metadata.
Each entry is a dict with id and either or both of form (set only when the key is present) and metadata (a list of metadata ops, as for patch_metadata). The entries may lie in several vocab layers; the user must have write access to each. An unknown id refuses the whole update, and an id may appear only once. Only an entry whose form really changes restates the documents linking it, and a strict-mode client picks up their new versions from the response.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The vocab item updates |
vocab_items.bulk_delete(body)Delete multiple vocab items in a single operation. Provide a list of IDs.
Each item's descendant vocab links are deleted too. Every document holding one of those links has its version bumped, and a strict-mode client picks up their new versions from the response.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The vocab item IDs to delete |
vocab_items.get(id)Get a vocab item by ID.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_items.delete(id, expected_link_count)Delete a vocab item, and every link to it.
Every document holding one of those links has its version bumped, and a strict-mode client picks up their new versions from the response.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
expected_link_count |
Any |
The number of links the caller showed. The delete is refused with a 409 when the entry has any other number of links by then. |
vocab_items.merge(survivor_id, loser_ids)Merge entries into this one, in one operation.
Every link to a loser moves to the survivor (keeping its id and metadata), except a link on words the survivor is already linked to, which is deleted, and then the losers are deleted. Links are read when the merge runs, so one made after the caller looked moves too. Every loser must be in the survivor's vocabulary, and a loser that is already gone is skipped, so a repeated merge changes nothing. References to a loser in other entries' metadata are the caller's to rewrite, in the same batch. Needs maintainer rights on the vocabulary.
| Parameter | Type | Description |
|---|---|---|
survivor_id |
Any |
The entry that stays |
loser_ids |
Any |
The entries merged into it |
vocab_items.update(id, form)Update a vocab item's form.
A document read carries the entry's form on every link to it, so a rename restates those documents: each has its version bumped, and a strict-mode client picks up their new versions from the response.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
form |
Any |
The vocab item form |
vocab_items.set_metadata(id, body)Replace all metadata for a vocab item.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
body |
Any |
The request body |
vocab_items.delete_metadata(id)Remove all metadata from a vocab item.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_items.patch_metadata(id, body)Edit metadata for a vocab item with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
body |
Any |
The metadata ops |
Vocab Layers
vocab_layers.get_item_at(id, item_id, as_of)One entry of the vocabulary as it was at as_of, also when it has been deleted since.
The same shape as vocab_items.get. A 404 when the entry was not in this vocabulary at that time.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The vocabulary ID |
item_id |
Any |
The entry ID |
as_of |
Any |
The moment to read at (ISO-8601 instant) |
vocab_layers.delete(id)Delete a vocab layer.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_layers.update(id, name)Update a vocab layer's name.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
name |
Any |
The name |
vocab_layers.list()List all vocab layers accessible to the current user.
Transparently follows server-side pagination cursors and returns the full flat list.
vocab_layers.list_page(limit, cursor)List one page of vocab layers.
| Parameter | Type | Description |
|---|---|---|
limit |
Any |
Page size (1..1000) |
cursor |
Any |
Opaque cursor from a previous page's next_cursor |
vocab_layers.iter_pages(page_size)Iterate over pages of vocab layers, yielding each page's entries list.
| Parameter | Type | Description |
|---|---|---|
page_size |
Any |
Page size (1..1000) |
vocab_layers.create(name, [id])Create a new vocab layer.
Also registers the current user as a maintainer.
| Parameter | Type | Description |
|---|---|---|
name |
Any |
The name |
id optional |
Any |
Optional. The id to create it under, a UUIDv7 this client minted (plaid_client.uuid7()), so a create sent again after its answer was lost lands once (409 with id_taken when the id was used before). |
vocab_layers.add_maintainer(id, user_id)Assign a user as a maintainer for this vocab layer.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
vocab_layers.remove_maintainer(id, user_id)Remove a user's maintainer privileges for this vocab layer.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
user_id |
Any |
The user ID |
Vocab Links
vocab_links.bulk_create(body)Create multiple vocab links in a single operation.
Entries may reference different vocab items, but all tokens across the call must belong to one document. Each entry is a dict with keys vocab_item, tokens, and optional metadata.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The vocab links to create |
vocab_links.bulk_delete(body)Delete multiple vocab links in a single operation. Provide a list of IDs.
| Parameter | Type | Description |
|---|---|---|
body |
Any |
The request body |
vocab_links.set_metadata(id, body)Replace all metadata for a vocab link.
The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
body |
Any |
The request body |
vocab_links.delete_metadata(id)Remove all metadata from a vocab link.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_links.patch_metadata(id, body)Edit metadata for a vocab link with a list of ops applied in order.
{"op": "set", "path": [...], "value": v} writes v at the path, creating missing objects along it; {"op": "delete", "path": [...]} removes the key at the path (a no-op when absent). A path is a non-empty list of keys, the first a top-level key. A path through a non-object is refused (400). See :func:metadata_ops and :func:apply_metadata_ops.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
body |
Any |
The metadata ops |
vocab_links.get(id)Get a vocab link by ID.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
vocab_links.delete(id)Delete a vocab link.
| Parameter | Type | Description |
|---|---|---|
id |
Any |
The resource ID |
Comments
comments.get()Read one comment.
comments.update()Edit a comment's body.
Only the comment's author may do this — not maintainers, not admins. Sets
editedon the returned comment.comments.delete()Delete a comment (author, or a maintainer of its project).
comments.list_in_vocab(vocab_id, entity_id)List the comments on a vocabulary's entries, oldest first.
Requires read access to the vocabulary. Transparently follows server-side pagination cursors and returns the full flat list.
vocab_idAnyentity_idAnycomments.counts_in_vocab()Comment counts per entry of a vocabulary, as an
{entry_id: n}dict.The response is NOT key-transformed: its keys are entry ids.