| Version: | 0.5.0 |
| Title: | Access 'Zotero' Libraries |
| Description: | Interface to the 'Zotero' reference manager https://www.zotero.org through its public web API and local client database. Provides paginated reads, versioned writes, read-only local queries, batch plans and resumable ledgers, bibliographic comparison, metadata mapping, and file attachment. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| Language: | en-US |
| URL: | https://kanameishi.github.io/zot/ |
| Depends: | R (≥ 4.1.0) |
| Imports: | data.table, DBI, digest, httr2, jsonlite, RSQLite |
| Suggests: | knitr, rmarkdown, testthat (≥ 3.2.0) |
| VignetteBuilder: | knitr, rmarkdown |
| Config/testthat/edition: | 3 |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-25 19:42:26 UTC; averrik |
| Author: | Alejandro Verri Kozlowski
|
| Maintainer: | Alejandro Verri Kozlowski <averri@fi.uba.ar> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-25 20:00:03 UTC |
zot: Access Zotero Libraries
Description
An interface to Zotero's web API and local client database. The package provides read operations, batch plans and ledgers, bibliographic comparison, metadata mapping, and attachments.
Details
Start with vignette("zot", package = "zot"). Functions can be called from
interactive sessions, scripts, automated jobs, and agent applications.
Author(s)
Maintainer: Alejandro Verri Kozlowski averri@fi.uba.ar (ORCID) [copyright holder]
Authors:
Alejandro Verri Kozlowski averri@fi.uba.ar (ORCID) [copyright holder]
See Also
Useful links:
Create an approval record for a preview
Description
Stores a preview digest, optional notes, and a timestamp in an object used
by zotExecute().
Usage
zotApprove(preview, notes = "")
Arguments
preview |
A |
notes |
Free-text metadata associated with the record. |
Details
This function does not authenticate a person, evaluate an authorization
policy, display the plan, or perform a write. The calling application
determines how approval records are created. A preview should not be
modified after this record is created: zotExecute() checks the approval
against the preview digest and recalculates the digest from the plan before
making requests.
Value
A zotApproval containing the preview digest.
See Also
zotPreview() and zotExecute().
Examples
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "ABCD1234",
payload = list(list(title = "A reviewed title"))
)
Preview <- zotPreview(Plan, describe = "Synthetic documentation batch")
Approval <- zotApprove(Preview, notes = "Documentation example")
Approval$digest == Preview$digest
List the collections of a library with their paths
Description
Reads every collection and derives each one's path from the root, joined
with / exactly as ztCollectionTree() names the collections it returns.
Usage
zotCollections(config, library = "user")
Arguments
config |
A |
library |
|
Value
A data.table in API order with collectionKey, name,
parentCollection (NA at the root), path, numItems and version.
See Also
ztCollectionTree(), ztRefile(), ztCensus().
Examples
Collections <- list(
list(key = "ROOT", version = 3L, meta = list(numItems = 2L),
data = list(name = "Projects", parentCollection = FALSE)),
list(key = "LEAF", version = 4L, meta = list(numItems = 1L),
data = list(name = "2026", parentCollection = "ROOT"))
)
Performer <- function(config, method, path, query, body, version) {
list(status = 200L, headers = list(`total-results` = "2"),
body = Collections)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotCollections(Config)
Configure Zotero API access
Description
Creates a configuration object for requests to the Zotero Web API. The object stores the user identifier, API key, and an optional request performer and file transport used by tests or alternative transports. Arguments may be passed explicitly or read from environment variables, which supports interactive, scripted, and agent-driven use.
Usage
zotConfig(
userID = Sys.getenv("ZOTERO_LIBRARY_ID"),
key = Sys.getenv("ZOTERO_API_KEY"),
performer = NULL,
fileTransport = NULL
)
Arguments
userID |
Zotero user identifier. The default reads
|
key |
Zotero API key. The default reads |
performer |
Optional function used by |
fileTransport |
Optional list of the three file-upload steps
( |
Value
An object of class zotConfig.
Examples
Config <- zotConfig(userID = "42", key = "example-key")
Config
Execute a previewed batch with a resumable ledger
Description
Recalculates the plan's digest and requires it to match both the preview and its approval, refuses a ledger written for a different batch, and skips the entries an earlier run completed. Every row is recorded before and after its effect, so an interrupted batch leaves a ledger that names the row whose outcome is unknown.
Usage
zotExecute(config, preview, approval, ledgerPath)
Arguments
config |
A |
preview |
The |
approval |
Its |
ledgerPath |
JSON ledger file; reuse the same path to resume. |
Details
Patch operations read the current item version, use a conditional write,
and compare requested fields with a subsequent API read. Create and attach
operations persist the new key as "created" before reading it back. Trash operations use the status
returned by zotTrash(). The approval record is application metadata; this
function does not independently determine who authorized the batch.
The digest is recalculated from the plan being executed, so a preview whose
rows changed after approval is refused instead of written under the old
consent. Before each effect the row is written as "attempt" and the ledger
is replaced atomically; the result overwrites it immediately afterwards. A
resumed batch that meets an "attempt" stops with a zotLedgerError: that
row's effect is unknown, and only the caller can reconcile it against the
library before retrying.
If a create read-back fails, its "created" entry retains the new key and
error. Resuming only reads that key and confirms the payload; it never posts
the item again. A transport exception during POST remains an "attempt"
because its effect is unknown. A server failure or a response without a
recognized result also remains an attempt. A rejected create response can be retried.
An attach row first recalculates the MD5 of its local file and refuses a
file changed since approval with a zotApprovalError, recorded as
"failed" before any request. It then creates the attachment item like a
create row and saves its key as "created". On that key it runs Zotero's
upload protocol (authorize, where "exists" counts as uploaded; upload;
register) through the configuration's file transport, then confirms the
item's MD5, filename, content type and parent. A failed upload leaves the
entry "created" with its error; resuming repeats the upload on the same
key, after checking the file again, and never creates a second item.
New ledgers have schema = 2 and identify existing items as library:key;
creates retain create-<row> for block-move plans. Ledgers without a schema
keep their original key-only format when every id selects exactly one plan
row. An ambiguous legacy ledger is refused without rewriting it. A legacy
failed create without a retained key also requires caller reconciliation:
older executors could lose the key after a successful POST.
Value
The ledger as a list (class zotLedger).
See Also
zotPreview(), zotApprove(), and zotVerify().
Examples
Store <- new.env(parent = emptyenv())
Store$version <- 1L
Store$title <- "Before"
Performer <- function(config, method, path, query, body, version) {
if (method == "GET") {
return(list(
status = 200L, headers = list(),
body = list(version = Store$version, data = list(title = Store$title))
))
}
Store$title <- body$title
Store$version <- Store$version + 1L
list(status = 204L, headers = list(), body = NULL)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "K1",
payload = list(list(title = "After"))
)
Preview <- zotPreview(Plan, describe = "Synthetic offline patch")
Approval <- zotApprove(Preview, notes = "Documentation example")
LedgerPath <- tempfile(fileext = ".json")
Ledger <- zotExecute(Config, Preview, Approval, ledgerPath = LedgerPath)
Ledger$items[["user:K1"]]$status
unlink(LedgerPath)
Fetch every page of a listing endpoint
Description
Follows Total-Results with start/limit pagination and returns the
concatenated item list.
Usage
zotGetAll(config, path, query = list(), limit = 100L)
Arguments
config |
A |
path |
API path starting at the host root (see |
query |
Named list of query parameters. |
limit |
Page size. |
Value
A list of parsed items.
See Also
zotGet() and zotRequest().
Examples
Performer <- function(config, method, path, query, body, version) {
Page <- if (query$start == 0L) {
list(list(key = "A"), list(key = "B"))
} else {
list(list(key = "C"))
}
list(status = 200L, headers = list(`total-results` = "3"), body = Page)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
Items <- zotGetAll(Config, path = "/users/42/items", limit = 2L)
vapply(Items, function(Item) Item$key, character(1L))
Map a record to the fields compared by strong identity
Description
Accepts an item as the Web API returns it (with key, data, library
and links), the data of such an item, a row of zotLocalRecords(), or
an identity list already in this shape, and returns the same identity list
for all of them: one rule for every plane.
Usage
zotIdentity(record)
Arguments
record |
A list or one-row data.frame in any of the shapes above. |
Details
year is the first four-digit year in date. author is the family name
(or single-field name) of the first creator of type author; an item
without one has NA. hasFile is TRUE for an API item whose links
carry an attachment, the value of an explicit hasFile field, and NA
when unknown, so that the file guard of zotSameItem() only acts on
observed facts. library is "user" or the group id when the record says
where it lives, NA otherwise.
Value
A list: itemKey, library, doi, md5, title, year,
author, hasFile.
See Also
zotSameItem(), ztMatches(), zotLocalRecords().
Examples
Item <- list(
key = "K1", library = list(type = "group", id = 555L),
links = list(attachment = list(href = "…")),
data = list(title = "A record", date = "March 2026", DOI = "10.1000/x",
creators = list(list(creatorType = "editor", lastName = "First"),
list(creatorType = "author", lastName = "Lovelace")))
)
zotIdentity(Item)
Read items by key
Description
Fetches the current API record of each key in batches of fifty, the
maximum the itemKey filter accepts. Keys the API does not return are
reported in missing; absence is a verdict, not an error.
Usage
zotItems(config, keys, library = "user", includeTrashed = FALSE, fields = NULL)
Arguments
config |
A |
keys |
Character vector of item keys, without duplicates. |
library |
|
includeTrashed |
Whether items in the Trash are returned. |
fields |
Optional character vector: keep only these |
Value
A list: items (named by key, in the order of keys), missing
and version (the library's Last-Modified-Version).
See Also
zotListItems(), zotGet(), zotVerify().
Examples
Performer <- function(config, method, path, query, body, version) {
list(status = 200L, headers = list(`last-modified-version` = "9"),
body = list(list(key = "K1", version = 1L,
data = list(title = "One", abstractNote = "…"))))
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
Read <- zotItems(Config, keys = c("K1", "K2"), fields = "title")
names(Read$items)
Read$missing
List the personal library and the accessible groups
Description
Reads the group memberships of the configured user and the current version
of every library. The personal library appears first as "user"; each
group appears under its numeric identifier, the value accepted by the
library argument of the other functions.
Usage
zotLibraries(config)
Arguments
config |
A |
Details
One request lists the groups and one limit = 1 request per library reads
its Last-Modified-Version header. The personal library has no name in the
API; its name is NA.
Value
A data.table with library, type ("user" or "group"),
name and version.
See Also
zotLibraryPath(), ztCensus(), zotCollections().
Examples
Performer <- function(config, method, path, query, body, version) {
if (grepl("/groups$", path)) {
return(list(status = 200L, headers = list(`total-results` = "1"),
body = list(list(id = 555L, data = list(name = "Team")))))
}
list(status = 200L, headers = list(`last-modified-version` = "12"),
body = list())
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotLibraries(Config)
Build a Zotero library API path
Description
Build a Zotero library API path
Usage
zotLibraryPath(config, library = "user")
Arguments
config |
A |
library |
|
Value
The API path prefix for the selected library.
See Also
zotConfig() and zotRequest().
Examples
Config <- zotConfig(userID = "42", key = "example-key")
zotLibraryPath(Config)
zotLibraryPath(Config, library = "5107760")
List items with a declared bound
Description
Bounded, version-stable listing of a library, a collection or an item's
children. One page is returned unless all = TRUE, which follows the
pagination up to max results and refuses a larger listing before
downloading it. The library version is read from every page and a change
between pages is reported as an error, so a listing never mixes two states.
Usage
zotListItems(
config,
library = "user",
collectionKey = NULL,
parentKey = NULL,
top = FALSE,
since = NULL,
itemType = NULL,
includeTrashed = FALSE,
versions = FALSE,
fields = NULL,
limit = PAGE_LIMIT,
all = FALSE,
max = NULL
)
Arguments
config |
A |
library |
|
collectionKey |
Restrict to the members of this collection. |
parentKey |
Restrict to the children of this item. |
top |
Top-level items only; it cannot be combined with |
since |
Only objects modified after this library version. |
itemType |
An API item type filter, for example |
includeTrashed |
Whether items in the Trash are returned. |
versions |
Return the key-to-version map instead of the records. |
fields |
Optional character vector: keep only these |
limit |
Page size, at most one hundred. |
all |
Follow the pagination to the end of the listing. |
max |
Required with |
Value
A list: items, version, total and truncated.
See Also
zotItems(), zotGetAll(), ztCensus().
Examples
Performer <- function(config, method, path, query, body, version) {
Page <- if (query$start == 0L) list(list(key = "A"), list(key = "B")) else list(list(key = "C"))
list(status = 200L,
headers = list(`total-results` = "3", `last-modified-version` = "7"),
body = Page)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotListItems(Config, top = TRUE, limit = 2L)$truncated
zotListItems(Config, top = TRUE, limit = 2L, all = TRUE, max = 10L)$total
Locate attachment files in the local Zotero storage
Description
Lists non-trashed attachments with their parent, hash and the local file
when it exists: imported files live under storage/<key>/ beside the
database, linked files at their absolute path. Filter by the keys of
attachments or of their parent records, or by MD5.
Usage
zotLocalFiles(
keys = NULL,
md5 = NULL,
path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite")
)
Arguments
keys |
Optional character vector of attachment or parent item keys. |
md5 |
Optional character vector of file hashes. |
path |
Database file; the storage directory is its sibling |
Details
The database may lag the Web API and a file may be absent locally even
when the server holds it: a NA in file is an observation about this
machine, not about the library. linkMode follows the client: 0 imported
file, 1 imported URL, 2 linked file, 3 linked URL.
Value
A data.table with itemKey, parentKey, groupID,
contentType, linkMode, md5 and file.
See Also
zotLocalRecords(), ztAttach(), zotSameItem().
Examples
Path <- tempfile(fileext = ".sqlite")
Connection <- DBI::dbConnect(RSQLite::SQLite(), Path)
DBI::dbWriteTable(Connection, "libraries", data.frame(libraryID = 1L))
DBI::dbWriteTable(Connection, "groups",
data.frame(libraryID = integer(), groupID = integer()))
DBI::dbWriteTable(Connection, "items", data.frame(
itemID = 1:2, key = c("PARENT", "FILE"), libraryID = 1L
))
DBI::dbWriteTable(Connection, "itemAttachments", data.frame(
itemID = 2L, parentItemID = 1L, linkMode = 0L,
contentType = "application/pdf", path = "storage:paper.pdf",
storageHash = "abc"
))
DBI::dbWriteTable(Connection, "deletedItems", data.frame(itemID = integer()))
DBI::dbDisconnect(Connection)
zotLocalFiles(keys = "PARENT", path = Path)
unlink(Path)
List items from the local database
Description
Returns key, library, type, title, and dates for non-trashed items as a
data.table. See zotLocalQuery() for synchronization and read-only details.
Usage
zotLocalItems(
library = NULL,
path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite")
)
Arguments
library |
|
path |
Database file. |
Value
A data.table with key, groupID (0 for personal), itemType,
title, dateAdded and dateModified.
See Also
zotLocalQuery() and ztCensus().
Examples
# A temporary database with the tables used by zotLocalItems().
Path <- tempfile(fileext = ".sqlite")
Connection <- DBI::dbConnect(RSQLite::SQLite(), Path)
DBI::dbWriteTable(Connection, "libraries", data.frame(libraryID = 1:2))
DBI::dbWriteTable(Connection, "groups",
data.frame(libraryID = 2L, groupID = 555L))
DBI::dbWriteTable(Connection, "itemTypes",
data.frame(itemTypeID = 1L, typeName = "journalArticle"))
DBI::dbWriteTable(Connection, "items", data.frame(
itemID = 1:3, key = c("AAA", "BBB", "CCC"), libraryID = c(1L, 2L, 1L),
itemTypeID = 1L, dateAdded = "2026-01-01", dateModified = "2026-01-01"
))
DBI::dbWriteTable(Connection, "fieldsCombined",
data.frame(fieldID = 110L, fieldName = "title"))
DBI::dbWriteTable(Connection, "itemData",
data.frame(itemID = 1:3, fieldID = 110L, valueID = 1:3))
DBI::dbWriteTable(Connection, "itemDataValues", data.frame(
valueID = 1:3, value = c("Personal article", "Group article", "Trashed article")
))
DBI::dbWriteTable(Connection, "deletedItems", data.frame(itemID = 3L))
DBI::dbDisconnect(Connection)
zotLocalItems(library = "user", path = Path)
zotLocalItems(library = 555, path = Path)
unlink(Path)
Read-only query over the local Zotero database
Description
Opens the local zotero.sqlite database in read-only mode. Local records
can lag the Web API until the desktop client synchronizes, and WAL mode can
make recent local changes temporarily unavailable to another reader.
Usage
zotLocalQuery(
sql,
params = list(),
path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite")
)
Arguments
sql |
One read-only SQL statement. |
params |
Positional query parameters. |
path |
Database file (default: the standard Zotero location). |
Details
The database connection is opened with SQLite's read-only flag, without
issuing any PRAGMA, and closed when the query finishes. zotLocalQuery()
does not copy or modify the desktop database. The caller remains
responsible for supplying a read-only SQL statement. The default path is
~/Zotero/zotero.sqlite; pass path explicitly for a portable
installation or a custom Zotero data directory.
While Zotero is writing, it locks the database. A read waits for the write
to finish for up to LOCALWAIT, a package constant of 10000 milliseconds
(10 s). If the database is still locked after that wait, the query fails
with a zotLocalError naming the wait and the original SQLite error; any
other SQLite error propagates unchanged.
Value
Query result as a data.table.
See Also
zotLocalItems() and zotRequest().
Examples
Path <- tempfile(fileext = ".sqlite")
Connection <- DBI::dbConnect(RSQLite::SQLite(), Path)
DBI::dbWriteTable(Connection, "items", data.frame(key = c("A", "B")))
DBI::dbDisconnect(Connection)
zotLocalQuery("SELECT key FROM items ORDER BY key", path = Path)
unlink(Path)
Read bibliographic records with their identity fields from the local database
Description
Returns, for every non-trashed regular item, the fields that
zotSameItem() compares: DOI, title, date and its year, the family name of
the first author, and whether the item carries a file. Attachments, notes
and annotations are not records and are excluded. See zotLocalQuery() for
synchronization and read-only details.
Usage
zotLocalRecords(
library = NULL,
path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite")
)
Arguments
library |
|
path |
Database file. |
Details
author is the family name of the creator of type author with the lowest
order; a record without an author has NA there and can only match by DOI
or file. hasFile is TRUE when a child attachment is an imported or
linked file (linkMode 0, 1 or 2); a linked URL is not a file. year is
the first four-digit year found in date, the same rule as
zotIdentity().
Value
A data.table with itemKey, groupID (0 for personal),
itemType, title, doi, date, year, author and hasFile.
See Also
zotLocalItems(), zotIdentity(), ztMatches().
Examples
Path <- tempfile(fileext = ".sqlite")
Connection <- DBI::dbConnect(RSQLite::SQLite(), Path)
DBI::dbWriteTable(Connection, "libraries", data.frame(libraryID = 1L))
DBI::dbWriteTable(Connection, "groups",
data.frame(libraryID = integer(), groupID = integer()))
DBI::dbWriteTable(Connection, "itemTypes",
data.frame(itemTypeID = 1L, typeName = "journalArticle"))
DBI::dbWriteTable(Connection, "items", data.frame(
itemID = 1L, key = "AAA", libraryID = 1L, itemTypeID = 1L,
dateAdded = "2026-01-01", dateModified = "2026-01-01"
))
DBI::dbWriteTable(Connection, "fieldsCombined", data.frame(
fieldID = c(110L, 26L, 14L), fieldName = c("title", "DOI", "date")
))
DBI::dbWriteTable(Connection, "itemData", data.frame(
itemID = 1L, fieldID = c(110L, 26L, 14L), valueID = 1:3
))
DBI::dbWriteTable(Connection, "itemDataValues", data.frame(
valueID = 1:3, value = c("A record", "10.1000/x", "2026-03-01")
))
DBI::dbWriteTable(Connection, "creatorTypes",
data.frame(creatorTypeID = 1L, creatorType = "author"))
DBI::dbWriteTable(Connection, "creators", data.frame(
creatorID = 1L, firstName = "Ada", lastName = "Lovelace"
))
DBI::dbWriteTable(Connection, "itemCreators", data.frame(
itemID = 1L, creatorID = 1L, creatorTypeID = 1L, orderIndex = 0L
))
DBI::dbWriteTable(Connection, "itemAttachments", data.frame(
itemID = integer(), parentItemID = integer(), linkMode = integer(),
contentType = character(), path = character(), storageHash = character()
))
DBI::dbWriteTable(Connection, "deletedItems", data.frame(itemID = integer()))
DBI::dbDisconnect(Connection)
zotLocalRecords(path = Path)
unlink(Path)
Preview a batch plan
Description
Validates and summarizes a plan for execution by zotExecute(). A row has
action "patch", "create", "attach", or "trash"; a user or group
library; an item key for patch and Trash rows; and a payload list.
Usage
zotPreview(plan, describe)
Arguments
plan |
A data.table with columns |
describe |
Text identifying the batch. |
Details
zotPreview() validates the plan, rejects unknown actions and recognized
source labels, copies the rows, and calculates a digest from a canonical JSON
representation. It does not contact Zotero.
Three rules protect the ledger and the write itself. A create row leaves
itemKey as NA, because zotExecute() identifies a create by its
position and a keyed create would make a resumed batch create the item
again. An identified item appears once per library, because the ledger
holds one entry per library and key and a repeated pair would drop the second
row. A payload carries no key or version field: zotPatch() sends the
version as a conditional header, and a payload version is read back
changed, which reports a successful write as failed.
An attach row creates an attachment item and uploads a local file; it
leaves itemKey as NA, like a create row. Its payload carries
parentItem (NULL for a standalone attachment), the local path, the
stored filename, contentType, an optional charset, the file's md5
(32 lowercase hexadecimal digits) and filesize (a whole number of bytes
above zero); ztAttachPlan() builds such rows. Source labels are refused in
filename only, because the local path is never stored in Zotero. The digest
covers the whole payload, so an approval binds the exact bytes through their
MD5.
Value
A zotPreview: the plan, its canonical SHA-256 digest, counts
by action and library (N rows and the bytes attach rows upload, 0 for
other actions) and creation time.
See Also
zotApprove(), zotExecute(), zotVerify(), and
ztAttachPlan().
Examples
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "ABCD1234",
payload = list(list(title = "A reviewed title"))
)
Preview <- zotPreview(Plan, describe = "Correct one title")
Preview
Read a batch file written by zot
Description
Reads a plan, preview, approval or ledger file and rebuilds the
corresponding object. A preview is rebuilt through zotPreview(), so its
digest is recalculated from the rows on disk and compared with the stored
one: a file edited after it was written is refused.
Usage
zotReadBatch(path, digest = NULL)
Arguments
path |
A file written by |
digest |
Optional expected SHA-256 digest; a mismatch is refused. |
Details
A ledger has no kind; it is recognized by its digest and items and
returned as the zotLedger list that zotExecute() produces.
The ledger's optional schema is preserved; reading
a legacy ledger does not migrate it or authorize its execution. A plan has no
stored digest; when digest is supplied for a plan it is compared with the
digest the plan would have under zotPreview().
Value
A plan data.table, a zotPreview, a zotApproval or a
zotLedger.
See Also
zotWriteBatch(), zotExecute().
Examples
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "ABCD1234",
payload = list(list(title = "A reviewed title"))
)
Preview <- zotPreview(Plan, describe = "Correct one title")
Path <- tempfile(fileext = ".json")
zotWriteBatch(Preview, Path)
identical(zotReadBatch(Path)$digest, Preview$digest)
unlink(Path)
Perform one Zotero API request
Description
Core transport for every zot API call: bounded retries, Backoff and
Retry-After honoring, and conditional writes through
If-Unmodified-Since-Version. Returns for every HTTP status — callers
interpret status (a 404 is a verdict, not a transport failure); only an
exhausted transport raises a zotHttpError.
Usage
zotRequest(config, method, path, query = list(), body = NULL, version = NULL)
zotGet(config, path, query = list())
zotPost(config, path, body)
zotPatch(config, path, body, version)
Arguments
config |
A |
method |
HTTP method: |
path |
API path starting at the host root (see |
query |
Named list of query parameters. |
body |
Request body, serialized as JSON when not |
version |
Value for |
Details
A performer receives config, method, path, query, body, and
version, and returns a list with status, headers, and body. This
makes the complete HTTP boundary replaceable in examples and tests without
changing the public request contract. Responses eligible for retry are
bounded by five attempts; other HTTP statuses are returned unchanged.
POST is retried only after rate limiting (HTTP 429). A server failure on
POST can follow an effect, so it is returned without another write; the
guarded executor retains that unknown outcome as an attempt. GET and
versioned PATCH retain their retries on HTTP 500, 502 and 503.
Value
A list with status, headers (named list) and body (parsed
JSON, or NULL when empty).
See Also
zotGetAll(), zotTrash(), and zotPreview().
Examples
Performer <- function(config, method, path, query, body, version) {
list(
status = if (method == "POST") 201L else 200L,
headers = list(),
body = list(method = method, path = path, body = body, version = version)
)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotGet(Config, path = "/users/42/items/ABCD1234")$status
zotPost(Config, path = "/users/42/items", body = list(list(title = "New")))$status
zotPatch(Config, path = "/users/42/items/ABCD1234",
body = list(title = "Revised"), version = 7L)$body$version
Strong-identity comparison of two bibliographic records
Description
A duplicate is only what strong identity proves: an exact DOI, an exact attachment MD5, or normalized title plus year plus first-author family name. A bare title match proves nothing — generic titles collide across distinct documents — and reuse is refused when the first record carries a file the second lacks, whatever the metadata says.
Usage
zotSameItem(a, b)
Arguments
a, b |
Lists with any of |
Value
A list: match (logical) and basis ("doi", "md5",
"title-year-author" or "none").
See Also
ztTwins() for pairwise candidate detection.
Examples
A <- list(doi = "10.1000/Example", hasFile = TRUE)
B <- list(doi = "10.1000/example", hasFile = TRUE)
zotSameItem(A, B)
# A title alone never establishes identity.
zotSameItem(list(title = "Introduction"), list(title = "Introduction"))
Probe the data planes zot can reach
Description
Reports, one row per plane, whether the installed package, the API
credentials, the Web API and the local database are available from this
process, with the observation behind each verdict. The API probe is one
limit = 1 read of the personal library; the local probe opens the
database read-only and counts its items.
Usage
zotStatus(
config = NULL,
path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite")
)
Arguments
config |
A |
path |
Local database file, as in |
Details
The credentials row never contains a credential: when a variable is
missing, detail names it and the file where it is defined, as
zotConfig() does. The local plane is advisory; the Web API is the plane
that writes are verified against.
Value
A data.table with plane, available and detail.
See Also
Examples
Performer <- function(config, method, path, query, body, version) {
list(status = 200L, headers = list(`last-modified-version` = "9"),
body = list())
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotStatus(Config, path = tempfile())
Move items to the recoverable Trash
Description
The only removal zot implements: deleted = 1 through a minimal
conditional PATCH. Permanent deletion is not part of this package;
emptying Trash is not provided by this package.
Usage
zotTrash(config, keys, library = "user")
Arguments
config |
A |
keys |
Character vector of item keys. |
library |
|
Value
A data.table with one row per key: itemKey, status
("trashed", "already", "missing" or "failed").
See Also
zotPatch(), zotPreview(), and zotExecute().
Examples
Store <- new.env(parent = emptyenv())
Store$version <- 1L
Store$deleted <- 0L
Performer <- function(config, method, path, query, body, version) {
if (method == "GET") {
return(list(
status = 200L,
headers = list(),
body = list(version = Store$version,
data = list(deleted = Store$deleted))
))
}
Store$deleted <- body$deleted
Store$version <- Store$version + 1L
list(status = 204L, headers = list(), body = NULL)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
zotTrash(Config, keys = "ABCD1234")
Census a ledger against its plan
Description
Summarizes item statuses and re-reads completed rows: a bounded sample of
patches by default, or every completed row when full is TRUE.
Usage
zotVerify(config, preview, ledger, full = FALSE)
Arguments
config |
A |
preview |
The executed |
ledger |
The |
full |
Re-read every completed row instead of sampling patches. |
Details
By default at most ten completed patch rows are sampled and create, attach
and Trash rows are not included. With full = TRUE every completed row is
re-read: a patch and a create compare the payload fields against the current
record, an attach row compares the MD5, filename, content type and parent of
the attachment, and a Trash row confirms deleted. A created item is
resolved through the key its ledger entry recorded. Rows whose current state does not confirm the
plan are listed in unconfirmed; the result does not replace inspection of
failed ledger entries. Missing plan rows are counted as "pending".
Schema and legacy identity checks are the same as zotExecute().
Value
A list: counts by status, sampleChecked, sampleConfirmed and
unconfirmed (the ledger ids that did not confirm), unfinished (plan ids
without a done entry), and complete (all plan rows done and none of the
checked rows unconfirmed). Completeness does not turn a sample into a full
read-back.
See Also
zotExecute() and ztCensus().
Examples
Performer <- function(config, method, path, query, body, version) {
list(
status = 200L, headers = list(),
body = list(version = 2L, data = list(title = "After"))
)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "K1",
payload = list(list(title = "After"))
)
Preview <- zotPreview(Plan, describe = "Synthetic verified patch")
Ledger <- structure(
list(schema = 2L, digest = Preview$digest,
items = list(`user:K1` = list(status = "done"))),
class = "zotLedger"
)
zotVerify(Config, Preview, Ledger)
Write a plan, preview or approval as a batch file
Description
Persists a guarded-flow object as JSON so that planning, approval and
execution can happen in separate processes. The file carries a kind
("zotPlan", "zotPreview" or "zotApproval") and the fields of the
corresponding object; a preview stores its rows together with its digest.
Usage
zotWriteBatch(x, path)
Arguments
x |
A plan (a data.table with |
path |
Destination file. |
Details
The file is written to a temporary sibling and renamed into place, so a
reader never observes a partially written batch file. An existing file at
path is replaced. Ledgers are written by zotExecute(), not here.
Value
path, invisibly.
See Also
zotReadBatch(), zotPreview(), zotApprove().
Examples
Plan <- data.table::data.table(
action = "patch", library = "user", itemKey = "ABCD1234",
payload = list(list(title = "A reviewed title"))
)
Path <- tempfile(fileext = ".json")
zotWriteBatch(Plan, Path)
zotReadBatch(Path)
unlink(Path)
Attach a local file to an existing item
Description
Implements Zotero's full upload protocol: create the attachment item,
request upload authorization (an identical file already in storage
short-circuits as exists), upload to the returned storage URL, register
the upload, and verify the attachment's MD5 server-side. Uploads may consume
Zotero storage quota and have immediate external effects.
Usage
ztAttach(
config,
parentKey,
filePath,
library = "user",
filename = basename(filePath),
contentType = .zotContentType(filePath),
fileTransport = config$fileTransport
)
Arguments
config |
A |
parentKey |
Key of the regular item receiving the file, or |
filePath |
Local file to upload. |
library |
|
filename |
Stored filename; defaults to the file's basename. |
contentType |
MIME type; defaults to Zotero's choice by extension table and text/binary sniff (see Details). |
fileTransport |
The three protocol steps as an injectable list
( |
Details
The default file transport performs every external protocol step. Supplying
fileTransport replaces only authorization, byte upload, and registration;
attachment creation and final API verification still use config. A stored
file with the same MD5 returns "exists" before uploading bytes.
The default content type follows Zotero's desktop client: a table of known
extensions (pdf, epub, md, bib, txt, html, csv, json, office
formats, images and others), and for any other extension a sniff of the
first 1024 bytes, which yields text/plain when every byte is a text
character and application/octet-stream otherwise. An empty file has no
type and is refused with a zotPlanError. A text/* file whose bytes are
valid UTF-8 is created with charset = "utf-8". The guarded lane offers the
same upload with preview, approval and a resumable ledger through
ztAttachPlan() and the attach action of zotExecute().
Value
A list: status ("uploaded", "exists" or "failed"), the
attachment key, and error when failed.
See Also
ztCleanName(), ztAttachPlan(), zotPreview(), and
zotExecute().
Examples
Path <- tempfile(fileext = ".pdf")
writeBin(as.raw(1:8), Path)
Performer <- function(config, method, path, query, body, version) {
list(
status = 200L, headers = list(),
body = list(successful = list(`0` = list(key = "ATT1")))
)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
Transport <- list(
authorize = function(...) list(exists = 1L),
upload = function(...) stop("upload should not run"),
register = function(...) stop("registration should not run")
)
ztAttach(Config, parentKey = NA_character_, filePath = Path,
fileTransport = Transport)
unlink(Path)
Build an attach plan for local files
Description
Produces one attach row per local file for the guarded flow:
zotPreview(), zotApprove(), zotExecute() and zotVerify(). Files
whose MD5 is already an attachment of the same parent are omitted.
Usage
ztAttachPlan(config, files, library = "user")
Arguments
config |
A |
files |
A data.table with |
library |
|
Details
Every file is checked before any request: a missing or empty file is
refused with a zotPlanError. Each row records the file's md5 and
filesize, so the approval of its preview binds the exact bytes. The stored
filename is cleaned with ztCleanName(); contentType and charset
default to the choice ztAttach() documents. The path is kept as given,
and a relative path is resolved against the working directory of execution.
Planning reads the children of each distinct parent once through
zotListItems() and omits the rows whose MD5 is already attached there,
with a message per omitted file. A parent whose children exceed one listing
page is refused with a zotBoundError, because the omission could not see
every attachment. Standalone files are not compared. Planning does not
write.
Value
A guarded-flow plan of attach rows, empty when every file is already attached.
See Also
ztAttach(), zotPreview(), and zotExecute().
Examples
Path <- tempfile(fileext = ".md")
writeLines("# Notes", Path)
Performer <- function(config, method, path, query, body, version) {
list(status = 200L,
headers = list(`total-results` = "0", `last-modified-version` = "7"),
body = list())
}
Config <- zotConfig(userID = "42", key = "example", performer = Performer)
Files <- data.table::data.table(path = Path, parentKey = "PARENT1")
Plan <- ztAttachPlan(Config, files = Files, library = "user")
Plan$payload[[1L]]$contentType
unlink(Path)
Build the create half of a block move
Description
Produces create rows in the destination library from the given records. Items carrying imported files are refused: moving files needs the upload protocol, which this plan builder does not perform.
Usage
ztBlockMoveCreate(records, toLibrary, collectionKey)
Arguments
records |
A data.table with |
toLibrary |
Destination library. |
collectionKey |
Destination collection key. |
Value
A guarded-flow plan of create rows, one per record, whose order
matches records.
See Also
ztBlockMoveTrash(), zotPreview(), and ztAttach().
Examples
Records <- data.table::data.table(
itemKey = "OLD1",
payload = list(list(itemType = "book", title = "Reviewed book")),
hasFile = FALSE
)
ztBlockMoveCreate(Records, toLibrary = "5107760", collectionKey = "DEST")
Build the trash half of a block move from its executed create ledger
Description
Returns Trash rows for originals whose corresponding create-ledger entry is
marked "done" and records a new item key.
Usage
ztBlockMoveTrash(records, createLedger, fromLibrary)
Arguments
records |
The |
createLedger |
The |
fromLibrary |
Library holding the originals. |
Value
A guarded-flow plan of trash rows.
See Also
ztBlockMoveCreate(), zotExecute(), and zotTrash().
Examples
Records <- data.table::data.table(
itemKey = "OLD1", payload = list(list(title = "Reviewed book")),
hasFile = FALSE
)
Ledger <- list(items = list(`create-1` = list(status = "done", new = "NEW1")))
ztBlockMoveTrash(Records, createLedger = Ledger, fromLibrary = "user")
Census a library through the API
Description
Returns top-level and Trash counts for one library, plus per-collection counts when collection keys are supplied.
Usage
ztCensus(config, library = "user", collectionKeys = NULL)
Arguments
config |
A |
library |
|
collectionKeys |
Optional named character vector: name -> key. |
Details
Each scope is counted from the API Total-Results response header while
requesting only one record. Supplying named collection keys adds one row per
requested collection without downloading the library contents.
Value
A data.table with scope and total.
See Also
zotGet(), zotLocalItems(), and zotVerify().
Examples
Performer <- function(config, method, path, query, body, version) {
Total <- if (grepl("/trash", path)) "7" else "100"
list(status = 200L, headers = list(`total-results` = Total), body = list())
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
ztCensus(Config)
Check a create payload against its item type template
Description
Verifies what the campaigns checked by hand before creating records: every
field exists for the type, text fields are single strings, array and
object fields have the template's shape, each creator has an accepted type
and either a single name or both firstName and lastName, and no text
still carries an acquisition-site label. Problems are reported together in
one zotPlanError.
Usage
ztCheckPayload(
config,
payload,
template = ztTemplate(config, payload$itemType)
)
Arguments
config |
A |
payload |
A create payload (named list) with an |
template |
The item type's |
Value
Invisibly, a list with itemType, fields and creators
(their count) when the payload passes.
See Also
ztTemplate(), ztCleanPayload(), zotPreview().
Examples
Template <- list(
itemType = "book", fields = c("itemType", "title", "creators", "tags"),
creatorTypes = "author",
template = list(itemType = "book", title = "", creators = list(), tags = list())
)
Payload <- list(itemType = "book", title = "A book",
creators = list(list(creatorType = "author", name = "Team")))
ztCheckPayload(config = NULL, payload = Payload, template = Template)
Normalize an incoming filename
Description
Removes source labels recognized by the package and copy markers such as
(1) before the extension. The extension and other parenthetical text are
retained.
Usage
ztCleanName(name)
Arguments
name |
A filename. |
Value
The cleaned filename.
See Also
ztCleanTitle() and ztCleanPayload().
Examples
ztCleanName("Dynamics (1).pdf")
ztCleanName("Course Notes (SEG Course).pdf")
Sanitize every text field of an incoming item payload
Description
Applies the package's text-normalization rules across an item payload. Titles lose trailing extensions and copy markers, filenames retain their extensions, and creator names and tags are included. Non-text values are returned unchanged.
Usage
ztCleanPayload(payload)
Arguments
payload |
An item payload (named list of fields). |
Value
The payload with every text field cleaned.
See Also
ztCleanName(), ztCleanTitle(), and zotPreview().
Examples
Payload <- list(
itemType = "book",
title = "Structural Analysis (1).pdf",
creators = list(list(creatorType = "author", firstName = "A.",
lastName = "Author")),
filename = "Structural Analysis (1).pdf"
)
ztCleanPayload(Payload)
Normalize an incoming title
Description
Like ztCleanName(), and additionally strips a trailing file extension
and trailing copy markers — a title is not a filename.
Usage
ztCleanTitle(title)
Arguments
title |
An item title. |
Value
The cleaned title.
See Also
ztCleanName(), ztCleanPayload(), and ztRetitle().
Examples
ztCleanTitle("Dynamics of Structures (1).pdf")
ztCleanTitle("Optimization (2nd Edition)")
Ensure a collection tree exists in a library
Description
Find or create each path (vectors of names, root first) and return the full path to key map. Creating a missing collection has an immediate API effect.
Usage
ztCollectionTree(config, library, paths)
Arguments
config |
A |
library |
|
paths |
A list of character vectors, e.g. |
Details
Existing collections are indexed by parent key and name. Missing nodes are created from the root downward and reused by later paths in the same call. Unlike the plan builders, this function has immediate external effects.
Value
Named character vector: "A/B" -> collection key.
See Also
ztRefile() and zotPreview().
Examples
Counter <- new.env(parent = emptyenv())
Counter$n <- 0L
Performer <- function(config, method, path, query, body, version) {
if (method == "GET") {
return(list(
status = 200L, headers = list(`total-results` = "0"), body = list()
))
}
Counter$n <- Counter$n + 1L
Key <- paste0("COLL", Counter$n)
list(
status = 200L, headers = list(),
body = list(successful = list(`0` = list(key = Key)))
)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
ztCollectionTree(Config, library = "user", paths = list(c("Projects", "2026")))
Map a Crossref record to a Zotero item payload
Description
The returned payload is cleaned with ztCleanPayload() and can be placed in
a create plan. The default fetcher contacts the public Crossref API; inject a
compatible function for reproducible or offline work.
Usage
ztCrossref(doi, getJson = .zotCrossrefGet)
Arguments
doi |
The DOI to resolve. |
getJson |
Injectable fetcher returning the parsed Crossref message for a DOI; the default performs the real request. |
Value
An item payload for a create row, or a classed error when the DOI does not resolve.
See Also
ztCleanPayload() and zotPreview().
Examples
Message <- function(doi) list(
title = list("A reproducible record"),
author = list(list(given = "Ada", family = "Lovelace")),
`container-title` = list("Example Journal"),
issued = list(`date-parts` = list(list(2026L)))
)
ztCrossref("10.1000/example", getJson = Message)
Match candidate records against a set of records by strong identity
Description
Nominates pairs whose normalized DOI, MD5 or title coincide and judges
each pair with zotSameItem(). The result reports every nominated pair
with its verdict, as ztTwins() does within one set, so a nominated title
collision that is not a match remains visible.
Usage
ztMatches(candidates, records)
Arguments
candidates, records |
Lists of records, or data.frames with one record
per row, in any shape |
Value
A data.table with index (position in candidates),
candidate (its key, or NA), itemKey, library, match and
basis.
See Also
zotIdentity(), zotSameItem(), ztTwins().
Examples
Candidates <- list(list(title = "Paper", year = "2026", author = "Lovelace"))
Records <- list(
list(key = "R1", library = list(type = "user"),
data = list(title = "Paper", date = "2026",
creators = list(list(creatorType = "author",
lastName = "Lovelace")))),
list(key = "R2", data = list(title = "Paper", date = "1999"))
)
ztMatches(Candidates, Records)
Build a refile plan that adds items to a collection
Description
Fetches each item's current memberships at plan time and produces patch
rows whose collections payload is the union with addTo — the Zotero
API replaces the whole membership list, so the merge is mandatory.
Usage
ztRefile(config, itemKeys, library, addTo)
Arguments
config |
A |
itemKeys |
Character vector of item keys. |
library |
|
addTo |
Collection key to add. |
Details
Planning performs one current API read per key. It does not write, and it omits missing items and items already in the destination collection. Memberships can change after planning; rebuild or review the plan before execution when concurrent changes are possible.
Value
A guarded-flow plan (items already members are omitted).
See Also
ztCollectionTree(), zotPreview(), and zotExecute().
Examples
Memberships <- list(K1 = list("OLD"), K2 = list("TARGET"))
Performer <- function(config, method, path, query, body, version) {
Key <- sub(".*/items/", "", path)
list(
status = 200L, headers = list(),
body = list(version = 1L, data = list(collections = Memberships[[Key]]))
)
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
ztRefile(Config, itemKeys = c("K1", "K2"), library = "user",
addTo = "TARGET")
Build a retitle plan from a deterministic transformation
Description
Pure plan builder: applies transform to every title and returns patch
rows only where the result differs. Feed the result to zotPreview().
Usage
ztRetitle(items, transform)
Arguments
items |
A data.table with |
transform |
A function from one title to its cleaned form. |
Value
A guarded-flow plan (possibly empty).
See Also
ztCleanTitle() and zotPreview().
Examples
Items <- data.table::data.table(
itemKey = c("A", "B"), library = "user",
title = c("Reviewed book (1).pdf", "Existing title")
)
ztRetitle(Items, transform = ztCleanTitle)
Fetch the field template of an item type
Description
Reads the empty item the API returns for a type and the creator types it accepts. The field names are the complete set a create payload may carry; the template values show each field's shape (text, array or object).
Usage
ztTemplate(config, itemType)
Arguments
config |
A |
itemType |
An API item type such as |
Value
A list: itemType, fields, creatorTypes and template.
See Also
ztCheckPayload(), ztCrossref(), zotPreview().
Examples
Performer <- function(config, method, path, query, body, version) {
if (path == "/items/new") {
return(list(status = 200L, headers = list(),
body = list(itemType = "book", title = "", creators = list(),
tags = list(), collections = list())))
}
list(status = 200L, headers = list(),
body = list(list(creatorType = "author", localized = "Author")))
}
Config <- structure(
list(userID = "42", key = "example", apiBase = "https://example.invalid",
performer = Performer),
class = "zotConfig"
)
ztTemplate(Config, itemType = "book")$fields
Detect duplicate candidates with evidence classes
Description
Groups records by normalized title to nominate candidates, then judges
every pair with zotSameItem() — the strong-identity rule decides, never
the title alone.
Usage
ztTwins(records)
Arguments
records |
A data.table with |
Value
A data.table of pairs: keyA, keyB, match, basis.
See Also
zotSameItem() and zotPreview().
Examples
Records <- data.table::data.table(
itemKey = c("A", "B"), title = c("Paper", "Paper"),
year = c(2026L, 2026L), author = c("Lovelace", "Lovelace"),
doi = NA_character_, md5 = NA_character_, hasFile = FALSE
)
ztTwins(Records)