> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pramaan.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Files

> Upload a file to use as input, and download the files capabilities make.

## Files you get back

Capabilities that make a file (`audio.speech`, `image.generate`, `video.generate`) return it as
a link on this API's own host, with an id and an expiry:

```json theme={null}
{
  "audioUrl": "https://api.pramaan.dev/files/file_01J...?e=...&s=...",
  "fileId": "file_01J...",
  "expiresAt": "2026-10-15T00:00:00.000Z"
}
```

* The link works without an API key, for anyone who has it, until `expiresAt` (7 days after the
  call). Download the file if you need it for longer.
* `GET /v1/files/{id}` returns a fresh link. `DELETE /v1/files/{id}` removes the file at once.
* A link can be downloaded about 20 times. After that it answers `429`; get the file once and
  serve it from your own storage if many people will fetch it.
* If the file cannot be stored, the call fails with `502 upstream_unavailable` and you are not
  charged. Retry it.

## Upload a file

Send the file as the request body, with its media type in `Content-Type` and its kind in the
query:

```bash theme={null}
curl "https://api.pramaan.dev/v1/files?kind=audio" \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @call.mp3
```

```json theme={null}
HTTP/1.1 201 Created
{ "fileId": "file_01J...", "kind": "audio", "origin": "upload", "contentType": "audio/mpeg",
  "sizeBytes": 482113, "createdAt": "...", "expiresAt": "...", "url": "https://api.pramaan.dev/files/..." }
```

| Kind | Largest file |
| - | - |
| `image` | 20 MB |
| `audio`, `document`, `data` | 100 MB |
| `video` | 500 MB |

* Uploading is free. You need a credit balance above zero.
* An upload is kept for 7 days. You can hold up to 1 GB of uploads at a time; over that the
  upload is refused with `402 spend_cap_reached` until you delete files or they expire.
* The media type must match the file's own bytes, or the upload is refused with `400`.

## Upload directly (required above 90 MB)

The request-body endpoint above accepts at most 90 MB. The per-kind limits in the table still
apply. Larger files go directly to storage, without passing through the API.

The SDK chooses the flow for you (a browser `File` is also a `Blob`):

```typescript theme={null}
const stored = await p.files.upload(file, { kind: "video", contentType: "video/mp4" });
// Pass stored.fileId to a capability.
```

For HTTP clients:

1. `POST /v1/files/uploads` with API authorization, `Content-Type: application/json`, and
   `{ "kind": "video", "contentType": "video/mp4", "sizeBytes": 120000000 }`.
   It returns `201` with `{ "fileId": "file_...", "uploadUrl": "...", "headers": { ... },
   "expiresAt": "..." }`. Size, type, credit and available storage are checked before the link
   is issued; pending uploads count toward storage quota, and a failed or deleted direct upload
   keeps counting for one hour after it was started.
2. Send one `PUT` to `uploadUrl` with the file as the body and **all returned headers**.
   Do not send your API key to this URL. The link expires in 15 minutes and accepts exactly
   the declared size and type. It only creates the object once, so repeating a successful PUT
   is refused. In browsers, omit the returned `content-length` header: fetch sets it from
   the Blob automatically. Browser uploads require the bucket owner to allow the app's origin.
3. `POST /v1/files/{fileId}/finish` with API authorization. It returns the usual file facts
   after checking the stored size and file contents. Until then the file cannot be used.
   If the bytes are not there yet, or storage does not answer, the upload stays pending and you
   can call finish again. A failed check deletes the object and returns an error explaining how
   to start again.

Treat the upload URL as a temporary secret. Keep it out of logs. If a PUT fails or the link
expires, start a new upload. Unfinished uploads are eligible for the hourly pending-file sweep
after one hour. Retention deletion must be enabled for the sweep to remove bytes.

## Upload a large object in parts

For an object larger than one `storage.object.put` takes (25 MB), or of a type the list above does
not accept, use a multipart upload to your own bucket. Each chunk is uploaded as a **part file**:
raw bytes, sent as `application/octet-stream` with `kind=part`.

```bash theme={null}
# 1. Start the upload. Keep the `ref` it returns.
curl https://api.pramaan.dev/v1/capabilities/storage.upload.create \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: report-create" \
  -d '{ "bucket": "documents", "path": "report.pdf", "contentType": "application/pdf" }'

# 2. Cut the file into chunks of 5 MiB to 8 MiB (the last one may be smaller).
split -b 8388608 report.pdf chunk-

# 3. For each chunk: upload it as a part file, then store it as part 1, 2, 3, ...
curl "https://api.pramaan.dev/v1/files?kind=part" \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @chunk-aa
curl https://api.pramaan.dev/v1/capabilities/storage.upload.part.put \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: report-part-1" \
  -d '{ "ref": "ref_...", "partNumber": 1, "fileId": "file_01J..." }'

# 4. Finish. `storage.upload.abort` gives the upload up instead.
curl https://api.pramaan.dev/v1/capabilities/storage.upload.complete \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: report-complete" \
  -d '{ "ref": "ref_..." }'
```

* A part file can only be passed to `storage.upload.part.put`. It has no link, cannot be
  downloaded, and every other capability refuses it with `400 invalid_request`.
* One part is at most 8 MiB. The parts of one upload may total 1 GiB. Part files count toward your
  1 GB of uploads until they are deleted.
* Part files are deleted when the upload completes or is aborted, and after 7 days otherwise.
  An upload near 1 GB does not fit in your 1 GB of uploads all at once: delete each part file
  (`DELETE /v1/files/{id}`) once its `storage.upload.part.put` has succeeded.
* The finished object is assembled in your bucket exactly as you sent it. Its content is not
  checked.
* Without `kind=part`, an `application/octet-stream` upload is refused as before.

## Use an uploaded file

Pass the `fileId` where a capability accepts one, in place of a public link:

```ts theme={null}
const transcript = await p.audio.transcribe({ fileId: "file_01J..." });
const parsed = await p.document.parse({ fileId: "file_01K..." });
```

| Capability | Field in place of `fileId` |
| - | - |
| `audio.transcribe`, `audio.transcription.create` | `audioUrl` |
| `document.parse`, `document.parsing.create`, `document.extract` | `url` or `content` |

Send exactly one of them. The audio capabilities take `audio` files; the document capabilities
take `document` and `image` files (a scan or a photo of a page). A `fileId` returned by a capability (a generated audio file, say) can
be passed on the same way.

## Errors

| Status | Code | When |
| - | - | - |
| `404` | `not_found` | No such file in your account, or it has expired ("File not found.") |
| `400` | `invalid_request` | The file is the wrong kind for the capability, or the media type does not match the bytes |
| `402` | `spend_cap_reached` | No credit, or the 1 GB of uploads is full |

See [errors](https://docs.pramaan.dev/guides/errors.md) for the full list.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.