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

# Browser upload grants

> Mint a short-lived token so a browser can upload without your API key

An upload grant lets an untrusted client POST bytes into one workspace. Your
backend mints the grant; the browser redeems it with `X-Upload-Token` instead
of `X-API-Key`. The grant is short-lived and single-use where Redis is on.

<Tabs>
  <Tab title="Python">
    ```python theme={"dark"}
    from ndi_sdk import NdiClient

    with NdiClient() as client:
        workspace = client.workspaces.create(name="browser-uploads")
        grant = client.files.create_upload_grant(
            workspace.workspace_id,
            path="inbox/statement.pdf",
            max_bytes=20_000_000,
        )
        print(grant.upload_url, grant.expires_at)
        # Hand grant.token to the browser. It POSTs multipart to grant.upload_url
        # with header X-Upload-Token: <token>, metadata={"path":"..."}, and file=
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts theme={"dark"}
    import { NdiClient } from "ndi-sdk";

    const client = new NdiClient();
    const workspace = await client.workspaces.create({ name: "browser-uploads" });
    const grant = await client.files.createUploadGrant(workspace.workspace_id, {
      path: "inbox/statement.pdf",
      max_bytes: 20_000_000,
    });
    console.log(grant.upload_url, grant.expires_at);
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    GRANT=$(curl -s -X POST "$NDI_BASE_URL/v1/workspaces/$WS/upload-grants" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"path":"inbox/statement.pdf","max_bytes":20000000}')

    UPLOAD_URL=$(echo "$GRANT" | jq -r '.upload_url')
    TOKEN=$(echo "$GRANT" | jq -r '.token')

    curl -s -X POST "$UPLOAD_URL" \
      -H "X-Upload-Token: $TOKEN" \
      -F 'metadata={"path":"inbox/statement.pdf"}' \
      -F "file=@statement.pdf"
    ```
  </Tab>
</Tabs>

## Constraints

| Field              | Meaning                                                                      |
| ------------------ | ---------------------------------------------------------------------------- |
| `path`             | Pin the destination; omit to let the client choose a workspace-relative path |
| `max_bytes`        | Per-grant cap, tighter than the service-wide limit                           |
| `total_size_bytes` | Exact expected size frozen into the grant                                    |
| `ttl_seconds`      | Lifetime; omit for the server default                                        |

Redeem against `upload_url` only. Do not send your API key to the browser.
After the redeem job succeeds, [ingest](/guides/ingest-reconcile) as usual.
