> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://onepin.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/docs/_mcp/server.

# Upload a file

Use an uploaded file as a workflow's script instead of inline text. Uploads use a presigned-URL flow (create → PUT bytes → confirm) and require the `uploads:write` scope.

> **Note**
>
> Categories: `script` (`.txt`, `.pdf`) or `dictionary` (pronunciation reference audio: `.mp3`, `.wav`, `.m4a`, `.ogg`, `.webm`).

#### Python

```python
import httpx
from onepin import OnepinClient

client = OnepinClient()

# 1. create the upload record and get a presigned URL
created = client.uploads.create(filename="script.txt", category="script").data
upload_id, upload_url = created.upload.id, created.upload_url

# 2. PUT the bytes straight to storage (not via the SDK)
with open("script.txt", "rb") as f:
    httpx.put(upload_url, content=f.read()).raise_for_status()

# 3. confirm and bind it to a workflow
confirmed = client.uploads.confirm(
    upload_id, context_type="workflow", context_id=workflow_id
).data
print(confirmed.char_count, confirmed.line_count, confirmed.detected_language)
```

#### CLI

```bash
# create + PUT bytes in one step; prints the upload id
onepin uploads create --file script.txt --category script

onepin uploads confirm "$UPLOAD_ID" --workflow-id "$WORKFLOW_ID"
```

Remove one with `onepin uploads delete "$UPLOAD_ID" --yes`.

## What confirm gives you back

For a `script` `.txt` / `.pdf` upload, confirming fills in three fields worth reading:

| Field               | Meaning                                                                                           |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| `char_count`        | Decoded character count — the sum of the stripped, non-empty lines. This is what you're billed on |
| `line_count`        | Number of non-empty lines                                                                         |
| `detected_language` | The dominant source language the server detected, as a bare code (`en`, `ko`)                     |

`detected_language` is a **bare** language, not a locale — map it to a supported regional locale before you put it in a node config (`en` → `en-us`). It's null for `dictionary` uploads and whenever detection didn't run.

## Related

* [Script](/docs/workflow/script) — pointing a script node at an upload