# Files

Upload the references a generation works from. Returns file ids you pass in a feature's args. There is no delete here and nothing to clean up: an upload is removed 24 hours after it lands, whether or not a generation used it. To remove a finished result early, delete the generation.

Base URL: https://api.neurall.io. Authenticate with `Authorization: nl-YOUR_API_KEY`.

### Upload Files

`POST /v3/files/upload`

Turns a local file into a file id: `args` fields like `image` and `audio` take these ids, never a URL or base64. Call this first, then generate.

Send `multipart/form-data` with a `files` field: up to 10 files, 30 MB each; images, audio, video or documents (PDF, ZIP, text, JSON). Uploading is permissive on purpose; each feature checks its own limits at generation time, so see that feature's docs for what it accepts.

An upload is deleted 24 hours after it lands. Keep the original on your side and upload it again when you need it later.

```bash
curl -X POST "https://api.neurall.io/v3/files/upload" \
  -H "Authorization: nl-YOUR_API_KEY" \
  -F "files=@/path/to/file"
```

**Body**

- `files` (file[], required): One or more files, all under the same `files` field name. Up to 10 per request, 30 MB each; over either limit answers 400.

**Responses**

- `200`: The uploaded file ids
  - `files` (object[]): One entry per uploaded file, in submission order.
  - `files[].id` (string): The file id to pass in a feature's args.
  - `files[].originalName` (string)
  - `files[].size` (integer): Bytes received.

  Example:

  ```json
  {
    "files": [
      {
        "id": "f_9c21e0b4",
        "originalName": "portrait.jpg",
        "size": 734112
      }
    ]
  }
  ```
- `401`: Unauthorized

### Create an Upload URL

`POST /v3/files/upload-url`

Both routes produce the same file id; this one is for callers the multipart route shuts out. The id comes first and the bytes go second, straight to storage, over a signed URL that carries its own credential for 15 minutes. No Authorization header travels with the bytes, so an agent on MCP (whose key lives in its client config, not its shell) or a client you would rather not hand your key can finish the upload. If you can POST the file, use /v3/files/upload instead.

The multipart route's limits apply (30 MB, same accepted types). Both echoed headers are signed: a PUT that changes or omits either is refused by storage with a 403.

The id means nothing until the PUT answers 200, then behaves exactly like a multipart id, 24 hour expiry included.

**Step 1: Ask for the URL**

POST the file's exact size in bytes and its media type. Nothing is uploaded yet; you are reserving an id and getting a signed address for the bytes.

```bash
curl -X POST "https://api.neurall.io/v3/files/upload-url" \
  -H "Authorization: nl-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "bytes": 734112, "contentType": "image/jpeg" }'
```

It answers:

```json
{
  "file": {
    "id": "f_51ac9d20"
  },
  "uploadUrl": "https://s3.amazonaws.com/neurall-uploads/f_51ac9d20?X-Amz-Signature=…",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/jpeg",
    "Content-Length": "734112"
  },
  "expiresIn": 900
}
```

**Step 2: PUT the bytes to uploadUrl**

Send the file to `uploadUrl` with exactly the echoed headers and no Authorization header (curl derives Content-Length from the file itself). A 200 with an empty body means the id is live. The URL is good for 15 minutes; past that, start over at step 1.

```bash
curl -X PUT "https://s3.amazonaws.com/neurall-uploads/f_51ac9d20?X-Amz-Signature=…" \
  -H "Content-Type: image/jpeg" \
  --data-binary @portrait.jpg
```

**Step 3: Use the id like any upload**

The id now behaves exactly like one from POST /v3/files/upload: pass it in a feature's args, and it expires 24 hours after it landed.

```bash
curl -X POST "https://api.neurall.io/v3/generations" \
  -H "Authorization: nl-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "feature": "image/self-portrait",
    "args": {
      "image": "f_51ac9d20",
      "prompt": "Cinematic studio portrait, moody side light."
    }
  }'
```

It answers:

```json
{
  "generation": {
    "id": "9f2c1d84-6a3b-4f21-9c77-2f0a1b8e5d43",
    "feature": "image/self-portrait",
    "kind": "image",
    "status": "pending"
  }
}
```

**Body**

- `bytes` (integer, required): The file's exact size in bytes. Signed into the URL, so the upload must send precisely this many. Over 30 MB answers 413.
- `contentType` (string, required): The file's media type, e.g. `image/jpeg`. Also signed, and stored on the object: promoting an upload into a saved subject or product copies it onto the permanent asset. Must be an accepted type (image, audio, video, or PDF/ZIP/text/JSON documents) or the request answers 415.

**Responses**

- `200`: The file id and where to PUT the bytes
  - `file` (object)
  - `file.id` (string)
  - `uploadUrl` (string)
  - `method` (string)
  - `headers` (object): The exact headers the PUT must carry.
  - `expiresIn` (integer): Seconds the URL stays valid.

  Example:

  ```json
  {
    "file": {
      "id": "f_51ac9d20"
    },
    "uploadUrl": "https://s3.amazonaws.com/neurall-uploads/f_51ac9d20?X-Amz-Signature=…",
    "method": "PUT",
    "headers": {
      "Content-Type": "image/jpeg",
      "Content-Length": "734112"
    },
    "expiresIn": 900
  }
  ```
- `400`: Missing or malformed bytes or contentType
- `401`: Unauthorized
- `413`: The file is over 30 MB
