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

# Document Upload Guide

> Complete guide to the 3-step document upload workflow

## Overview

Documents are uploaded via short-lived presigned URLs, then registered with the API as case documents. This guide walks through each step.

## The 3-Step Upload Flow

```mermaid theme={null}
sequenceDiagram
    participant Client
    participant API as Enterprise API v2
    participant Storage

    Client->>API: 1. POST /sign-upload
    API->>Client: upload_id + presigned URL
    Client->>Storage: 2. PUT (direct upload)
    Storage->>Client: 200 OK
    Client->>API: 3. POST /documents (create record)
    API->>Client: Document created
```

## Step 1: Request Presigned Upload URL

Request a temporary upload URL from the Enterprise API v2:

```bash theme={null}
curl -X POST https://enterprise.tavrn.ai/v2/cases/case_abc123/sign-upload \
  -H "Authorization: Bearer tavrn_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "medical_record.pdf",
    "file_type": "application/pdf"
  }'
```

```python theme={null}
response = requests.post(
    f"{BASE_URL}/cases/{case_id}/sign-upload",
    headers=headers,
    json={
        "file_name": "medical_record.pdf",
        "file_type": "application/pdf"
    }
)

upload_data = response.json()
upload_id = upload_data["upload_id"]
upload_url = upload_data["upload_url"]
expires_at = upload_data["expires_at"]
```

**Request Body:**

| Field | Type | Required | Description |
| - | - | - | - |
| `file_name` | string | Yes | Original file name (e.g., "medical\_record.pdf") |
| `file_type` | string | Yes | MIME type (e.g., "application/pdf") |

**Response:**

```json theme={null}
{
  "upload_id": "upload_xyz789",
  "upload_url": "https://s3.amazonaws.com/...",
  "expires_at": "2025-02-01T13:34:56Z"
}
```

| Field | Type | Description |
| - | - | - |
| `upload_id` | string | Temporary identifier (valid for 1 hour) |
| `upload_url` | string | Upload URL (valid for 15 minutes) |
| `expires_at` | string | Upload identifier expiration time (ISO 8601, one hour after signing); the PUT URL expires after 15 minutes |

<Warning>
  The presigned URL expires after **15 minutes**. Complete Step 2 before expiration or request a new URL.
</Warning>

## Step 2: Upload the File

Upload your file using the presigned URL:

```bash theme={null}
curl -X PUT "https://s3.amazonaws.com/..." \
  -H "Content-Type: application/pdf" \
  --data-binary @medical_record.pdf
```

```python theme={null}
# Upload file
with open("medical_record.pdf", "rb") as f:
    upload_response = requests.put(
        upload_url,
        data=f,
        headers={"Content-Type": "application/pdf"}
    )

if upload_response.status_code != 200:
    raise Exception(f"S3 upload failed: {upload_response.status_code}")
```

```javascript theme={null}
// Browser upload (with file input)
const file = document.querySelector('input[type="file"]').files[0];

const uploadResponse = await fetch(uploadUrl, {
  method: 'PUT',
  body: file,
  headers: {
    'Content-Type': file.type
  }
});

if (!uploadResponse.ok) {
  throw new Error(`S3 upload failed: ${uploadResponse.status}`);
}
```

**Important Notes:**

* Use **PUT** method (not POST)
* Set `Content-Type` header matching the file type from Step 1
* Send raw binary file data (not multipart/form-data)
* Upload must complete within 15 minutes

## Step 3: Create Document Record

After a successful upload, create the document record:

```bash theme={null}
curl -X POST https://enterprise.tavrn.ai/v2/cases/case_abc123/documents \
  -H "Authorization: Bearer tavrn_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "upload_id": "upload_xyz789",
    "path": "Example_General_Hospital"
  }'
```

```python theme={null}
document = requests.post(
    f"{BASE_URL}/cases/{case_id}/documents",
    headers=headers,
    json={
        "upload_id": upload_id,
        "path": "Example_General_Hospital"
    }
).json()

document_id = document["id"]
```

**Request Body:**

| Field | Type | Required | Description |
| - | - | - | - |
| `upload_id` | string | Yes | Upload ID from Step 1 |
| `path` | string | No | Virtual path for organization (e.g., "Example\_General\_Hospital") |

**Response:**

```json theme={null}
{
  "id": "doc_abc123",
  "case_id": "case_abc123",
  "file_name": "medical_record.pdf",
  "file_size": 1234567,
  "file_type": "application/pdf",
  "path": "Example_General_Hospital",
  "archived": false,
  "created_at": "2025-02-01T12:45:00Z",
  "updated_at": "2025-02-01T12:45:00Z"
}
```

<Note>
  The `upload_id` expires after **1 hour**. Complete Step 3 within this window or the upload will be discarded.
</Note>

## Complete Example

### Python

```python theme={null}
import requests
import os

BASE_URL = "https://enterprise.tavrn.ai/v2"
API_KEY = os.environ["TAVRN_API_KEY"]

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

def upload_document(case_id, file_path, virtual_path=None):
    """Upload a document using the 3-step flow"""

    file_name = os.path.basename(file_path)
    file_size = os.path.getsize(file_path)

    # Determine MIME type
    if file_path.endswith('.pdf'):
        file_type = 'application/pdf'
    elif file_path.endswith('.jpg') or file_path.endswith('.jpeg'):
        file_type = 'image/jpeg'
    elif file_path.endswith('.png'):
        file_type = 'image/png'
    else:
        file_type = 'application/octet-stream'

    # Step 1: Request presigned URL
    print(f"Step 1: Requesting presigned URL for {file_name}")
    sign_response = requests.post(
        f"{BASE_URL}/cases/{case_id}/sign-upload",
        headers=headers,
        json={
            "file_name": file_name,
            "file_type": file_type
        }
    )
    sign_response.raise_for_status()
    upload_data = sign_response.json()

    # Step 2: Upload to S3
    print(f"Step 2: Uploading {file_name} to S3 ({file_size} bytes)")
    with open(file_path, 'rb') as f:
        s3_response = requests.put(
            upload_data["upload_url"],
            data=f,
            headers={"Content-Type": file_type}
        )
    s3_response.raise_for_status()

    # Step 3: Create document record
    print(f"Step 3: Creating document record")
    create_response = requests.post(
        f"{BASE_URL}/cases/{case_id}/documents",
        headers=headers,
        json={
            "upload_id": upload_data["upload_id"],
            "path": virtual_path
        }
    )
    create_response.raise_for_status()
    document = create_response.json()

    print(f"✓ Document created: {document['id']}")
    return document

# Usage
case_id = "case_abc123"
doc = upload_document(
    case_id=case_id,
    file_path="./records/medical_record.pdf",
    virtual_path="Example_General_Hospital"
)
```

### Node.js

```javascript theme={null}
const fs = require('fs');
const fetch = require('node-fetch');

const BASE_URL = 'https://enterprise.tavrn.ai/v2';
const API_KEY = process.env.TAVRN_API_KEY;

async function uploadDocument(caseId, filePath, virtualPath = null) {
  const fileName = filePath.split('/').pop();
  const fileBuffer = fs.readFileSync(filePath);
  const fileType = 'application/pdf'; // Adjust based on file type

  const headers = {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  };

  // Step 1: Request presigned URL
  console.log(`Step 1: Requesting presigned URL for ${fileName}`);
  const signResponse = await fetch(`${BASE_URL}/cases/${caseId}/sign-upload`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      file_name: fileName,
      file_type: fileType
    })
  });
  const uploadData = await signResponse.json();

  // Step 2: Upload to S3
  console.log(`Step 2: Uploading ${fileName} to S3`);
  const s3Response = await fetch(uploadData.upload_url, {
    method: 'PUT',
    body: fileBuffer,
    headers: {
      'Content-Type': fileType
    }
  });

  if (!s3Response.ok) {
    throw new Error(`S3 upload failed: ${s3Response.status}`);
  }

  // Step 3: Create document record
  console.log(`Step 3: Creating document record`);
  const createResponse = await fetch(`${BASE_URL}/cases/${caseId}/documents`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      upload_id: uploadData.upload_id,
      path: virtualPath
    })
  });
  const document = await createResponse.json();

  console.log(`✓ Document created: ${document.id}`);
  return document;
}

// Usage
uploadDocument(
  'case_abc123',
  './records/medical_record.pdf',
  'Example_General_Hospital'
);
```

## Batch Upload

For uploading multiple files:

```python theme={null}
def upload_multiple_documents(case_id, file_paths, virtual_path=None):
    """Upload multiple documents in sequence"""
    documents = []
    failed = []

    for file_path in file_paths:
        try:
            doc = upload_document(case_id, file_path, virtual_path)
            documents.append(doc)
            print(f"✓ {file_path}")
        except Exception as e:
            failed.append({"file": file_path, "error": str(e)})
            print(f"✗ {file_path}: {e}")

    return {
        "succeeded": documents,
        "failed": failed
    }

# Usage
results = upload_multiple_documents(
    case_id="case_abc123",
    file_paths=[
        "./records/er_visit.pdf",
        "./records/mri_report.pdf",
        "./records/followup_notes.pdf"
    ],
    virtual_path="Example_General_Hospital"
)

print(f"\nSucceeded: {len(results['succeeded'])}")
print(f"Failed: {len(results['failed'])}")
```

## File Constraints

| Constraint | Limit |
| - | - |
| **Maximum file size** | 5 GB per file |
| **Upload URL expiry** | 15 minutes |
| **Upload ID expiry** | 1 hour |
| **Supported formats** | PDF, TIFF, JPG, PNG |

## Error Handling

### Common Errors

#### Upload URL Expired

```json theme={null}
{
  "error": {
    "code": "upload_expired",
    "message": "Upload URL has expired"
  }
}
```

**Solution**: Request a new presigned URL (Step 1).

#### Upload ID Not Found

```json theme={null}
{
  "error": {
    "code": "resource_not_found",
    "message": "Upload ID not found or expired"
  }
}
```

**Causes:**

* Upload ID expired (>1 hour old)
* File was never uploaded to S3 (Step 2 failed)
* Upload ID already used

**Solution**: Start over from Step 1.

#### File Not Uploaded

```json theme={null}
{
  "error": {
    "code": "file_not_found",
    "message": "File not found at upload location"
  }
}
```

**Cause**: Step 2 (S3 upload) failed or was not completed.

**Solution**: Retry Step 2 with the same upload URL (if not expired).

## Best Practices

### 1. Check File Size Before Upload

```python theme={null}
MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024  # 5 GB

file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE:
    raise ValueError(f"File too large: {file_size} bytes (max: {MAX_FILE_SIZE})")
```

### 2. Handle Timeouts

```python theme={null}
import requests

try:
    response = requests.put(
        upload_url,
        data=file_data,
        timeout=300  # 5 minute timeout
    )
except requests.exceptions.Timeout:
    print("Upload timed out, retrying...")
    # Retry logic here
```

### 3. Verify Upload Success

```python theme={null}
# Check S3 upload response
if s3_response.status_code != 200:
    raise Exception(f"S3 upload failed with status {s3_response.status_code}")

# Verify document creation
if 'id' not in document:
    raise Exception("Document creation failed")
```

### 4. Use Virtual Paths for Organization

Organize documents by provider or document type:

```python theme={null}
# By provider
upload_document(case_id, "er_visit.pdf", "Example_General_Hospital")
upload_document(case_id, "consult.pdf", "Orthopedic_Clinic")

# By type
upload_document(case_id, "bill_1.pdf", "Billing")
upload_document(case_id, "xray.pdf", "Imaging")
```

### 5. Clean Up on Failure

```python theme={null}
def safe_upload(case_id, file_path, virtual_path=None):
    upload_id = None

    try:
        # Step 1
        upload_data = sign_upload(case_id, file_path)
        upload_id = upload_data["upload_id"]

        # Step 2
        upload_to_s3(upload_data["upload_url"], file_path)

        # Step 3
        return create_document(case_id, upload_id, virtual_path)

    except Exception as e:
        # Log failure
        print(f"Upload failed: {e}")

        # Clean up (upload_id will expire automatically)
        raise
```

## Security Considerations

1. **Never expose upload URLs** - They grant write access for the file you signed
2. **Don't reuse upload IDs** - Each upload requires a fresh `upload_id`
3. **Validate file types** - Ensure uploaded files match the `file_type` you signed for

## Next Steps

* [Upload a retrieval authorization form](/api-reference/v2/endpoint/retrieval-requests/sign-upload)
* [Request presigned upload URL](/api-reference/v2/endpoint/cases/sign-upload)
