Files and recordings
Upload documents to a note, follow their text extraction, and read lecture recordings.
Uploading files
A file upload takes three calls. The file itself goes straight to storage and never passes through the API.
POST /notes/{noteId}/fileswithfileName,mimeTypeandsizeBytes. The response has the file'sid, a signedupload.urland theupload.fieldsto send with it.- POST the file to
upload.urlasmultipart/form-data. Send every field fromupload.fieldsin the order given, then the file itself asfile, last. POST /files/{fileId}/complete. It checks that storage has the file, starts extracting its text and returns 202 with thejobId. PollGET /files/{fileId}untilstatusisreadyorfailed.
curl "$UPLOAD_URL" \
-F "bucket=workroad" \
-F "key=note/<noteId>/<fileId>-lecture.pdf" \
-F "Content-Type=application/pdf" \
-F "x-amz-date=<value>" \
-F "x-amz-algorithm=<value>" \
-F "x-amz-credential=<value>" \
-F "policy=<value>" \
-F "x-amz-signature=<value>" \
-F "[email protected]"Storage accepts the upload for 10 minutes after step 1 and refuses a file larger than sizeBytes, so send the exact size. Complete right after the upload, inside the same 10 minutes. The server keeps an uncompleted file for 24 hours after step 1, then deletes it and its row. Completing before the upload lands returns 409 conflict with details.reason upload-missing. Upload, then complete again. Completing checks the file's first bytes against its type, so a ZIP named notes.pdf returns 409 conflict with details.reason wrong-type, and the file is deleted.
The API takes documents up to 50 MiB: PDF, DOCX, PPTX, XLSX, ODT, ODS, ODP, RTF, EPUB, Markdown, HTML, CSV, LaTeX, Jupyter notebooks, plain text and source code. The file name's extension must match mimeType; the mimeType field of the create call lists each type. HTML, LaTeX and source code are sent as text/plain. Images are uploaded in the app.
Uploads are limited to 200 files and 2 GiB per user, and 25 files per note. Files on notes in the trash still count until permanent deletion. Over a limit, step 1 returns 409 conflict with details.reason set to files-per-user, files-per-note or bytes-per-user, plus details.limit and details.used in files or bytes. Restoring a note and deleting a file always remain available above these limits. Deleting a PDF preserves its embedded images in the note, and those images count toward the next upload's quota check. Delete files with DELETE /files/{fileId} or permanently delete trashed notes to make room.
A failed or expired file names the cause in failure. upload-expired and object-expired mean the 24 hours ran out and the upload is gone; start a new one. object-missing means storage had nothing when parsing started, unsupported-format and too-large mean the file can't be read, and not-parsed means nobody completed the upload. Completing a failed document again retries the parse. Completing a ready file returns 409 conflict with details.reason already-parsed and queues nothing. If the user already has too many jobs queued, completing returns 429 with Retry-After: 5.
GET /notes/{noteId}/files also lists images added in the app, with kind image. Only the app parses images, so completing one returns 409 conflict with details.reason image-file.
Anyone with a file's url can read the file once it is uploaded, without a key. Treat the URL as a secret.
Recordings
GET /notes/{noteId}/recordings lists a note's lecture recordings, and GET /recordings/{recordingId} returns one with its transcript, a line per entry, once it is finished. PUT /recordings/{recordingId}/summary saves a summary and keyTerms you wrote from the transcript in place of the ones the recording had, and keeps its chapters. It returns 409 with details.reason not-finished while the recording is still coming in, or summary-running while WorkRoad's AI is writing a summary.
List a note's files
Every file attached to the note, newest first, with its upload and parse status.
Required scopes: files:view
AuthorizationBearer <token>noteId*stringNote id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*array<>curl -X GET 'https://workroad.app/api/v1/notes/497f6eca-6276-4993-bfeb-53cbbbba6f08/files' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": [ { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a", "kind": "document", "name": "Cell biology lecture.pdf", "mimeType": "application/pdf", "sizeBytes": 482113, "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf", "status": "ready", "failure": null, "chars": 18240, "imageCount": 3, "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d", "createdAt": "2026-01-15T09:30:00.000Z" } ]}Start a file upload
Reserves a file on the note and returns a signed upload. POST the file to upload.url with the upload.fields within 10 minutes, then call POST /files/{fileId}/complete. A file that is never completed is deleted after 24 hours. Each user can hold 200 files and 2 GiB, and each note 25 files. Trashed files, and files on trashed notes, count until they are permanently deleted.
Required scopes: files:upload
AuthorizationBearer <token>noteId*stringNote id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
fileName*stringThe file's name, shown in the app. Its extension must match mimeType, for example notes.pdf with application/pdf.
1 <= length <= 255mimeType*stringThe type for the file's extension: application/pdf (.pdf), application/vnd.openxmlformats-officedocument.wordprocessingml.document (.docx), application/vnd.openxmlformats-officedocument.presentationml.presentation (.pptx), application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (.xlsx), application/vnd.oasis.opendocument.text (.odt), application/vnd.oasis.opendocument.spreadsheet (.ods), application/vnd.oasis.opendocument.presentation (.odp), application/rtf (.rtf), application/epub+zip (.epub), text/markdown (.md, .markdown), text/csv (.csv), text/plain (.html, .htm, .txt, .text, .log, .tex, .py, .js, .mjs, .cjs, .jsx, .ts, .mts, .cts, .tsx, .go, .rs, .java, .c, .h, .cpp, .cc, .cxx, .hpp, .sh, .bash, .zsh, .sql, .yaml, .yml, .json, .css, .scss, .kt, .swift, .rb, .php, .cs, .r, .lua, .toml, .ini, .xml), application/x-ipynb+json (.ipynb). HTML, LaTeX and source code are uploaded as text/plain. Images are uploaded in the app.
1 <= lengthsizeBytes*integerThe exact size of the file in bytes, at most 52428800 (50 MiB). Storage refuses a larger upload.
1 <= value <= 52428800Created
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/notes/497f6eca-6276-4993-bfeb-53cbbbba6f08/files' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "fileName": "string", "mimeType": "string", "sizeBytes": 1}'{ "data": { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "status": "awaiting_upload", "upload": { "url": "https://storage.example.com/workroad", "fields": [ { "name": "bucket", "value": "workroad" }, { "name": "key", "value": "note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf" }, { "name": "Content-Type", "value": "application/pdf" }, { "name": "x-amz-date", "value": "20260115T093000Z" }, { "name": "x-amz-algorithm", "value": "AWS4-HMAC-SHA256" }, { "name": "x-amz-credential", "value": "EXAMPLEKEY/20260115/us-east-1/s3/aws4_request" }, { "name": "policy", "value": "eyJleHBpcmF0aW9uIjoiMjAyNi0wMS0xNVQwOTo0MDowMFoifQ==" }, { "name": "x-amz-signature", "value": "9b2f0c1d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c" } ] }, "expiresAt": "2026-01-15T09:40:00.000Z" }}Complete a file upload
Checks that the upload reached storage and starts extracting the file's text. Poll GET /files/{fileId} until status is ready or failed. Calling it again while the file is parsing returns the same job.
Required scopes: files:upload
AuthorizationBearer <token>fileId*stringNote file id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255Accepted
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/files/497f6eca-6276-4993-bfeb-53cbbbba6f08/complete' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "status": "parsing", "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d" }}Get a file
The file's upload and parse status. failure explains a failed or expired file.
Required scopes: files:view
AuthorizationBearer <token>fileId*stringNote file id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/files/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a", "kind": "document", "name": "Cell biology lecture.pdf", "mimeType": "application/pdf", "sizeBytes": 482113, "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf", "status": "ready", "failure": null, "chars": 18240, "imageCount": 3, "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d", "createdAt": "2026-01-15T09:30:00.000Z" }}Move a file to the trash
The file disappears from every read and stops being searchable; POST /files/{fileId}/restore brings it back. Extracted images the note still shows are kept as standalone images, so the note doesn't change. If preserving those images would exceed a file or storage quota, nothing is moved. An image file is also removed from the note's content, and a restore doesn't put it back there. A trashed file keeps counting toward the storage limit until the trash is emptied after 30 days.
Required scopes: files:delete
AuthorizationBearer <token>fileId*stringNote file id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X DELETE 'https://workroad.app/api/v1/files/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "deleted": true }}Restore a file from the trash
Brings a trashed file back to its note's files and returns it. Needs files:delete, the scope that trashed it. The note must be out of the trash. An image file removed from the note's content stays out of it. The file never stopped counting toward the file and storage limits, so a restore always succeeds.
Required scopes: files:delete
AuthorizationBearer <token>fileId*stringNote file id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255OK
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/files/497f6eca-6276-4993-bfeb-53cbbbba6f08/restore' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f", "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a", "kind": "document", "name": "Cell biology lecture.pdf", "mimeType": "application/pdf", "sizeBytes": 482113, "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf", "status": "ready", "failure": null, "chars": 18240, "imageCount": 3, "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d", "createdAt": "2026-01-15T09:30:00.000Z" }}List a note's recordings
The lecture recordings on a note, newest first, without their transcripts. Read one with GET /recordings/{recordingId}.
Required scopes: notes:view
AuthorizationBearer <token>noteId*stringNote id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*array<>curl -X GET 'https://workroad.app/api/v1/notes/497f6eca-6276-4993-bfeb-53cbbbba6f08/recordings' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": [ { "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716", "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f", "title": "Lecture 7: CPU scheduling", "status": "finished", "summaryStatus": "done", "createdAt": "2026-01-15T09:30:00.000Z" } ]}Get a recording with its transcript
The recording's transcript, one line per entry, and its summary, key terms and chapters when it has them. Write notes, flashcards or a practice test from the transcript with your own model, then save them with POST /notes, POST /sets or POST /tests/from-questions.
Required scopes: notes:view
AuthorizationBearer <token>recordingId*stringRecording id from GET /notes/{noteId}/recordings.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/recordings/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716", "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f", "title": "Lecture 7: CPU scheduling", "status": "finished", "summaryStatus": "done", "createdAt": "2026-01-15T09:30:00.000Z", "transcript": [ "Today we look at how the scheduler picks the next process.", "Round robin gives each process a fixed time slice, in turn." ], "summary": "How a CPU scheduler chooses the next process, with round robin as the main example.", "keyTerms": [ { "term": "Round robin", "definition": "Each process runs for a fixed time slice, in turn." } ], "chapters": [ { "startMs": 0, "title": "Scheduling", "summary": "Why the CPU needs a scheduler." } ] }}Save a recording's summary
Saves a summary and key terms you wrote from the transcript, replacing the ones the recording had. Chapters stay as they are. The recording page shows them as it shows the ones WorkRoad's AI writes.
Use this to save content you wrote yourself. It uses none of the user's WorkRoad AI uses.
Required scopes: notes:update
AuthorizationBearer <token>recordingId*stringRecording id from GET /notes/{noteId}/recordings.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidapplication/json- body
summary*stringWhat the lecture covered, in plain text, up to 4000 characters.
1 <= length <= 4000keyTerms?array<>Up to 40 terms the lecture defines, each once. The app can make a study set from them.
items <= 40[]OK
application/json- response
data*curl -X PUT 'https://workroad.app/api/v1/recordings/497f6eca-6276-4993-bfeb-53cbbbba6f08/summary' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "summary": "string"}'{ "data": { "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716", "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f", "title": "Lecture 7: CPU scheduling", "status": "finished", "summaryStatus": "done", "createdAt": "2026-01-15T09:30:00.000Z", "transcript": [ "Today we look at how the scheduler picks the next process.", "Round robin gives each process a fixed time slice, in turn." ], "summary": "How a CPU scheduler chooses the next process, with round robin as the main example.", "keyTerms": [ { "term": "Round robin", "definition": "Each process runs for a fixed time slice, in turn." } ], "chapters": [ { "startMs": 0, "title": "Scheduling", "summary": "Why the CPU needs a scheduler." } ] }}