Skip to content

Reference

Everything the app knows of, to look up: the settings, the keys in which Nextcloud keeps them, the routes, the versions that the tests cover, where data goes, and what stays when the app goes.

Settings

The settings are under Administration settings → Paperless Unified Search, for administrators only, and hold for every user of the Nextcloud. Test connection and save stores them only once Paperless answers; it and Disconnect ask for the password when it wasn't confirmed in the last 30 minutes.

Setting What it does Default App config key
Paperless URL The address of Paperless, with a path when Paperless runs under one, such as https://paperless.example.com — paperless_url
Paperless runs in my local network Lets Nextcloud reach this URL, and no other, at an address of the local network, without redirects (details) Off allow_local
Paperless API token The token of the Paperless account of the search; blank keeps the stored one, for the same server — None: in Nextcloud's credentials manager, as paperless_unified_search.api-token
Archive account The Nextcloud account whose files stand for the documents; blank takes the account of Paperless Sync The account of Paperless Sync archive_owner
Always include Paperless in global search Asks Paperless in every search of every user, without Search connected services; reload Nextcloud after a change Off always_search

Disconnect deletes the URL, the token, the switches, the archive account, the version of Paperless and the history of the problems. Clear history deletes only the history.

App configuration

Nextcloud keeps the settings in its app configuration, which occ reads:

occ config:list paperless_unified_search
occ config:app:get paperless_unified_search paperless_url
Key Holds
paperless_url The URL of Paperless, as the settings stored it: without a slash at its end, with the host in its ASCII form
allow_local Whether Paperless runs in the local network
archive_owner The ID of the archive account, as Nextcloud spells it; missing when the settings take the account of Paperless Sync
always_search Whether every search asks Paperless
paperless_version, paperless_api_version The versions of Paperless and of its API, such as 3.3.0 and 10, as its last answer named them
diagnostics The history of the problems of the search, as JSON; a lazy key, which Nextcloud loads only when it is read
diagnostics_pending Whether a failure in the history still waits for a search that works

The account of Paperless Sync is occ config:app:get paperless_sync target_user.

Change the settings in the administration settings, which test the connection first. occ config:app:set can still turn a switch on or off, such as occ config:app:set paperless_unified_search always_search --value=true --type=boolean, which every user's next reload takes. Don't change paperless_url that way: the stored token would follow the URL to the new server, which the settings prevent by asking for the token again.

Set up and check it with occ

Four commands of occ do without a browser what the settings do, and more: they set the app up for Ansible, Docker and other scripts, test it for monitoring, and say why a user doesn't find a document.

Command What it does Exit code
paperless_unified_search:setup Tests the connection with the URL and the token, then saves the settings, as Test connection and save does 0 once saved, 1 when Paperless doesn't answer or a setting is wrong, 2 for a wrong call, such as a missing --url or an empty token source
paperless_unified_search:test Tests the connection with the stored settings and names the version of Paperless 0 when Paperless answers, 1 otherwise
paperless_unified_search:search <user> <term> Searches as the user and says for each document whether it shows to them, with --explain why not 0 once Paperless answered
paperless_unified_search:check Tests the connection and the archive account, and counts the documents of Paperless that have a file 0 when the search can show documents, 1 otherwise

Set up the app

The token never goes on the command line, where the history of the shell and the list of processes would show it to others: setup reads it from the standard input with --token-stdin, or from an environment variable with --token-env=NAME. An option that you leave out keeps its stored value, and without a token the stored one stays, but only for the server it was saved for, as on the settings page.

Option Sets
--url=URL The address of Paperless, such as https://paperless.example.com; required
--token-stdin, --token-env=NAME The API token, from the first line of the standard input or from the environment variable NAME
--archive-account=ID The account that owns the synchronized files; empty for the account of Paperless Sync
--allow-local, --no-allow-local Paperless runs in my local network
--always-search, --no-always-search Always include Paperless in global search

On a server, as the user of the web server:

printf '%s\n' "$PAPERLESS_TOKEN" | sudo -u www-data php occ paperless_unified_search:setup \
  --url=https://paperless.example.com --archive-account=paperless --token-stdin

With Docker Compose and the image of Nextcloud, from a file that only you can read:

docker compose exec -T --user www-data nextcloud php occ paperless_unified_search:setup \
  --url=http://paperless:8000 --archive-account=paperless --allow-local --token-stdin \
  < paperless-token.txt

With Ansible, from its vault, and without the token in its output:

- name: Connect Paperless Unified Search to Paperless-ngx
  ansible.builtin.command:
    argv: [php, occ, paperless_unified_search:setup, --url=https://paperless.example.com,
           --archive-account=paperless, --token-stdin]
    chdir: /var/www/nextcloud
    stdin: "{{ vault_paperless_token }}"
  become: true
  become_user: www-data
  no_log: true

Explain a hidden document

The search leaves out every document that has no file of the archive account that the searching user can open, without a trace: twelve documents that jamie may not open look exactly like none. search lists the documents that Paperless ranks first, 10 by default and up to 50 with --limit, with whether each shows to the user, and --explain names the reason of each one that doesn't:

$ occ paperless_unified_search:search jamie invoice --limit=5 --explain
+-----+-----------------------------------+------------+--------------------------------------------------------------------------------+
| ID  | Title                             | Verdict    | File                                                                           |
+-----+-----------------------------------+------------+--------------------------------------------------------------------------------+
| 412 | Electricity invoice - August 2026 | shown      | /Paperless/2026/Electricity invoice - August 2026 [P412].pdf                   |
| 389 | Home insurance renewal            | not shared | /Private/Home insurance renewal [P389].pdf of paperless, not shared with jamie |
| 371 | Internet invoice - July 2026      | shown      | /Paperless/2026/Internet invoice - July 2026 [P371].pdf                        |
| 356 | Water bill 2026                   | no file    |                                                                                |
| 344 | Old invoice                       | not owned  | /Scans/Old invoice [P344].pdf of jamie                                         |
+-----+-----------------------------------+------------+--------------------------------------------------------------------------------+
Paperless found 12 document(s) for "invoice"; 2 of the 5 above show to jamie.
Not shared (389): the archive account paperless has the file, but hasn't shared it with jamie. Share the file, or a folder that holds it, read-only with jamie.
Not owned (344): jamie sees a file with the marker of the document, but only the files of the archive account paperless stand for documents.
No file (356): no file of the archive account paperless has the marker of the document, such as [P356], in its name. The synchronization hasn't written it yet, or names files without it.
---
config:
  flowchart:
    wrappingWidth: 400
    nodeSpacing: 30
---
flowchart TB
    accTitle: Why a document does not show
    accDescr: Paperless finds document 389 when jamie searches, and occ paperless_unified_search:search jamie invoice --explain asks three questions in turn. Does jamie see a file of the archive account with [P389] in its name? Then the document shows. Otherwise, does the archive account have such a file in its own folders? Then it is not shared with jamie, and sharing the file or its folder read-only with jamie shows it. Otherwise, does jamie see a file with the marker of another account? Then it is not owned, since only files of the archive account stand for documents. Otherwise there is no file, and the synchronization has yet to write one with the marker in its name.

    hit["Paperless finds document 389<br>when jamie searches"] --> visible{"Does jamie see a file of<br>the archive account<br>with [P389] in its name?"}
    visible -- "yes" --> shown["shown"]
    visible -- "no" --> archived{"Does the archive account<br>have such a file in its<br>own folders?"}
    archived -- "yes" --> unshared["not shared: share the<br>file or its folder<br>read-only with jamie"]
    archived -- "no" --> other{"Does jamie see a file<br>with [P389] of another<br>account?"}
    other -- "yes" --> unowned["not owned: only files<br>of the archive account<br>stand for documents"]
    other -- "no" --> missing["no file: let the<br>synchronization write<br>one with the marker"]

    classDef yes fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
    classDef no fill:#dc26262e,stroke:#dc2626,stroke-width:2px
    class shown yes
    class unshared,unowned,missing no

Without --explain, search decides as the search itself does, with one search of the file cache, and calls every document that doesn't show hidden; with it, a search of the file cache follows for each further question, for the documents that are still unexplained. --json writes the documents for scripts, each with its id, title, verdict, path and owner.

Check the whole set-up

check tests the connection and the archive account, and compares the documents that the account of the API token sees in Paperless with the files of the archive account whose names carry their markers:

$ occ paperless_unified_search:check
✓ Paperless 3.3.0 (API 10) answers at https://paperless.example.com.
✓ The archive account is paperless, the account of Paperless Sync.
! Coverage: 1,180 of 1,234 Paperless document(s) have a file of the archive account. Without one, among others: 17, 18, 19, 20, 23, 31, 40, 41, 42, 57.

Documents without a file are no failure, as long as the search can show others: the synchronization may not have written them yet, or leaves some out on purpose. When not one document has a file, as with an archive account that owns none of the synchronized files, check exits with 1 and says what to correct. search --explain with a term of such a document says why it doesn't show.

In the administration overview

Administration settings → Overview checks the app among the other setup checks of Nextcloud, and so does occ setupchecks. It warns when the search shows no Paperless documents for a reason that an administrator corrects, and links to the settings of the app: a token that Nextcloud can no longer decrypt, no archive account or one that doesn't exist, or a search that failed with none working since. Otherwise it names the Paperless that the app is connected to, with the version of its last answer. It reads only what the app stored and sends no request to Paperless, so the overview opens as fast as without it.

The routes of the settings

The settings page saves through four routes of the app, which only administrators may call, each with Nextcloud's CSRF token. A script calls them with the login of an administrator and the header OCS-APIRequest: true, as the end-to-end tests do; /index.php goes before /apps where Nextcloud runs without pretty URLs.

Route Does
POST /apps/paperless_unified_search/settings Tests the connection and saves, with the parameters url, token, archiveOwner, allowLocal and alwaysSearch; asks for the password like Test connection and save
DELETE /apps/paperless_unified_search/settings Disconnect: deletes the settings, the token and the history; asks for the password
DELETE /apps/paperless_unified_search/settings/diagnostics Clear history
POST /apps/paperless_unified_search/settings/try Try a search: searches with the parameter term as the account user and says for each document whether it shows and why not, with its ID and title alone; the log records which administrator tried a search as whom, without the term
curl -u admin -H 'OCS-APIRequest: true' -H 'Accept: application/json' \
  --data-urlencode 'url=https://paperless.example.com' \
  --data-urlencode 'archiveOwner=paperless' \
  --data-urlencode "token@$HOME/.paperless-token" \
  'https://cloud.example.com/apps/paperless_unified_search/settings'

token@file hands the token over in a file, which keeps it out of the history of the shell and the list of processes. A saved setting answers HTTP 200 with the settings as JSON: url, tokenConfigured, alwaysSearch, archiveOwner, syncAccount, tokenUnreadable, archiveOwnerMissing, allowLocal, paperlessVersion and paperlessApiVersion, never the token. A refusal answers HTTP 400, or 500 when something after the test failed, with the message to show in the language of the administrator and a code:

Code Means
invalid_url, unsupported_scheme, url_extras, host_not_ascii The URL isn't one, isn't HTTP or HTTPS, has credentials, a query or a fragment, or a host that isn't in its ASCII form
token_required, token_for_other_server No token is stored, or the stored one is for another server
unknown_archive_account The archive account doesn't exist
unauthorized Paperless refused the token, with HTTP 401 or 403
not_found HTTP 404: the path of the URL is wrong
redirect The URL forwards, and with the local network Nextcloud follows no redirect
http Another status, such as an error of Paperless with 5xx
unsupported_version HTTP 406: Paperless is older than 2.16 and doesn't speak version 9 of its API; updating Paperless-ngx fixes it
not_paperless The answer isn't a list of documents of Paperless-ngx
local_address Nextcloud refused the address of the local network
tls, tls_handshake Nextcloud doesn't trust the certificate, or couldn't set up an encrypted connection
unknown_host, timeout, unreachable The host doesn't resolve, Paperless didn't answer in time, or the connection failed
internal Something after the test failed, such as the database; the log of Nextcloud has it

Troubleshooting says what to do about each.

The search and the smart picker

ID Where
The search provider paperless_unified_search_documents Paperless documents in Nextcloud's search, and GET /ocs/v2.php/search/providers/paperless_unified_search_documents/search?term=… in its OCS API, with limit (5 by default, up to 50) and cursor
The provider of the smart picker paperless_unified_search-documents Paperless documents in the smart picker of Talk, Text, Deck and Mail

Use it from scripts and AI assistants shows the answer of the API.

Requests to Paperless

Every request of the app to Paperless is one GET of <URL>/api/documents/, from the Nextcloud server:

Headers Authorization: Token …, Accept: application/json and User-Agent: Nextcloud-Paperless-Unified-Search/<version>
A search query with the term, page, page_size=25 and fields=id,title,created,correspondent,document_type,__search_hit__; on Paperless 3, a query that it can't parse yet once more with text in place of query
The test of the connection page_size=1 and fields=id
Time 3 seconds to connect, 10 seconds for the answer
A second try Only for a request that failed within 3.5 seconds, before it had a connection, and whose address Nextcloud didn't refuse
Redirects Without Paperless runs in my local network, up to 5, never to an address of the local network; Nextcloud leaves the token out of a redirect to another scheme, host or port, which Paperless then refuses, and keeps it for one within the same server. With the switch, none

Compatibility

Nextcloud 33 Nextcloud 34 Nextcloud 35
Paperless-ngx 3.x (API 9) End-to-end tests End-to-end tests End-to-end tests
Paperless-ngx 2.16 to 2.20 (API 9) Unit tests Unit tests Unit tests
  • Nextcloud: the end-to-end tests run the app in the current release of each version that appinfo/info.xml supports, in every pull request that changes the app and on every change of main, and every Monday also in the coming version of Nextcloud, its beta or release candidate.
  • Paperless-ngx: the end-to-end tests ask a stand-in of Paperless that answers as Paperless-ngx 3.3.0 does: its pages, its fields, its headers of the version, its excerpts and its refusal of a query that is still being typed. The unit tests check the answers of 2.20 and 3.3, such as that only Paperless 3 gets the simple mode of its search, which Paperless 2 doesn't know. Every request names version 9 of the API, which Paperless-ngx 2.16 and newer and every 3.x speak, so a new release of Paperless can't change the results unnoticed; contract tests check the app every week against real Paperless-ngx, the last release of 2.20 and the newest of 3.x. An older Paperless answers HTTP 406, and the test of the connection says so.
  • PHP: 8.2 or newer, as Nextcloud supports it; the unit tests run on 8.2.

Data flows

From To What When
The browser or an app of the user Nextcloud The search term Whenever the user searches
Nextcloud Paperless The term, the page and the fields to answer with, and the API token With Search connected services on, after choosing Paperless documents in the smart picker, through the API, or always when Paperless is trusted
Paperless Nextcloud Of each document that the account of the token may view: its ID, title, date, the IDs of its correspondent and its type, and the excerpt around the term; the versions of Paperless and of its API In every answer
Nextcloud The browser or an app of the user Of each document whose file the user can open: its title, date and excerpt, and the link, the ID and the path of its file In every answer
---
config:
  sequence:
    wrap: true
    width: 120
    actorMargin: 10
---
sequenceDiagram
    accTitle: The data flow of a search
    accDescr: A user searches for invoice. The unified search passes the term, the cursor, the limit, the date filter and the user to the search provider. Without a term, an archive account or a connection, the provider answers with no results and doesn't ask Paperless. Otherwise, until the page of results is full, the Paperless client asks Paperless-ngx with the token for the page of 25 hits that holds the next hit, within the days of the date filter, and only for the fields that a result shows, and Paperless answers with the ID, the title, the date, the IDs of the correspondent and the type and the excerpt of documents 412, 389, 371 and more, and whether a next page exists. One call of the file locator covers all documents of that page and returns for each a file of the archive account with its marker, or none. For the documents with a file, the names ask Paperless, through the client, for the correspondents and the types that the cache doesn't hold, with one request per kind, and keep their names for ten minutes. This repeats for at most 4 pages of Paperless or 3 seconds. The provider answers with an entry for every document with a file, up to the limit, with the preview of the file or the icon of its type, the title, the date in the language of the user, the correspondent, the type and the excerpt, and the cursor of the first hit that it didn't go through.

    actor User
    participant Search as Unified search
    participant Provider as Search provider
    participant Client as Paperless client
    participant Paperless as Paperless-ngx
    participant Locator as File locator
    participant Names as Names
    User->>Search: invoice
    Search->>Provider: term, cursor, limit,<br>date filter and user
    alt no term, no archive account or no connection
        Provider-->>Search: no results, without asking Paperless
    else
        loop until the page of results is full, for at most 4 pages of Paperless or 3 seconds
            Provider->>Client: term, the days of the date filter,<br>the page of 25 hits with the next hit
            Client->>Paperless: GET /api/documents/<br>?query=invoice&page=1<br>&page_size=25&fields=…<br>&created__date__gte=…,<br>with the token
            Paperless-->>Client: ID, title, date, correspondent,<br>type and excerpt of documents<br>412, 389, 371, …, and whether<br>a next page exists
            Provider->>Locator: the documents of the page,<br>user, archive account
            Locator-->>Provider: per document a file of the<br>archive account with its marker,<br>or none
            Provider->>Names: the correspondents and types<br>of the documents with a file
            Names->>Paperless: through the client, for the IDs<br>that the cache lacks: GET<br>/api/correspondents/?id__in=3,5<br>and /api/document_types/?id__in=1,2
            Paperless-->>Names: their names, which the cache<br>keeps for ten minutes
            Names-->>Provider: the names
        end
        Provider-->>Search: an entry for every document with a file, up to the limit: preview or icon, title, date, correspondent, type and excerpt, and the cursor of the next hit
    end

Nextcloud stores the settings in its app configuration, the token encrypted in its credentials manager, and the history of the latest 20 problems, without the term, the token and the query of any URL. Nothing of the documents is stored. Architecture explains every step, and the security design what protects each of them.

Security and privacy

  • The Paperless API token is stored only in Nextcloud's server-side credentials manager.
  • The token is never returned to browser JavaScript or rendered into HTML.
  • The token is sent only to the Paperless server it was saved for; a URL with another scheme, host or port needs it entered again.
  • Saving the settings and disconnecting ask for the administrator's password when it wasn't confirmed in the last 30 minutes.
  • occ takes the token from its standard input or an environment variable, never from the command line, where the history of the shell and the list of processes would show it.
  • Nextcloud's protection against addresses of the local network stays on for every app. Paperless runs in my local network lifts it only for the requests of this app to the Paperless URL of the settings, which follow no redirect.
  • Search results are filtered through the current user's Nextcloud filesystem view.
  • The settings show administrators the history of the problems of the search without the API token, the search term or the query of any URL.
  • Try a search shows administrators which documents a user finds for a term, by their IDs and titles alone, without their content, an excerpt or their files; the audit log of the app Auditing / Logging (admin_audit) records which administrator tried a search as which user, whatever the log level, and so does the log of Nextcloud at level info, without the term.
  • Search terms are sent server-to-server only when connected-services search or trusted-service mode is enabled, after the user chose Paperless documents in the smart picker, or when a client asks this search by its ID through the API.
  • A link that the smart picker inserts is the link of the file in Nextcloud, which shows its preview only to readers who can open the file.
  • Administrators can explicitly trust the configured Paperless server to include it automatically in every user's global searches.
  • No deployment credentials, private hostnames, internal addresses, or instance configuration belong in this repository.
  • Gitleaks scans every push and pull request.
  • Dependency Review blocks newly introduced vulnerable or unapproved dependencies.
  • Psalm analyzes the PHP code and CodeQL scans the JavaScript on every pull request.
  • Every release includes an SPDX SBOM, a detached signature, and public Sigstore build provenance.
  • OpenSSF Scorecard audits the repository's supply-chain security every week.

See SECURITY.md for reporting security issues.

The log of Nextcloud

The app writes to the log of Nextcloud with app set to paperless_unified_search, never with the term or the token:

Level Message Means
Warning The test of the connection to Paperless failed: reason (error) Test connection and save failed; the reason is the code of the routes above
Error The settings of Paperless Unified Search could not be saved (error) Something after the test failed, such as the database
Warning Paperless unified search failed (error) A search failed; Search problems has it too
Warning Paperless unified search left out n result(s) it could not show (error) Results whose files failed to answer were left out
Warning Paperless unified search skipped n file(s) it could not read (error) Files of the archive that Nextcloud couldn't read
Warning Paperless unified search looks for the files of each document on its own … A version of Nextcloud whose query of the file cache the app doesn't know yet: the search still works, with one query for each document instead of one for each page
Warning The stored Paperless API token can't be decrypted … The secret of config.php changed: enter the token again
Debug Paperless rejected the query of a search with HTTP status A query still being typed, or a page past the last one: no problem

Uninstall

  1. Disconnect in the settings. It deletes the URL, the token, the switches, the archive account, the version of Paperless and the history of the problems; only diagnostics_pending stays, with the keys that Nextcloud keeps of every app, such as enabled and installed_version.
  2. Remove the app: occ app:remove paperless_unified_search disables it and deletes its code; or Apps → Disable and Remove in the browser. Nextcloud keeps the app configuration of a removed app: occ config:list paperless_unified_search lists what is left, and occ config:app:delete paperless_unified_search <key> deletes a key.
  3. In Paperless, delete the API token, or the account of the search: a token that Nextcloud forgot still works until Paperless forgets it too.

The files of the archive and their shares stay as they are: they belong to the archive account and to the synchronization, not to this app. Without Disconnect, the encrypted token stays in the credentials manager of Nextcloud, under paperless_unified_search.api-token, until the app is installed again and disconnected.