---
title: "Files"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://openemail.uk/docs/api/reference/files"
area: "API"
category: "Reference"
---

# Files

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

## Operations

Every file the mailbox holds, sent, received and uploaded, with its bytes, its totals and the download links it went out as. It is the Files page of the app, gated like the mail: `threads:read` reads it and `threads:write` uploads and deletes. Only an upload that no message or live download link uses can be deleted, one at a time or up to 100 at once. Received and sent files stay with their message.

### `GET /files`

List files

Every attachment the mailbox holds, sent and received, and every file uploaded to it, the Files page of the app: name, type, size, the address it came to, the thread it belongs to and whether it can be deleted. A deleted file is not listed.

Only an upload that nothing depends on can be deleted, and its `deletable` is true. Every other file has a `usage` saying what keeps it: `received` or `sent` for a file that came in or went out on a message, `linked` for an upload that went out as a download link that still works, and `scheduled` for one attached to a message that has not gone out yet.

Files have scopes of their own: `files:read` reads them and `files:write` uploads, deletes and publishes them. Reading an email with `threads:read` still reads the attachments on it. A key limited to particular addresses or domains sees only the files that arrived at them. It does not see files uploaded for the whole workspace, which have no `deliveredTo`.

The index starts from the day it shipped, so an older mailbox lists what has arrived since. Older attachments are still on their messages, where `GET /threads/{id}/messages/{messageId}/attachments` reads them.

Requires the `files:read` scope.

- Scopes: `files:read`.

**Query parameters**

- `q` (`string`, up to 200 characters): Searches the file name and its type. Words match loosely, and a close spelling is tried when nothing matches exactly.
- `kind` (`string`, one of `"image"`, `"pdf"`, `"audio"`, `"video"`, `"text"`): Keeps one kind of file, the choices of the filter on the Files page: images, PDFs, audio, video or text.
- `direction` (`string`, one of `"inbound"`, `"outbound"`, `"uploaded"`): Keeps one direction: `inbound` for files that arrived on a message, `outbound` for files that went out on one, and `uploaded` for files put on the Files page.
- `address` (`string`, up to 320 characters): Keeps the files of one address, the `deliveredTo` of the file, compared without regard to case.
- `since` (`string`, format `date-time`): Keeps files added at or after this moment, as an ISO 8601 date or date-time.
- `until` (`string`, format `date-time`): Keeps files added before this moment, as an ISO 8601 date or date-time.
- `sort` (`string`, one of `"newest"`, `"oldest"`, `"largest"`, `"name"`, default `"newest"`): The order. A cursor carries on in the order it was handed out in, and one handed out under another `sort` is a 400 `invalid_cursor`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `FileList`: A page of files in the order `sort` names.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.list()`](https://openemail.uk/docs/sdk/reference/files#list), [`files.listAll()`](https://openemail.uk/docs/sdk/reference/files#listAll), [`files.iterate()`](https://openemail.uk/docs/sdk/reference/files#iterate); CLI [`openemail files list`](https://openemail.uk/docs/cli/reference/files#files-list); MCP [`listFiles`](https://openemail.uk/docs/mcp/tools/files#listFiles).

### `POST /files`

Upload a file

Stores a file on the Files page, the way its Upload button does, and answers with it. Send the file itself as the body, up to 100 MB, with its type in `Content-Type` and its name in `filename`. Attach it to a send as `{ "fileId": "..." }` in the `attachments` of `POST /emails`, which is how a file larger than the inline cap goes out.

A key limited to particular addresses or domains uploads to the first address it holds, which becomes `deliveredTo`. An unlimited key uploads for the whole workspace and `deliveredTo` is null, so a narrowed key never reaches that file.

A workspace keeps up to 10 GB of uploads and takes 500 an hour. Nothing makes an upload safe to repeat, so after a lost response look for the file with `GET /files` before sending it again.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Query parameters**

- `filename` (`string`): The name the file is stored and downloaded under, such as `report.pdf`, taken as it is. It is cleaned rather than refused: characters a file name cannot hold become `_`, and a name longer than 255 characters is cut, keeping its extension. Left out, `X-Filename` is read instead.

**Headers**

- `X-Filename` (`string`): The file name, URI encoded, for a client that would rather not put it in the URL. `filename` wins when both are sent.

**Request body**

Content type: `*/*`.

`binary`

**Returns**

- `201` `File`: Stored. `direction` is `uploaded`, `usage` is null and `deletable` is true.

**Errors**

- `400`: `upload_no_name`: no name in `filename` or `X-Filename`. `upload_empty`: the body is empty. `upload_dangerous`: a program or script, judged by its name, which cannot be stored.
- `403`: `insufficient_scope`: the key lacks `files:write`. `upload_no_address`: a key limited to particular addresses or domains that holds no address.
- `413`: `upload_too_large`: the file is larger than 100 MB.
- `429`: `upload_rate_limited`: 500 files were uploaded to this workspace in the last hour, deleted ones included. Try again later.
- `502`: `upload_failed`: the file could not be stored, and nothing was kept. Try again.
- `507`: `upload_storage_full`: the uploads on this workspace would pass 10 GB. Delete some first.
- The errors every operation can return: `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.upload()`](https://openemail.uk/docs/sdk/reference/files#upload); CLI [`openemail files upload`](https://openemail.uk/docs/cli/reference/files#files-upload); MCP [`uploadFile`](https://openemail.uk/docs/mcp/tools/files#uploadFile).

### `GET /files/stats`

Count files

The numbers on the Analytics tab of the Files page in one request: how many files the mailbox holds and how many bytes they take, in all and split into received, sent and uploaded, the file types and the addresses taking the most space, how many files arrived on each of the last 30 days, and the download links still working with how often they were fetched.

Deleted files are not counted. A key limited to particular addresses or domains counts only the files that arrived at them, so files uploaded for the whole workspace are left out of its numbers.

Requires the `files:read` scope.

- Scopes: `files:read`.

**Returns**

- `200` `FileStats`: The numbers on the Analytics tab of the Files page.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.stats()`](https://openemail.uk/docs/sdk/reference/files#stats); CLI [`openemail files stats`](https://openemail.uk/docs/cli/reference/files#files-stats); MCP [`getFileStats`](https://openemail.uk/docs/mcp/tools/files#getFileStats).

### `POST /files/batch-delete`

Delete files in bulk

Deletes up to 100 files in one call, each the way `DELETE /files/{id}` deletes one, and reports the rest instead of failing. It is what selecting several files on the Files page and pressing Delete does.

Only an upload that nothing depends on is deleted. A file that was received or sent, or an upload that went out as a download link that still works or is attached to a message that has not gone out yet, comes back in `kept` with its `usage` and a `reason`. An id that is unknown, already deleted or outside the addresses a narrowed key holds comes back in `missing`. The rule is checked again at the moment of deleting, so a file that became used in between is kept. There is no undo.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Request body**

- `ids` (`string[]`, required, 1 to 100 items): 1 to 100 file ids from `GET /files`, each at most 128 characters. A repeated id counts once.

**Returns**

- `200` `FileBatchDelete`: What was deleted, what was kept and why, and what was not found.

**Errors**

- `400`: `malformed_json`: the body is not JSON.
- `422`: `invalid_parameter` on `ids` for none or more than 100, and `unknown_parameter` for any key but `ids`.
- The errors every operation can return: `401`, `403`, `404`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.deleteMany()`](https://openemail.uk/docs/sdk/reference/files#deleteMany); CLI [`openemail files delete-many`](https://openemail.uk/docs/cli/reference/files#files-delete-many); MCP [`deleteFiles`](https://openemail.uk/docs/mcp/tools/files#deleteFiles).

### `GET /files/{id}`

Retrieve a file

One file, with `usage` and `deletable` saying whether it can be deleted and what keeps it when it cannot. A deleted file, one outside the addresses a narrowed key holds, and an unknown id all answer 404.

Requires the `files:read` scope.

- Scopes: `files:read`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Returns**

- `200` `File`: The file. `GET /files/{id}/content` has its bytes.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.get()`](https://openemail.uk/docs/sdk/reference/files#get); CLI [`openemail files get`](https://openemail.uk/docs/cli/reference/files#files-get); MCP [`getFile`](https://openemail.uk/docs/mcp/tools/files#getFile).

### `DELETE /files/{id}`

Delete a file

Deletes an uploaded file: its bytes are removed from storage and it leaves the Files page. The row stays, marked deleted, so the index knows the file existed. There is no undo.

Only an upload that nothing depends on can be deleted, the files whose `deletable` is true. A file that was received or sent stays with its message, and so does an upload that went out as a download link that still works or is attached to a message that has not gone out yet: those answer 409 `file_in_use`. An upload that was sent inside a message can still be deleted, because the message keeps its own copy, which is listed as a separate `sent` file.

It needs the same scope as uploading. A key limited to particular addresses or domains may delete only a file that arrived at one of them. `POST /files/batch-delete` deletes many at once.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Returns**

- `200` `object`: Deleted. The bytes are gone and the file leaves the Files page.
  - `object` (`string`, one of `"file"`)
  - `id` (`string`)
  - `deleted` (`boolean`, one of `true`)

**Errors**

- `404`: `resource_not_found`: the id is unknown, the file is already deleted, or it is outside the addresses a narrowed key holds.
- `409`: `file_in_use` on `id`: something depends on the file, so it stays. The message names the file and says why, and `usage` on `GET /files/{id}` says the same.
- The errors every operation can return: `400`, `401`, `403`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.delete()`](https://openemail.uk/docs/sdk/reference/files#delete); CLI [`openemail files delete`](https://openemail.uk/docs/cli/reference/files#files-delete); MCP [`deleteFile`](https://openemail.uk/docs/mcp/tools/files#deleteFile).

### `GET /files/{id}/content`

Download a file

The file exactly as it is stored, the same bytes the Download action on the Files page saves. A file whose bytes are gone from storage answers 404 like a missing one.

Requires the `files:read` scope.

- Scopes: `files:read`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Query parameters**

- `download` (`string`, one of `"true"`, `"false"`): `true` always answers `Content-Disposition: attachment`. Otherwise an image, a PDF or plain text answers `inline`.

**Returns**

- `200` `binary` (`application/octet-stream`): The bytes, under the file's own `Content-Type` and name.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.download()`](https://openemail.uk/docs/sdk/reference/files#download); CLI [`openemail files download`](https://openemail.uk/docs/cli/reference/files#files-download).

### `GET /files/{id}/links`

List the download links of a file

Every download link made for this file, newest first, revoked ones included. A large attachment sent from OpenEmail travels as a link rather than inside the message, and each send makes a link of its own, and `POST /files/{id}/links` makes one on purpose, so this is how often each copy was fetched and when last.

A file attached inside a message has no links, because nothing records when those are opened.

Requires the `files:read` scope.

- Scopes: `files:read`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Query parameters**

- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `FileLinkList`: A page of links, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.listLinks()`](https://openemail.uk/docs/sdk/reference/files#listLinks), [`files.listAllLinks()`](https://openemail.uk/docs/sdk/reference/files#listAllLinks), [`files.iterateLinks()`](https://openemail.uk/docs/sdk/reference/files#iterateLinks); CLI [`openemail files list-links`](https://openemail.uk/docs/cli/reference/files#files-list-links); MCP [`getFile`](https://openemail.uk/docs/mcp/tools/files#getFile), [`getFileDownloads`](https://openemail.uk/docs/mcp/tools/tracking#getFileDownloads).

### `POST /files/{id}/links`

Publish a file at a public link

Makes a public download link for the file that anybody holding it can open without signing in, which is how an image or document is referenced from an email or a web page. Each call makes a new link with its own download count. The link keeps working until it is revoked or the file is deleted.

A file that came in or went out on a message is copied to public storage first. Programs and scripts are refused with 422 `file_unshareable`.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Request body**

- `domain` (`string`): The domain whose files host serves the link, such as `acme.com` for `files.acme.com`. Left out, the domain of the address the file belongs to is used, and a file with no address, or a domain with no files host, is served from the API address.

**Returns**

- `201` `FileLink`: The new link. `url` opens the file with no sign-in.

**Errors**

- `422`: `file_unshareable`: the file is a program or script, which never gets a public link.
- The errors every operation can return: `400`, `401`, `403`, `404`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.createLink()`](https://openemail.uk/docs/sdk/reference/files#createLink); CLI [`openemail files create-link`](https://openemail.uk/docs/cli/reference/files#files-create-link); MCP [`createFileLink`](https://openemail.uk/docs/mcp/tools/files#createFileLink).

### `DELETE /files/{id}/links`

Revoke every link to a file

Makes the file private, as Make private on the Files page does: every public download link to it stops working at once, including the ones in mail that already went out, and `revoked` says how many were still working. A file with no live links answers `revoked: 0`, so the call is safe to repeat. The file itself stays, and `POST /files/{id}/links` can publish it again at a new link.

A key limited to particular addresses reaches only the files that arrived at them, and any other file is a 404.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.

**Returns**

- `200` `FileLinkRevocation`: How many links stopped working.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.revokeAllLinks()`](https://openemail.uk/docs/sdk/reference/files#revokeAllLinks); CLI [`openemail files revoke-all-links`](https://openemail.uk/docs/cli/reference/files#files-revoke-all-links); MCP [`makeFilePrivate`](https://openemail.uk/docs/mcp/tools/files#makeFilePrivate).

### `DELETE /files/{id}/links/{linkId}`

Revoke a public link

Stops a public link from working, for good, including in mail that already went out with it. Revoking a link that is already revoked answers with it unchanged. An unknown link is a 404 `not_found`.

Requires the `files:write` scope.

- Scopes: `files:write`.

**Path parameters**

- `id` (`string`, required): The id from `GET /files`, `file_` and 24 hex.
- `linkId` (`string`, required): The link id from the links of the file.

**Returns**

- `200` `FileLink`: The link, with `revokedAt` set.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`files.revokeLink()`](https://openemail.uk/docs/sdk/reference/files#revokeLink); CLI [`openemail files revoke-link`](https://openemail.uk/docs/cli/reference/files#files-revoke-link); MCP [`revokeFileLink`](https://openemail.uk/docs/mcp/tools/files#revokeFileLink).

### Objects

#### `File`

`object`

- `object` (`string`, one of `"file"`)
- `id` (`string`): `file_` and 24 hex.
- `filename` (`string`)
- `mimeType` (`string`)
- `sizeBytes` (`integer`)
- `direction` (`string`, one of `"inbound"`, `"outbound"`, `"uploaded"`): `inbound` for a file that arrived, `outbound` for one that was sent, `uploaded` for one added on the Files page or with `POST /files`.
- `threadId` (`string`, nullable)
- `messageId` (`string`, nullable)
- `deliveredTo` (`string`, nullable): The address it came to, lower-cased. An upload carries the first address its uploader was limited to, and none when the uploader was not limited, which the app shows as the whole workspace. A key limited to some addresses sees only files whose `deliveredTo` it holds, so a file with none recorded reaches only an unrestricted key.
- `usage` (`string`, nullable, one of `"received"`, `"sent"`, `"linked"`, `"scheduled"`): Why the file is kept, or null when it can be deleted. `received` and `sent` are files that came in or went out on a message, and they stay with it. `linked` is an upload that went out as a download link that still works, and `scheduled` is one attached to a message that has not gone out yet.
- `deletable` (`boolean`): Whether `DELETE /files/{id}` will take it. True only for an upload that nothing depends on, which is when `usage` is null.
- `visibility` (`string`, one of `"public"`, `"private"`): `public` while at least one download link to the file works: a link made with `POST /files/{id}/links`, or the link a sent message carries for it. `private` otherwise, and every upload starts private.
- `publicUrl` (`string`, nullable): A working public download link, on the domain's own files host when one is set up and on ours otherwise. A published link is preferred over a link from a sent message. Null while the file is private.
- `createdAt` (`string`, format `date-time`)

#### `FileBatchDelete`

`object`

- `object` (`string`, one of `"file_batch_delete"`)
- `deleted` (`string[]`): The ids this call deleted.
- `kept` (`object[]`): Files something depends on. Nothing happened to them.
  - `id` (`string`)
  - `filename` (`string`)
  - `usage` (`string`, one of `"received"`, `"sent"`, `"linked"`, `"scheduled"`): What keeps it, the same value as `usage` on the file.
  - `reason` (`string`): The same in a sentence, such as "It went out as a download link that still works."
- `missing` (`string[]`): Ids that are unknown, already deleted, or outside the addresses a narrowed key holds.

#### `FileLink`

`object`

- `object` (`string`, one of `"file_link"`)
- `id` (`string`): `fshr_` and 24 hex.
- `fileId` (`string`, nullable)
- `filename` (`string`)
- `mimeType` (`string`)
- `sizeBytes` (`integer`)
- `host` (`string`, nullable): The files domain the link was served from, or null for the default host.
- `url` (`string`): The link: as it went out in the message, or as `POST /files/{id}/links` published it.
- `published` (`boolean`): True for a link made with `POST /files/{id}/links`. Such a link does not stop the file being deleted, and deleting the file revokes it. A link that went out in a message is false and keeps an upload from being deleted while it works.
- `downloads` (`integer`): Fetches by a person. Scanners and link previewers are left out.
- `lastDownloadAt` (`string`, nullable, format `date-time`)
- `revokedAt` (`string`, nullable, format `date-time`): When the link stopped working, or null while it still works. An upload with a link that still works cannot be deleted.
- `createdAt` (`string`, format `date-time`)

#### `FileLinkList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`FileLink[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `FileLinkRevocation`

`object`

- `object` (`string`, required, one of `"file_link_revocation"`)
- `fileId` (`string`, required)
- `revoked` (`integer`, required, at least 0): How many links were still working and now are not. Links revoked earlier are not counted.

#### `FileList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`File[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `FileStats`

`object`

- `object` (`string`, one of `"file_stats"`)
- `totals` (`object`): Every file counted here, however it came.
  - `files` (`integer`)
  - `bytes` (`integer`): Their sizes added up.
- `received` (`object`): Files that came in on a message.
  - `files` (`integer`)
  - `bytes` (`integer`): Their sizes added up.
- `sent` (`object`): Files that went out on a message.
  - `files` (`integer`)
  - `bytes` (`integer`): Their sizes added up.
- `uploaded` (`object`): Files uploaded on the Files page or with `POST /files`.
  - `files` (`integer`)
  - `bytes` (`integer`): Their sizes added up.
- `types` (`object[]`): The 8 types taking the most bytes, largest first.
  - `mimeType` (`string`)
  - `files` (`integer`)
  - `bytes` (`integer`)
- `byDay` (`object[]`): Files added on each of the last 30 days in UTC, today included, oldest first. A day on which none arrived has no entry, so a chart must fill the gaps.
  - `day` (`string`, format `date`): `YYYY-MM-DD`.
  - `files` (`integer`)
  - `bytes` (`integer`)
- `addresses` (`object[]`): The 6 addresses holding the most bytes, largest first. A file uploaded for the whole workspace has no address, so it counts in the totals and never here.
  - `address` (`string`)
  - `files` (`integer`)
  - `bytes` (`integer`)
- `links` (`object`): The download links that still work.
  - `shares` (`integer`): How many links.
  - `downloads` (`integer`): Fetches by a person across all of them. Scanners and link previewers are left out.
  - `lastDownloadAt` (`string`, nullable, format `date-time`): The latest fetch, or null when none was ever fetched.
