Skip to content

Private API catalogue

Every Overleaf route the server calls, with the request fields it sends and the response fields it reads. These are the browser-facing endpoints of Overleaf's open-source web service (overleaf/overleaf, services/web/app/src/router.mjs), reached through the saved web session and its CSRF token. None of them is a public or documented API, so this page is the contract the server assumes and the first place to look when Overleaf changes something.

Responses are private API shapes. The server reads only the fields listed here, validates the ones it depends on, and never logs or returns a response body. Where a response is validated, an unexpected shape surfaces as PROTOCOL_UNSUPPORTED rather than a parse exception.

Session and bootstrap

Method and route Sent Read Notes
GET /project session cookie <meta name="ol-csrfToken">, ol-user_id, ol-maxDocLength from the HTML Runs once at startup. A missing CSRF token means the session expired (AUTH_EXPIRED).
GET /socket.io/1/?... and WS /socket.io/1/websocket/:sessionId session cookie, projectId Socket.IO 0.9 handshake, then the joinProject payload: project.rootFolder, rootDoc_id, compiler, imageName, spellCheckLanguage, trackChangesState, permissionsLevel, protocolVersion One cached socket per project; document text goes over this channel as OT.

Projects

Method and route Sent Read Used by
POST /api/project {} totalSize, projects[] with _id or id, name, accessLevel, lastUpdated, archived, trashed (validated) list_projects, auth_status, and the name and trashed check behind manage_project
POST /project/new { projectName, template } where template is example or none project_id (validated) create_project
POST /Project/:id/clone { projectName } project_id (validated) clone_project
POST /project/new/upload multipart qqfile (the archive) and name project_id (validated); { success: false, error } or HTTP 422 with a short error code on rejection; HTTP 429 when throttled import_project_zip
POST /project/:id/rename { newProjectName } nothing manage_project rename
POST /project/:id/settings any of rootDocId, compiler, imageName, spellCheckLanguage nothing; the server re-joins the project to report the persisted values update_project_settings
POST /project/:id/trash, DELETE /project/:id/trash nothing nothing manage_project trash, restore
POST /Project/:id/archive, DELETE /Project/:id/archive nothing nothing manage_project archive, unarchive
DELETE /Project/:id nothing nothing manage_project delete; the server only sends it for a project the list reports as trashed
GET /Project/:id/download/zip nothing the archive's bytes, streamed to a temporary file and checked for a zip signature at the start and a zip end record at the end; Content-Type only when the bytes are not a zip; HTTP 429 without Retry-After past about 10 a minute per project; HTTP 403, not a login redirect, for an expired session download_project_zip

The capitalised /Project/ prefix is Overleaf's own; both spellings are live routes.

Overleaf builds the zip archive while it streams it, with no Content-Length, so a transfer cut short arrives with HTTP 200; only the missing end record shows it. A file Overleaf fails to read while building the archive is left out without an error.

Files and folders

Method and route Sent Read Used by
POST /project/:id/doc { parent_folder_id, name } _id create_file
POST /project/:id/folder { parent_folder_id, name } _id; HTTP 429 past about 60 a minute per project manage_entity create_folder
POST /project/:id/:type/:entityId/rename { name } nothing manage_entity rename
POST /project/:id/:type/:entityId/move { folder_id } nothing manage_entity move
DELETE /project/:id/:type/:entityId nothing nothing manage_entity delete; deleting a folder removes its subtree
POST /project/:id/upload?folder_id= multipart qqfile and name success, entity_id, entity_type, hash; HTTP 422 with a short error code on rejection; HTTP 429 past about 500 a project in 15 minutes upload_file, batch_upload
GET /Project/:id/doc/:entityId/download, GET /Project/:id/file/:entityId nothing raw bytes download_file

plan_sync, sync_directory, delete_entities, and batch_upload add no routes of their own. They are built from the rows above and from the document channel: the tree comes from a fresh joinProject, documents are read and written over OT exactly as for read_file and write_file, and files, folders, and deletes use the same routes as upload_file and manage_entity.

An upload over an existing path replaces the entity. In Overleaf's source a text document replacing a text document keeps its id and is updated as a diff, recorded as tracked changes when track changes is on for the uploading user; a binary replacing a binary, and a document replacing a binary or the reverse, become a new entity with a new id.

:type is doc, file, or folder. Rejection codes the server translates: duplicate_file_name (a folder of the same name is in the way), invalid_filename, project_has_too_many_files, folder_not_found (uploads); invalid_zip_file, empty_zip_file, zip_contents_too_large (zip import). Only a value matching ^[a-z][a-z0-9_]{0,63}$ is ever propagated, under details.overleafError.

Compilation

Method and route Sent Read Used by
POST /project/:id/compile?file_line_errors=true { rootDoc_id, check: "silent", incrementalCompilesEnabled: true }, plus stopOnFirstError: true and draft: true when asked status; outputFiles[] reduced to path, type, and build; numeric stats; clsiServerId. Everything else, url, timings, and pdfDownloadDomain included, is dropped compile_project
GET /project/:id/build/:buildId/output/:file ?clsiserverid= when the compile response named one the output file: output.log and .blg logs, at most 1 MB, for the summary and get_compile_log; any output for download_compile_output compile_project, get_compile_log, download_compile_output, the compile output resources
POST /project/:id/compile/stop nothing nothing stop_compile

file_line_errors makes TeX run with -file-line-error, so each error names its file as ./path.tex:12: message; the parser also reads the classic ! message form with its l.<n> marker. Overleaf's compile statuses (from CompileManager, ClsiManager, and the editor's local-compile-context) are success, failure, stopped-on-first-error, too-recently-compiled, rate-limited, autocompile-backoff, compile-in-progress, project-too-large, validation-problems, clsi-maintenance, unavailable, timedout, terminated, and exited; the tool reference maps each to a result or an error code. The response carries no top-level build id: it is each output file's build, validated as Overleaf does (^[0-9a-f]+-[0-9a-f]+$), as is clsiServerId (^[a-z0-9-]+$). The output route is built from those and a checked file path, never from an output file's url, which on overleaf.com points at a separate download domain. Overleaf keeps build output only for a while; a 404 from the output route is BUILD_NOT_FOUND.

Review

Method and route Sent Read Used by
GET /project/:id/threads nothing thread ids, messages, authors, resolution state list_comments
GET /project/:id/ranges nothing per-document comment ranges, when the deployment exposes it list_comments
POST /project/:id/thread/:threadId/messages { content } nothing add_comment, reply_to_comment
DELETE /project/:id/doc/:docId/thread/:threadId nothing nothing orphan cleanup after a failed add_comment attachment
POST /project/:id/doc/:docId/thread/:threadId/resolve and /reopen nothing nothing set_comment_status on ShareJS documents

Comment ranges themselves travel over the OT channel, not REST.

History

Method and route Sent Read Used by
GET /project/:id/updates?min_count=25 nothing updates[] with version ranges, timestamps, and users (validated; email fields dropped) monitor_project_history

HTTP status mapping

The HTTP client maps statuses before any tool sees them: 401 or a redirect to /login is AUTH_EXPIRED, 403 is PERMISSION_DENIED, 404 is NOT_FOUND, 413 is UPDATE_TOO_LARGE, 429 is RATE_LIMITED with retryAfterMs from Retry-After when present (the zip download sends none), and anything else that is not successful is REMOTE_ERROR with details.status. Writes are never retried on any of these.

Every id in a route is URI-encoded, and a project or thread id containing /, \, ., ?, #, or % is refused before a request is built. A path that would resolve to another origin than OVERLEAF_BASE_URL's is refused before it is sent. Reads follow redirects; a request that changes something follows none, because fetch would carry the x-csrf-token header to another origin: a redirect to /login is still AUTH_EXPIRED, any other is REMOTE_ERROR with the status. A body that should be JSON and is not, such as an HTML page, is PROTOCOL_UNSUPPORTED with details.path and details.contentType, never quoted. A rejection's error field reaches details.overleafError only when it is a short lowercase code.

Socket errors (connectionRejected, a failed joinDoc, leaveDoc, or applyOtUpdate acknowledgement, otUpdateError, and a Socket.IO error packet) are free text, and otUpdateError can quote the rejected update. Their text never reaches a caller: a message this release recognizes becomes an identifier in details.reason, such as invalid_session or update_too_large, and anything else is unrecognized.