Documents
PDFs in folders, linked to notes, projects, tasks and people.
On this page
List folders
GET/folders
Folders of documents, alphabetical.
Alphabetical ignores case and accents ("apple", "Banana", "éclair"); ties by id. Filters are for browsing, so sync unfiltered.
curl "$NIFTY_URL/api/v1/folders" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
parent_idULID · optional | Only this folder's subfolders. Not with root. |
roottrue · optional | true: only top-level folders. Anything else, or with parent_id, is 400. |
ordername | id | updated_at · optional | The field to sort by; ties are broken by id, so the order is stable. |
directionasc | desc · optional | asc (ascending) or desc (descending), for order. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Receipts",
"parent_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"color": "amber",
"icon": "wallet",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add a folder
POST/folders
Needs a name. Top level unless parent_id is sent. A blank color gets the first one no other folder uses
(colours and icons are the vocabulary's colors and picker_icons, as GET /category_choices lists them).
curl -X POST "$NIFTY_URL/api/v1/folders" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"folder":{"name":"Receipts","parent_id":null}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
folderobject | |
folder.namestring · optional | Spaces are squished. A name a sibling has, ignoring case, is a 422. |
folder.parent_idULID or null · optional | The folder it's in; null for the top level. Not an id, unknown, itself or one inside it: a 422. |
folder.colorstring or null · optional | A key from the vocabulary's colors (or GET /category_choices). Blank picks the first one no other folder uses. |
folder.iconstring or null · optional | An icon from the vocabulary's picker_icons (or GET /category_choices), or null for the default folder icon. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Receipts",
"parent_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"color": "amber",
"icon": "wallet",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get a folder
GET/folders/{id}
curl "$NIFTY_URL/api/v1/folders/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Receipts",
"parent_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"color": "amber",
"icon": "wallet",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change a folder
PATCH/folders/{id}
Also PUT /folders/{id}, the same.
Rename, recolour or move a folder.
Only the fields sent change. Moving is changing parent_id (null: the top level); into itself or a folder
inside it is a 422 on parent_id. Documents hold only folder_id, so no document changes.
curl -X PATCH "$NIFTY_URL/api/v1/folders/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"folder":{"name":"Receipts","parent_id":null}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
folderobject | |
folder.namestring · optional | Spaces are squished. A name a sibling has, ignoring case, is a 422. |
folder.parent_idULID or null · optional | The folder it's in; null for the top level. Not an id, unknown, itself or one inside it: a 422. |
folder.colorstring or null · optional | A key from the vocabulary's colors (or GET /category_choices). Blank picks the first one no other folder uses. |
folder.iconstring or null · optional | An icon from the vocabulary's picker_icons (or GET /category_choices), or null for the default folder icon. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Receipts",
"parent_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"color": "amber",
"icon": "wallet",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete an empty folder
DELETE/folders/{id}
Permanent, with a folder deletion record. Only an empty folder (no subfolders, no documents) can be
deleted; otherwise it's 409 folder_not_empty. Documents deleted in the last 20 seconds that were in it lose
their folder_id (and get a new updated_at), so restoring one brings it back unfiled.
curl -X DELETE "$NIFTY_URL/api/v1/folders/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 409 429
Document counts per folder
GET/folder_counts
How many live documents each folder holds directly, and their size.
Not a record and not paginated: there's nothing to sync, so ask again when documents change. Ordered by
folder_id, the null row (documents in no folder) first; folders with no live documents are left out, and
so is the null row when every document is in a folder. Subfolders' documents aren't counted: add them up
from parent_id. A deleted document isn't counted, even while it can be restored.
curl "$NIFTY_URL/api/v1/folder_counts" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": [
{
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"document_count": 1,
"byte_size": 1
}
]
}
List documents
GET/documents
Documents (PDFs), alphabetical.
Live documents. Alphabetical ignores case and accents; ties by id. Filters combine (AND), except
unsorted; they're for browsing, so sync unfiltered. Search by name with GET /search?types=document.
curl "$NIFTY_URL/api/v1/documents" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
folder_idULID · optional | Only this folder's documents (not its subfolders', unless include_subfolders). |
include_subfoldersboolean · optional | true: with folder_id, its documents and those in every folder below it, at any depth. Without folder_id it's 400. |
unsortedtrue · optional | true: only documents with no folder and no link to a live note, project, task or person. Not with any other filter; anything else is 400. |
note_idULID · optional | Only documents linked to this note. At most one of note_id, project_id, task_id and person_id (two is 400); an unknown or deleted one is an empty page. |
project_idULID · optional | A record's ID. |
task_idULID · optional | A record's ID. |
person_idULID · optional | A record's ID. |
ordername | created_at | updated_at | id · optional | The field to sort by; ties are broken by id, so the order is stable. |
directionasc | desc · optional | asc (ascending) or desc (descending), for order. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Boiler warranty",
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"project_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"task_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"person_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"links": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"file": {
"filename": "string",
"byte_size": 1,
"content_type": "application/pdf",
"url": "https://example.com",
"download_url": "https://example.com",
"thumbnail_url": "https://example.com"
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Upload a PDF
POST/documents
A multipart/form-data body (any other type is 415) with the PDF in file and, optionally, the other
fields. The type is read from the bytes: anything but a PDF is a 422 on file, whatever its declared type or
name. Up to 50 MB. A body without a Content-Length is 411, and one over 50 MB (plus 1 MB for the other
fields) is 413, both before anything is read, even the token. With no folder and no links it's Unsorted.
Idempotency-Key compares the fields and the file's bytes, not the raw body, so a retry with new multipart
boundaries is the same request.
curl -X POST "$NIFTY_URL/api/v1/documents" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-F '[email protected]'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
filefile | The PDF. Its file name (1–255 characters, no NUL) is kept as file.filename. |
namestring · optional | Spaces are squished. Blank: the file name without .pdf. |
folder_idULID · optional | A record's ID. |
note_ids[]ULID[] · optional | |
project_ids[]ULID[] · optional | |
task_ids[]ULID[] · optional | |
person_ids[]ULID[] · optional |
Response
201 Created Errors: 400 401 403 409 411 413 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Boiler warranty",
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"project_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"task_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"person_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"links": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"file": {
"filename": "string",
"byte_size": 1,
"content_type": "application/pdf",
"url": "https://example.com",
"download_url": "https://example.com",
"thumbnail_url": "https://example.com"
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get a document
GET/documents/{id}
A deleted document is a 404.
curl "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Boiler warranty",
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"project_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"task_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"person_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"links": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"file": {
"filename": "string",
"byte_size": 1,
"content_type": "application/pdf",
"url": "https://example.com",
"download_url": "https://example.com",
"thumbnail_url": "https://example.com"
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change a document
PATCH/documents/{id}
Also PUT /documents/{id}, the same.
Rename, file or link a document.
JSON. Only the fields sent change. Each id list replaces that kind's links (to live records; links to ones deleted in the last 20 seconds are kept, so undoing their delete restores them). The file can't be replaced: delete the document and upload again.
curl -X PATCH "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document":{"name":"Boiler warranty","folder_id":"01j9zq3f0a2b4c6d8e0f2g4h6j","project_ids":["01j9zq3p4q6r8s0t2v5w7x9y1z"]}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
documentDocumentWrite | |
document.namestring · optional | Spaces are squished. Needn't be unique. |
document.folder_idULID or null · optional | Null: no folder. Not an id, or an unknown folder, is a 422. |
document.note_idsULID[] or null · optional | Replaces its links to notes; null or [] removes them. An unknown or deleted id, or one that isn't an id, is a 422 on the field. |
document.project_idsULID[] or null · optional | |
document.task_idsULID[] or null · optional | A task in a deleted list, person or project counts as deleted. |
document.person_idsULID[] or null · optional |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Boiler warranty",
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"project_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"task_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"person_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"links": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"file": {
"filename": "string",
"byte_size": 1,
"content_type": "application/pdf",
"url": "https://example.com",
"download_url": "https://example.com",
"thumbnail_url": "https://example.com"
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete a document
DELETE/documents/{id}
Restorable for 20 seconds (POST /documents/{id}/restoration), with its folder and links; then permanent,
file and all. Gone from every endpoint at once, with a document deletion record. Deleting again is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Restore a document
POST/documents/{document_id}/restoration
Undo deleting a document.
Within 20 seconds of DELETE /documents/{id}: brings it back with its folder and links. Its deletion record is
removed and it gets a new updated_at. No body. Restoring one that isn't deleted changes nothing and returns
it, so a retry is safe. Once the 20 seconds have passed, or for an unknown id, it's a 404.
curl -X POST "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e/restoration" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
document_idULID | A record's ID. |
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Response
200 Restored. The document, as GET /documents/{id} returns it. Errors: 400 401 403 404 409 415 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Boiler warranty",
"folder_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"project_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"task_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"person_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"links": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"file": {
"filename": "string",
"byte_size": 1,
"content_type": "application/pdf",
"url": "https://example.com",
"download_url": "https://example.com",
"thumbnail_url": "https://example.com"
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Download a document's PDF
GET/documents/{document_id}/file
The PDF itself.
The file, with Content-Type: application/pdf, X-Content-Type-Options: nosniff and Cache-Control: private, max-age=0 (no-store under the web session). Content-Disposition names it after the document's
current name plus .pdf. Send a single Range (e.g. bytes=0-65535) for part of it (206, with
Content-Range); several ranges get the whole file. Needs the token (or the web session) like any endpoint:
there's no public URL. The web app shows it inline in a same-origin <iframe> (X-Frame-Options: SAMEORIGIN).
curl "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e/file" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
document_idULID | A record's ID. |
Query parameters
| Name | Description |
|---|---|
dispositioninline | attachment · optional | attachment for a download; anything but inline or attachment is 400. |
Headers
| Name | Description |
|---|---|
Rangestring · optional |
Response
200 The whole PDF. Errors: 400 401 404 416 429
"@file.pdf"
Get a document's thumbnail
GET/documents/{document_id}/thumbnail
A JPEG of the first page.
The first page, 480 pixels wide (at most 1440 tall) on white, with Content-Type: image/jpeg,
X-Content-Type-Options: nosniff and Cache-Control: private, max-age=0 (no-store under the web session).
404 when the document has none (file.thumbnail_url is null). Needs the token (or the web session).
curl "$NIFTY_URL/api/v1/documents/01j9zq3k8m5x2v7c4n6b0t1r9e/thumbnail" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
document_idULID | A record's ID. |
Response
200 The JPEG. Errors: 401 404 429
"@file.pdf"
Link a document
POST/document_links
Link a document to a note, project, task or person.
Exactly one target. The document gets a new updated_at and lists the link in links. An unknown or deleted
document or target (a task in a deleted list, person or project counts as deleted), no target or two, or one
already linked, is a 422 on the field.
curl -X POST "$NIFTY_URL/api/v1/document_links" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document_link":{"document_id":"01j9zq3d1e3f5g7h9j1k3m5n7p","note_id":"01j9zq3n6p8q0r2s4t6v8w0x2y"}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
document_linkDocumentLinkWrite | document_id and exactly one of note_id, project_id, task_id and person_id. |
document_link.document_idULID | A record's ID. |
document_link.note_idULID or null · optional | A record's ID. |
document_link.project_idULID or null · optional | A record's ID. |
document_link.task_idULID or null · optional | A record's ID. |
document_link.person_idULID or null · optional | A record's ID. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"document_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"note_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"task_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"created_at": "2026-10-10T09:30:00Z"
}
}
Remove a document link
DELETE/document_links/{id}
Permanent; the document stays (in Unsorted if that was its last folder or link) and gets a new updated_at. An
unknown link, one whose document is deleted, or one to a deleted record (not in links; it comes back if that
record's delete is undone) is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/document_links/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429