---
title: "Upload a file"
method: POST
path: "/file"
tags: ["Files"]
---

# Upload a file

`POST /file`

## Request

- Base URL: `https://api.elaichi.ai`
- URL: `https://api.elaichi.ai/file`
- Auth: HTTP bearer

Uploads a file in one request, for a backend that holds a session or an API token, and for the Elaichi app’s Files screen (which uses it for **Upload**). Two forms, told apart by `X-Filename` (or `?filename=`):

- **With it, the body is the file** (`Content-Type` is the declared type, `Content-Length` is required, at most 95 MiB). Streamed straight into storage with the size counted on the bytes actually read, so a header that lies is caught mid-stream and nothing is kept. `411` `upload_length_required` without a length, `413` `upload_too_large` over the limit. A raw upload of a JSON file sends `X-Filename`, so it is never taken for the second form.
- **Without it, the body is JSON** `{ filename, content, encoding: utf8 | base64, mime? }` for content the client wrote itself, at most 1 MiB of decoded content (`413` `inline_content_too_large`; the body is never buffered past that, whatever it declares).

Either way the answer is `201 { file }`, the file as `GET /file/{id}` reads it, owned by the caller and kept for 7 days. The stored type is read from the bytes, never taken from the declaration. `origin` is `upload` for a person’s session, `api` for a token. Audited as `file.uploaded`.

## Query parameters

- `filename` string

## Headers

- `X-Organization-Id` string
- `X-Filename` string

## Request body

- object
  - `filename` string, required
  - `content` string, required
  - `encoding` 'utf8' | 'base64'
  - `mime` string

## Response `201`

Created

- object
  - `file` object
    - `file_id` string
    - `status` 'pending' | 'ready' | 'expired': `pending`: an upload link you made is waiting for its bytes (not a file yet: no type, size or URL). `ready`: a file you can open. `expired`: your link ran out unused, or the file it filled is gone.
    - `filename` string
    - `mime` string, nullable
    - `size_bytes` integer, nullable
    - `expires_at` string, date-time
    - `url` string, nullable: The Elaichi app’s download page, for a `ready` file.
    - `origin` 'tool' | 'upload' | 'request' | 'inline' | 'api' | 'url_import' | 'web_fetch' | 'export': How the bytes got here: `tool` (a connector tool returned them), `upload` (a person chose it on the Files screen), `request` (a person handed it to an app that asked, through an upload request; `origin_host` is the app), `inline` (content an assistant wrote with `file.create`), `api` (a backend sent it to `POST /file` with a token), `url_import`, `web_fetch` (fetched from the web), `export` (produced by an export). Provenance for wording (“Uploaded by Dana”), never an access gate.
    - `origin_host` string, nullable: The web host an import or web fetch read from, or the asking app’s name for origin `request`; null otherwise.
    - `max_bytes` integer, nullable: The link’s size limit while `pending`.
    - `access` 'owner' | 'view' | 'null', nullable
    - `can_open` boolean
    - `can_delete` boolean
    - `can_share` boolean
    - `can_see_shares` boolean
    - `can_revoke_share` boolean

## Other responses

- `400`: Bad request
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not found
- `429`: Rate limited

---

[API](https://skmtc.dev/elaichi/apis/elaichi-api.md) · [All operations](https://skmtc.dev/elaichi/apis/elaichi-api/llms.txt) · [OpenAPI document](https://skmtc.dev/elaichi/apis/elaichi-api/revisions/b22cfb7b6d15?raw)
