# Files

> Upload a document once, then download it as a PDF, as Word, as embeddable HTML or as its listing, with or without its metadata.

> Integrating Version Story into a codebase as a coding agent? Follow https://www.versionstory.com/developers/quickstart.md: it signs the engineer in, gets an API key, and puts the PDF redline in their app first.

A file is one immutable document held by Version Story, named by a `file_id`. Uploads, a
comparison's redline, an edit's results and created documents are all files, and every endpoint
that reads a document takes its `file_id`. Every response that carries a document carries it as the
same File object, the one `GET /v1/files/{file_id}` answers with.

| | |
| --- | --- |
| Base URL | `https://api.versionstory.com` |
| Auth | `Authorization: Bearer vs_...`. See [Authentication](/developers/authentication) |

## POST /v1/files

`multipart/form-data` with one file field, `document`: a `.docx`, `.doc` or `.pdf` to read, edit or
show, or a `.xlsx` or `.pptx` to compare with another of its kind.

```bash
curl -sS https://api.versionstory.com/v1/files \
  -H "Authorization: Bearer vs_live_..." \
  -F "document=@nda.docx"
```

```json
{ "file_id": "3f2a9c1e-...", "status": "processing" }
```

- **A `.docx`** answers at once while it is prepared in the background. `processing` needs no polling
  of its own: every request that reads the file waits for it.
- **A `.doc` or `.pdf`** is converted to Word first. The request waits up to 20 seconds and answers
  `ready`, `failed` with an `error`, or `processing` if conversion is still running. A redline PDF's
  insertions and deletions become tracked changes; send the optional field
  `track_redline_changes=false` to keep them as formatting. See
  [Redline PDFs](/developers/reference/formats-and-limits#redline-pdfs).
- **A workbook or presentation** is stored as it is and answers `ready`. It can be compared, never
  read or edited.

## GET /v1/files/&#123;file_id&#125;

Signed downloads for one file. The `pdf` shows the file's own tracked changes and comments as a
redline: it is what to show a person.

| Parameter | In | Required | Default | Meaning |
| --- | --- | --- | --- | --- |
| `file_id` | path | Yes | — | The file's id |
| `format` | query | No | `docx` | One or more of `pdf`, `html`, `docx`, `md`, `json`. Repeatable or comma-separated. A workbook or presentation is served as it is (`xlsx`, `pptx`) |
| `clean_metadata` | query | No | — | `standard` or `strict`: serve every format with its metadata removed. See [Clean metadata](#clean-metadata) |
| `clean_metadata_revisions` | query | No | the preset's | With `clean_metadata`: `keep`, `accept` or `reject` tracked changes |
| `clean_metadata_comments` | query | No | the preset's | With `clean_metadata`: `keep` or `remove` comments |
| `clean_metadata_authors` | query | No | the preset's | With `clean_metadata`: `keep` or `anonymize` the names on changes and comments |
| `search` | query | No | — | `md` only: return only the paragraphs holding a phrase. See [Reading the listing](#reading-the-listing) |
| `offset`, `max_chars` | query | No | — | `md` only: read the listing in parts. See [Reading the listing](#reading-the-listing) |

| `format` | Is |
| --- | --- |
| `pdf` | The document as a PDF, with its tracked changes and comments shown. The view to show a person |
| `html` | The document as one self-contained HTML file, to frame where the view must be interactive. See [Embed redlines](/developers/rest/embed) |
| `docx` | The Word file |
| `md` | The document's listing as Markdown: its text, each paragraph's locator, and its tracked changes. What edits are written from, and what a model reads. See [Edit](/developers/rest/edit#the-listing) |
| `json` | The same listing as structured data, for code. See [Document JSON](/developers/reference/json-format) |

```bash
curl -sS "https://api.versionstory.com/v1/files/3f2a9c1e-...?format=pdf,docx" \
  -H "Authorization: Bearer vs_live_..."
```

```json
{
  "file_id": "3f2a9c1e-...",
  "file_name": "nda.docx",
  "status": "processing",
  "downloads": [
    {
      "format": "docx",
      "url": "https://documents.versionstory.com/...",
      "file_name": "nda.docx",
      "expires_at": "2026-10-22T18:00:00Z"
    }
  ],
  "pending_formats": ["pdf"]
}
```

`status` is the state of the formats this request asked for: `processing` while one is still being
made (the response carries `Retry-After`; poll again), `ready` when every one is in `downloads`, and
`failed` when the document cannot be used or a requested format will never be made. The first request
for a PDF or HTML starts making it. A format that failed is named in `failed_formats`, and a file whose
source could not be read at all answers `"status": "failed"` with an `error`.

Signed links are valid for one hour, and polling again mints fresh ones. A workbook or presentation
has no `pdf`, `html`, `md` or `json`, and asking for one is `400 UNSUPPORTED_FORMAT`.

### Reading the listing

The `md` listing of a long document can be large. Two ways to read only part of it, both for `md`
alone:

- **`search`**, repeated for several phrases (`?format=md&search=Closing&search=36%20months`). Once
  the listing is ready, the response adds `markdown`, holding only the paragraphs that contain a
  phrase, each after the lines that say where it is (its header, footer, note, table, row and cell).
  `matches` gives each phrase with the number of paragraphs holding it. A phrase matches as an edit's
  text selector does, ignoring case. The paragraphs stop at 48,000 characters, and
  `omitted_paragraphs` then counts those left out.
- **`offset` and `max_chars`** read the listing in parts. `markdown` holds the part starting at
  `offset` (0, or the last response's `next_offset`) and ending at the last whole line that fits in
  `max_chars`. The response adds `total_chars` and, until the part reaches the end, `next_offset`.

`search` takes neither `offset` nor `max_chars`; sending them together, or with any format but `md`,
is `400 INVALID_REQUEST`.

### Clean metadata

A document carries more than its text: who wrote and edited it, when, on which template and from
which network folder, its comments and tracked changes, hidden text, and the earlier revisions a PDF
keeps. Add `clean_metadata` to download a copy without it, to send outside the firm.

```bash
curl -sS "https://api.versionstory.com/v1/files/3f2a9c1e-...?format=pdf,docx&clean_metadata=standard" \
  -H "Authorization: Bearer vs_live_..."
```

| Preset | What it removes |
| --- | --- |
| `standard` | What nobody sees on the page: document properties, people and account ids, revision ids, hidden text, document variables, mail merge sources, custom XML, printer settings, image metadata, and a PDF's XMP, earlier revisions and JavaScript. Tracked changes, comments and authors' names stay |
| `strict` | Everything `standard` removes, and the review: tracked changes accepted, comments, PDF notes and speaker notes removed, authors anonymized, white or tiny text, hidden sheets and slides, and macros removed, and PDF form fields and stamps flattened into the page |

The three choices override the preset: `clean_metadata=standard&clean_metadata_comments=remove` keeps
tracked changes but removes comments. Each choice needs `clean_metadata`; one sent without it is
`400 INVALID_REQUEST`, so a request never gets the uncleaned document by mistake. `clean_metadata`
cleans documents, not listings, so it does not apply to `md`, `json` or `html`.

Each download in the response is the cleaned copy, with a `metadata_report`:

```json
{
  "format": "docx",
  "url": "https://documents.versionstory.com/...",
  "file_name": "nda.docx",
  "expires_at": "2026-10-22T18:00:00Z",
  "metadata_report": {
    "format": "docx",
    "preset": "standard",
    "items": [
      { "type": "author", "category": "properties", "count": 1, "action": "remove", "examples": [] },
      { "type": "comments", "category": "review", "count": 4, "action": "keep", "examples": [] }
    ],
    "warnings": [
      { "type": "document_numbers", "message": "A header or footer shows a document number or file path. It is visible text, so it was left in place." }
    ]
  }
}
```

`items` lists every kind of hidden data found and the `action` taken: what was removed, reset or
converted, and with `keep` or `report`, what was left in place. Show the `warnings` to whoever sends
the document; they name what the cleaner cannot decide for you, such as visible document numbers or
text under a dark box that only looks redacted. Every type and what each preset does to it is in
[Metadata types](/developers/reference/metadata).

A PDF that Version Story made from a Word file (a redline's, or an uploaded document's) draws its
comments, their authors and its tracked changes onto the page, where no clean of the PDF can reach
them. So its clean cleans the Word file with the same options and renders the PDF again: what the
options remove from the Word file is gone from the PDF too. A comparison's redline is cleaned through
this endpoint by its `file.file_id`.

A format whose clean is still running is in `pending_formats`, as a PDF still converting is; poll
until it is `ready`. One that cannot be cleaned is in `failed_formats`, and its uncleaned original is
never served in its place. Word, Excel, PowerPoint and PDF files are cleaned, and each copy is kept for
the options that made it, so asking again is immediate.

## POST /v1/files/&#123;file_id&#125;/acknowledge

For an organization with [zero-day retention](/developers/reference/data-handling#retention), deletes
an uploaded file, a created document or a [selective comparison's](/developers/rest/selective-compare#the-pdf-file)
PDF as soon as you have saved what you need, instead of an hour after it was created. No body.

```bash
curl -sS -X POST https://api.versionstory.com/v1/files/3f2a9c1e-.../acknowledge \
  -H "Authorization: Bearer vs_live_..."
```

```json
{ "file_id": "3f2a9c1e-...", "status": "deletion_queued" }
```

An edit's files are deleted with the edit: acknowledge those with
[`POST /v1/edit/{edit_id}/acknowledge`](/developers/rest/edit#post-v1editeditidacknowledge).

## Errors

| Status | Code | Cause |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | A missing or empty `document`; an unknown `clean_metadata` value, a choice sent without it, or `clean_metadata` with `md`, `json` or `html`; or `search`, `offset` or `max_chars` sent together or with a format other than `md` |
| 400 | `UNSUPPORTED_FILE_TYPE` | The upload is not a `.docx`, `.doc`, `.pdf`, `.xlsx` or `.pptx` |
| 400 | `DOCUMENT_UNREADABLE` | The upload is not a readable Word file: corrupt, or encrypted |
| 400 | `UNSUPPORTED_FORMAT` | A `format` is unknown, or not offered for this file |
| 401 | `INVALID_API_KEY` | Missing, malformed, unknown, revoked, or expired key |
| 402 | `USAGE_LIMIT_REACHED` | The organization's monthly upload allowance is exhausted |
| 404 | `FILE_NOT_FOUND` | No such file, or it belongs to another organization |
| 409 | `CONFLICT` | An acknowledgment of a file without zero-day retention, or of an edit's own file |
| 410 | `CONTENT_DELETED` | The file's content was deleted under zero-day retention |
| 413 | `FILE_TOO_LARGE` | The upload exceeds the 100 MB limit |

### Kept for existing integrations

`GET /v1/files/{file_id}/content` answers the listing in the response body rather than as a download:
`markdown` with `format=md`, `document` with `format=json`. It is deprecated, and marked so in the
OpenAPI specification; read the `md` and `json` downloads above instead. Its `document` gives `level`
the meaning the download calls `list_level`.

## Related

- [Edit](/developers/rest/edit): change a document with tracked changes
- [Embed redlines](/developers/rest/embed): frame a document's HTML
- [Metadata types](/developers/reference/metadata): every kind of hidden data a clean reports
- [Data handling](/developers/reference/data-handling): what is kept, and for how long
