# Start a file upload

URL: https://www.atmee.ai/docs/v2/api/reference/avatars/createUpload

`POST /v1/uploads`

Gets a file to the Avatar API ahead of the create or replace that uses it,
for clients that cannot send a file in the same request (an MCP tool, a
chat assistant). `kind` is `portrait` (jpg, png or webp, <= 10 MB), `voice`
(a 10-60 s recording: mp3, wav, m4a, aac, ogg, flac, opus or webm, <= 12 MB)
or `knowledge` (pdf, docx, doc, txt, md, html, csv <= 50 MB; audio or video
<= 20 MB). `contentType`, when given, is the only type the upload accepts.

Send the file one of two ways:

- `PUT` it to `uploadUrl` with `uploadHeaders` (a `Content-Type` from
  `allowedTypes` when none was declared), at most `maxBytes`:
  `curl -X PUT -H 'Content-Type: image/png' --data-binary @portrait.png '<uploadUrl>'`.
- Or give a person `uploadPageUrl` to open: a page that shows what is being
  uploaded and how it will be used, takes the file (or, for a voice, records
  one in the browser) and sends it. The link works for 15 minutes
  (`uploadPageExpiresAt`) and until the file has arrived.

Poll `GET /v1/uploads/{uploadId}` until `status` is `uploaded`, then pass
`upload:<uploadId>` where `POST /v1/avatars` (`assets`) or the JSON form of
`PUT /v1/avatars/{avatarId}/appearance|voice|knowledge` takes a URL. An
upload works once and expires an hour after it was created. Requires
`avatars:write`.

## Request body

- `application/json`: `UploadCreateRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `UploadKind` | yes |  |
| `fileName` | string | no | The file's name; a knowledge file is stored and described under it. |
| `contentType` | string | no | The file's type, one this kind takes; the upload then accepts only it. |

## Responses

| Status | Description | Schema |
| --- | --- | --- |
| `201` | The upload, waiting for its file. | `UploadCreateResponse` |
| `400` | invalid_body \| unsupported_content_type (not a type this kind takes) | `ApiErrorResponse` |
| `401` | Missing or invalid API key -- or a user token that is invalid, anonymous, or of a user who may act for no account (invalid_login) | none |
| `403` | insufficient_scope \| account_not_allowed (a user token whose X-Account-Id names an account the user may not act for) | `ApiErrorResponse` |
| `409` | account_ambiguous -- a user token of a user who may act for several accounts, without an `X-Account-Id` header naming one. | `ApiErrorResponse` |
| `422` | Request body failed schema validation (e.g. an unknown kind). | none |
| `429` | upload_limit_reached -- too many unused uploads in this account; use them or let them expire. | `ApiErrorResponse` |
| `503` | upload_unavailable -- this instance has no upload store. | `ApiErrorResponse` |

## `201` response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | string | yes |  |
| `kind` | `UploadKind` | yes |  |
| `status` | string (`pending`) | yes |  |
| `uploadUrl` | string | yes | The signed target to PUT the file's bytes to, before expiresAt. |
| `uploadHeaders` | object | yes | Headers the PUT must send. Without a declared contentType, send a Content-Type from allowedTypes. |
| `maxBytes` | integer | yes | The most bytes the PUT takes. A knowledge upload declared as audio or video gets the media cap (20 MB), one declared as a document or with no type the document cap (50 MB). |
| `allowedTypes` | array of string | yes |  |
| `expiresAt` | string | yes |  |
| `uploadPageUrl` | string | no | A page a person opens to upload the file (or record a voice). Absent where no page is served. |
| `uploadPageExpiresAt` | string | no |  |
