Let an agent organize your Litlas library

Create and remove boards, manage papers on a board, delete papers from the library, and read or update paper notes through an account-scoped API.

Quick start

All endpoints use JSON and are rooted at the following base path.

API base path/api/v1/agent

In an interactive Bash shell, keep the one-time key in a non-exported variable and pass it to curl only through a private configuration file descriptor. This prevents the key from entering shell history, child-process environments, or curl arguments:

Bash — set up the key
unset LITLAS_API_KEYbuiltin printf 'Litlas API key: ' >&2IFS= read -r -s LITLAS_API_KEYbuiltin printf '\n' >&2litlas_curl() {  if [[ ! ${LITLAS_API_KEY-} =~ ^litlas_sk_[A-Za-z0-9_-]{43}$ ]]; then    builtin printf 'Invalid Litlas API key.\n' >&2    return 64  fi  command curl --config <(    builtin printf 'header = "Authorization: Bearer %s"\n' "$LITLAS_API_KEY"  ) "$@"}

Authentication and API keys

Send your Litlas API key in the Authorization header on every request.

Request header
Authorization: Bearer <YOUR_LITLAS_API_KEY>

The complete key is shown only once, immediately after it is issued or rotated. Litlas stores only what it needs to verify the key and cannot show the complete value again.

Rotating the key invalidates the previous key immediately. Update the agent or secret store before its next request; requests using the old key return 401.

Treat the key like a password. Do not put it in a URL, source code, chat transcript, log, shell history, or repository. Never type the complete value directly into a shell command. After pasting it into the intended secret store, overwrite your clipboard and hide the key in Settings.

When finished, clear the key and helper function from the current shell:

Bash — clear the key
unset LITLAS_API_KEYunset -f litlas_curl

Identifiers and note behavior

board_id

An opaque board identifier returned by the board list and create endpoints. Copy it exactly; do not derive it from the board name.

item_count

The total number of live items attached to the board: papers plus documents. It can differ from papers.length returned by the board papers endpoint, which lists papers only.

paper_id

An opaque Litlas paper identifier returned with a paper. Copy it exactly instead of parsing it or deriving it from a title, DOI, or board position. The API removes surrounding whitespace but preserves every internal character: repeated spaces remain distinct, and a paper_id can contain /, ?, #, or % characters.

URL-encode paper_id in paths

Encode the complete identifier as one path segment. Do not split it at slashes and do not encode the entire URL.

JavaScript
const paperId = 'SOURCE/ID';encodeURIComponent(paperId); // 'SOURCE%2FID'

A note belongs to the paper

A paper has one account-level note shared across every board. Updating it through any board context changes the same note everywhere; removing a paper from a board does not delete that note. A PATCH appends to the existing note by default (mode "append"), because an agent only ever holds the note as it read it and replacing by default would drop whatever was written in between. Send mode "replace" to overwrite the whole note, which is also the only way to clear it. GET and PATCH require the paper to be an active saved item in the account library; otherwise they return 404.

Deleting is separate from removing from a board

Removing a paper from a board only detaches it; the paper and its note stay in the library. Deleting the paper from the library is the separate DELETE on the paper itself. By default it moves the paper to the library trash, where it stays restorable from the Library trash screen until its retention period expires, and its board memberships are kept so a restore puts it back where it was. Adding permanent=true deletes the paper, its note, and its saved snapshot outright, whether or not it is in the trash. That cannot be undone from the app or the API.

Input limits

  • Board name: 1–120 characters, counted before whitespace is normalized. Litlas then trims surrounding whitespace, collapses internal whitespace, and rejects a case-insensitive duplicate name in the same account with 400.
  • paper_id: 1–120 characters after surrounding whitespace is removed. Internal whitespace is preserved, not collapsed; do not case-fold or otherwise normalize this opaque identifier.
  • note: at most 2000 characters. In mode "replace" an empty string clears the note; in mode "append" a blank note is rejected with 422 instead of silently doing nothing. When appending makes the stored note exceed the limit the request fails with 422 note_too_long and nothing is written; the note is never truncated.
  • expected_updated_at: 1–64 characters and cannot be blank.
  • mode: one of append, replace. Defaults to append.
  • permanent: a boolean query parameter on the library delete. Defaults to false, which moves the paper to the trash.
  • Unknown fields in a JSON request body are rejected with 422.
  • Unknown query parameters are rejected with 422, so a misspelled parameter cannot silently run with its default.

Update notes without overwriting another agent

expected_updated_at is required. Use optimistic concurrency for every note edit:

  1. GET the note and retain its updated_at value.
  2. PATCH with that exact value as expected_updated_at. Send only the new text to append it, or the complete new note with mode "replace" to overwrite.
  3. On 409 note_conflict, do not overwrite or silently merge. Read current_note and current_updated_at from the conflict, reconcile explicitly, then send a new PATCH.
  4. Never reuse a version that already succeeded: because a version is consumed once, a blindly repeated append cannot double-write.
Stale note conflict (409)
{  "detail": {    "code": "note_conflict",    "paper_id": "P43NTWMNFA70",    "current_note": "The note saved by another client.",    "current_updated_at": "2026-09-03T12:34:00Z"  }}

Endpoints

Commands with a JSON body pass the displayed example to curl on standard input instead of an argument. If the real body contains sensitive content, do not replace the example inside an interactive shell command or history; have the agent generate the JSON on standard input without logging it.

Upload a PDF

POST/api/v1/agent/library/upload-pdf

Send multipart/form-data with exactly one of pdf (up to 50 MiB) or source_url (a public PDF URL), and optional board_ids (a JSON array). URL downloads validate every redirect and fail if the result is not a PDF. Metadata is extracted from the PDF. Papers outside the catalog are stored privately in your library with their PDF. Existing board memberships and notes are preserved. The response example shows the principal fields; paper and item include the standard library metadata.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request POST \  --form 'pdf=@paper.pdf;type=application/pdf' \  --form-string 'board_ids=["board_01JABCDEF"]' \  'https://litlas.ai/api/v1/agent/library/upload-pdf'
Success response (HTTP 200)
{  "ok": true,  "created": true,  "pdf_uploaded": true,  "paper": {    "id": "browser_import:example",    "title": "Example paper title"  },  "item": {    "paper_id": "browser_import:example",    "board_ids": [      "board_01JABCDEF"    ]  }}
Retry and idempotency

Check pdf_uploaded and the returned paper.id. Re-uploading the same identified paper updates its PDF; it does not create another library item. After a timeout, check the destination board before retrying. Invalid PDF returns 400, oversized file 413, and unavailable storage 503.

Upload a BibTeX file

POST/api/v1/agent/library/import-bibtex

Send multipart/form-data with bib (UTF-8, at most 500,000 characters) and optional board_ids (a JSON array). The shared library importer saves entries missing from the catalog as private records. It does not download PDFs. Check imported_count, unmatched_count and note_conflict_count; imported_paper_ids contains at most 20 IDs. Without board_ids, board declarations in the file replace memberships, as in the library import UI. The response example shows the principal fields.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request POST \  --form 'bib=@papers.bib;type=application/x-bibtex' \  --form-string 'board_ids=["board_01JABCDEF"]' \  'https://litlas.ai/api/v1/agent/library/import-bibtex'
Success response (HTTP 200)
{  "total_entries": 1,  "imported_count": 1,  "matched_count": 0,  "captured_count": 1,  "unmatched_count": 0,  "note_conflict_count": 0,  "imported_paper_ids": [    "browser_import:example"  ],  "target_board_ids": [    "board_01JABCDEF"  ]}
Retry and idempotency

Existing papers are matched again and personal notes are preserved. Specifying board_ids adds those memberships. Invalid or empty BibTeX returns 400; oversized files return 413. Resolve reported unmatched entries and note conflicts explicitly.

List boards

GET/api/v1/agent/boards

Returns the boards owned by the API-key account. Litlas verifies stale owner-membership mirrors before calculating item_count. If that verification needs the paper catalog and it is unavailable, the whole request returns 502 paper_metadata_unavailable; if stored identities are ambiguous, it returns 409 library_identity_ambiguous. It never returns a partial board list or a possibly inaccurate count.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request GET \  'https://litlas.ai/api/v1/agent/boards'
Success response (HTTP 200)
{  "boards": [    {      "board_id": "board_01JABCDEF",      "name": "Reading list",      "item_count": 0,      "created_at": "2026-09-03T12:30:00Z",      "updated_at": "2026-09-03T12:30:00Z"    }  ]}
Retry and idempotency

Safe to repeat. It may perform the membership self-healing described above. On 502, retry only after the paper catalog has recovered. On an identity-related 409, follow the retryable contract below instead of guessing.

Create a board

POST/api/v1/agent/boards

Creates a board for the account and returns it under board.

JSON request body
{  "name": "Reading list"}
Example request
litlas_curl --fail-with-body --silent --show-error \  --request POST \  --header 'Content-Type: application/json' \  --data-binary @- \  'https://litlas.ai/api/v1/agent/boards' <<'JSON'{"name":"Reading list"}JSON
Success response (HTTP 201)
{  "board": {    "board_id": "board_01JABCDEF",    "name": "Reading list",    "item_count": 0,    "created_at": "2026-09-03T12:30:00Z",    "updated_at": "2026-09-03T12:30:00Z"  }}
Retry and idempotency

Not idempotent. Re-sending the same name returns 400 instead of creating a duplicate. After an uncertain result, list boards before deciding what to do.

Delete a board

DELETE/api/v1/agent/boards/{board_id}

Deletes the selected board and detaches its papers and documents. Saved papers and their notes remain in the account library; attached document records also remain, with only the board link removed.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request DELETE \  'https://litlas.ai/api/v1/agent/boards/board_01JABCDEF'
Success response (HTTP 200)
{  "board_id": "board_01JABCDEF",  "deleted": true}
Retry and idempotency

After a successful delete, repeating the request returns 404. After an uncertain result, list boards before retrying.

List papers on a board

GET/api/v1/agent/boards/{board_id}/papers

Returns the papers currently attached to the selected board. Before returning them, Litlas verifies stale owner-membership mirrors. If that verification needs an unavailable paper catalog, the whole request returns 502 paper_metadata_unavailable; if stored identities are ambiguous, it returns 409 library_identity_ambiguous. Metadata resolution for the returned papers can also produce 502. It never returns a 200 response with unverified membership or only the papers whose metadata resolved.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request GET \  'https://litlas.ai/api/v1/agent/boards/board_01JABCDEF/papers'
Success response (HTTP 200)
{  "board_id": "board_01JABCDEF",  "papers": [    {      "paper_id": "P43NTWMNFA70",      "title": "Example paper title",      "authors": [        "Ada Lovelace",        "Alan Turing"      ],      "year": 2026    }  ]}
Retry and idempotency

Safe to repeat. It may perform the membership self-healing described above. On 502, retry only after the paper catalog has recovered. On an identity-related 409, follow the retryable contract below instead of guessing.

Add a paper to a board

POST/api/v1/agent/boards/{board_id}/papers

Attaches the paper identified by paper_id to the selected board. If it is not already an active saved paper, this operation also saves it to the account library. A valid catalog lookup that finds no metadata returns 404 only when no snapshot owned by that account can resolve it; if the paper catalog cannot be reached or its response cannot be validated and no owned snapshot can resolve the metadata, the request returns 502 with detail.code paper_metadata_unavailable.

JSON request body
{  "paper_id": "P43NTWMNFA70"}
Example request
litlas_curl --fail-with-body --silent --show-error \  --request POST \  --header 'Content-Type: application/json' \  --data-binary @- \  'https://litlas.ai/api/v1/agent/boards/board_01JABCDEF/papers' <<'JSON'{"paper_id":"P43NTWMNFA70"}JSON
Success response (HTTP 200)
{  "board_id": "board_01JABCDEF",  "paper_id": "P43NTWMNFA70",  "added": true}
Retry and idempotency

Idempotent for one board and paper: a repeat succeeds with added set to false. On paper_metadata_unavailable, first GET the target board's papers. Retry the add at most once only if that read succeeds and confirms the paper is absent; if the read fails or cannot confirm absence, stop and surface the error. For an identity-related 409, follow the retryable contract below.

Remove a paper from a board

DELETE/api/v1/agent/boards/{board_id}/papers/{paper_id}

Removes only this board membership. It does not delete an existing saved paper from the account library or its account-level note. Litlas needs no catalog lookup when the supplied paper_id exactly matches a membership on this board, or when it exactly matches a saved paper and the board holds no other paper; an exact saved paper alone is not enough. Otherwise Litlas resolves the identifier through the paper catalog first, and returns 502 with detail.code paper_metadata_unavailable before applying a membership change when the catalog cannot be reached or its response cannot be validated and no snapshot owned by that account can resolve it. A valid catalog lookup that finds no metadata, or an identifier that names no membership on this board, is not an error: the request succeeds with removed set to false.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request DELETE \  'https://litlas.ai/api/v1/agent/boards/board_01JABCDEF/papers/P43NTWMNFA70'
Success response (HTTP 200)
{  "board_id": "board_01JABCDEF",  "paper_id": "P43NTWMNFA70",  "removed": true}
Retry and idempotency

Idempotent for one board and paper: if the membership is already absent, removed is false. On paper_metadata_unavailable, do not loop blindly: first GET the target board's papers. Retry the same DELETE at most once only if that read succeeds and contains the exact opaque paper_id; if the read fails, the exact id is absent, or the retry fails, stop and surface the error instead of guessing an alias. For an identity-related 409, follow the retryable contract below.

Read a paper note

GET/api/v1/agent/papers/{paper_id}/note

Returns the account-level note and the updated_at version required by PATCH. An exact local paper identity needs no catalog lookup. When only a possible alias can identify the saved paper, an unavailable catalog returns 502 paper_metadata_unavailable and ambiguous stored identities return 409 library_identity_ambiguous; no partial 200 response is returned. A confirmed missing or inactive item returns 404.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request GET \  'https://litlas.ai/api/v1/agent/papers/P43NTWMNFA70/note'
Success response (HTTP 200)
{  "paper_id": "P43NTWMNFA70",  "note": "Initial note.",  "updated_at": "2026-09-03T12:31:00Z"}
Retry and idempotency

Read-only. On 502, retry only after the paper catalog has recovered. On an identity-related 409, follow the retryable contract below instead of guessing.

Update a paper note

PATCH/api/v1/agent/papers/{paper_id}/note

Writes the account-level note only if expected_updated_at still matches. The default mode "append" keeps the stored note and adds the submitted text after it, separated by a blank line; if the note is empty the submitted text becomes the note. Mode "replace" overwrites the whole note and an empty string clears it. Appending past the note length limit fails with 422 note_too_long and writes nothing. An exact local paper identity needs no catalog lookup. When only a possible alias can identify the saved paper, an unavailable catalog returns 502 paper_metadata_unavailable and ambiguous stored identities return 409 library_identity_ambiguous before the note is changed. A confirmed missing or inactive item returns 404.

JSON request body
{  "note": "Key finding and follow-up questions.",  "expected_updated_at": "2026-09-03T12:31:00Z",  "mode": "append"}
Example request
litlas_curl --fail-with-body --silent --show-error \  --request PATCH \  --header 'Content-Type: application/json' \  --data-binary @- \  'https://litlas.ai/api/v1/agent/papers/P43NTWMNFA70/note' <<'JSON'{"note":"Key finding and follow-up questions.","expected_updated_at":"2026-09-03T12:31:00Z","mode":"append"}JSON
Success response (HTTP 200)
{  "paper_id": "P43NTWMNFA70",  "note": "Initial note.\n\nKey finding and follow-up questions.",  "updated_at": "2026-09-03T12:35:00Z"}
Retry and idempotency

Do not blindly retry. On 502, retry only after the paper catalog has recovered. Reusing a version after another update returns 409; GET the note again and reconcile explicitly. That version check is what makes a repeated append safe, so never reuse a version that already succeeded. For an identity-related 409, follow the retryable contract below.

Delete a paper from the library

DELETE/api/v1/agent/papers/{paper_id}

Deletes the paper from the account library. This is not the board-membership delete: it removes the saved paper itself. By default the paper is moved to the library trash, so it keeps its note and its board memberships and stays restorable from the Library trash screen until the retention period expires. With permanent=true the paper, its note, its board memberships, and its saved snapshot are deleted outright, whether or not it was in the trash, and cannot be restored. An exact local paper identity needs no catalog lookup. When only a possible alias can identify the saved paper, an unavailable catalog returns 502 paper_metadata_unavailable and ambiguous stored identities return 409 library_identity_ambiguous before anything is deleted. A paper that is not in this account library returns 404.

Example request
litlas_curl --fail-with-body --silent --show-error \  --request DELETE \  'https://litlas.ai/api/v1/agent/papers/P43NTWMNFA70?permanent=false'
Success response (HTTP 200)
{  "paper_id": "P43NTWMNFA70",  "permanent": false,  "deleted": true}
Retry and idempotency

Repeating the default delete is idempotent: a paper already in the trash stays trashed and its retention period does not restart. Repeating a permanent=true delete returns 404 because the paper is gone. On 502, retry only after the paper catalog has recovered. For an identity-related 409, follow the retryable contract below. Verify before escalating to a permanent delete: it cannot be undone.

Errors

A request succeeds only with a 2xx response. Do not treat an error body as a successful result.

StatusMeaning
400The request is malformed or cannot be applied.
401The Bearer key is missing, invalid, or was invalidated by rotation.
403The key is valid, but this account is not permitted to perform the operation.
404The board or paper does not exist in this account scope.
409The resource state conflicts with the request, including a stale note version, a free-plan capacity limit, a concurrent library identity change, or stored identities that cannot be resolved unambiguously.
422A path parameter, query parameter, or JSON field does not match the documented schema. detail.code is note_too_long when appending would push the note past its length limit; nothing is written in that case.
502Required paper metadata or catalog verification was unavailable. detail.code is paper_metadata_unavailable and detail.paper_id identifies the paper. Board listings never return a partial list or a possibly inaccurate count; board-paper listings do not return 200 with unverified membership or a partial metadata result; paper addition does not report success; paper removal and note PATCH apply no requested change before this response.
500 / other 5xxLitlas could not complete the request. The result is not confirmed; inspect the response before retrying a write.

Error response schema

Every error has a top-level detail field. Depending on the error, detail is a string, a machine-readable object, or the standard validation array. Branch on the HTTP status first and, when detail is an object, on detail.code. Never assume one detail shape for every non-2xx response.

String detail example
{  "detail": "Board not found"}
Free-plan capacity exceeded (409)
{  "detail": {    "code": "free_capacity_exceeded",    "kind": "items",    "limit": 100,    "used": 100  }}
Library identity changed (409)
{  "detail": {    "code": "library_identity_changed",    "message": "The library changed concurrently. Retry the request.",    "retryable": true  }}
Stored library identity is ambiguous (409)
{  "detail": {    "code": "library_identity_ambiguous",    "message": "Stored library identities cannot be resolved unambiguously. No changes were applied.",    "retryable": false  }}
Stale note conflict (409)
{  "detail": {    "code": "note_conflict",    "paper_id": "P43NTWMNFA70",    "current_note": "The note saved by another client.",    "current_updated_at": "2026-09-03T12:34:00Z"  }}
Appended note exceeds the length limit (422)
{  "detail": {    "code": "note_too_long",    "paper_id": "P43NTWMNFA70",    "limit": 2000,    "length": 2064  }}
Paper metadata or catalog unavailable (502)
{  "detail": {    "code": "paper_metadata_unavailable",    "paper_id": "P43NTWMNFA70"  }}

Safe retries for library identity conflicts

Requests that resolve stored paper identities can return one of these 409 responses after a concurrent identity change or when stored identities cannot be resolved safely. Use retryable as an explicit instruction:

  • library_identity_changed with retryable: true: start the same request again from the beginning so Litlas re-resolves current identity. Retry at most once; if it repeats, stop and surface the error.
  • library_identity_ambiguous with retryable: false: do not guess, modify stored data, or retry automatically. No mutation was confirmed; stop and surface the error. Ask the account owner or Litlas support to repair the stored identity conflict, and retry only after they explicitly confirm that it is resolved.
  • Other 409 responses have their own recovery rules. In particular, reconcile note_conflict explicitly and do not retry free_capacity_exceeded unchanged.

The examples use curl --fail-with-body so HTTP errors produce a non-zero exit status while preserving the response body for diagnosis.

Request conventions

  • Send Content-Type: application/json when a request has a JSON body.
  • Path identifiers must be URL-encoded if they are inserted programmatically.
  • Only the query parameters documented for an endpoint are accepted. Anything else is a 422, so a typo cannot quietly fall back to a default.
  • Every Agent API response, including errors, sends Cache-Control: private, no-store and Pragma: no-cache. Do not persist account data in an intermediary cache.
  • Never guess that a timed-out write succeeded or failed. Read the resource before deciding whether to retry.
API documentation | Litlas