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

# Get Upload URL

> Generate a pre-signed URL to upload a file directly to S3

Generate a pre-signed S3 URL for uploading a file to a recording. Supports audio, image, and video files. Upload the file directly to S3 using the returned URL. After all files are uploaded, call the [Complete Upload](/api-reference/uploads/complete) endpoint.

## Authentication

This endpoint accepts two authentication methods:

| Auth                            | Use case                                          |
| ------------------------------- | ------------------------------------------------- |
| [API key](/authentication)      | Backend uploading on behalf of devices (BLE sync) |
| [Device token](/authentication) | 4G/WiFi devices uploading directly                |

<RequestExample>
  ```bash cURL (audio) theme={null}
  curl -X POST https://api.bota.dev/v1/recordings/rec_abc123/upload-url \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "type": "audio",
      "content_type": "audio/opus",
      "file_size_bytes": 2048576
    }'
  ```

  ```bash cURL (image) theme={null}
  curl -X POST https://api.bota.dev/v1/recordings/rec_abc123/upload-url \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "type": "image",
      "content_type": "image/jpeg",
      "file_size_bytes": 245000,
      "captured_at": "2025-01-15T09:05:30Z"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.bota.dev/v1/recordings/rec_abc123/upload-url', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'audio',
      content_type: 'audio/opus',
      file_size_bytes: 2048576,
    }),
  });

  const { media_id, upload_url, headers, expires_at } = await response.json();
  ```

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

  response = requests.post(
      'https://api.bota.dev/v1/recordings/rec_abc123/upload-url',
      headers={
          'Authorization': 'Bearer sk_live_...',
          'Content-Type': 'application/json',
      },
      json={
          'type': 'audio',
          'content_type': 'audio/opus',
          'file_size_bytes': 2048576,
      },
  )

  result = response.json()
  upload_url = result['upload_url']
  ```
</RequestExample>

## Path Parameters

<ParamField path="id" type="string" required>
  The recording's unique identifier (e.g., `rec_abc123`).
</ParamField>

## Request Body

<ParamField body="type" type="string" required>
  Media type: `audio`, `image`, or `video`.
</ParamField>

<ParamField body="content_type" type="string">
  MIME type of the file. Defaults based on `type`:

  * **Audio** (default: `audio/opus`): `audio/opus`, `audio/wav`, `audio/mpeg`, `audio/mp4`, `audio/webm`, `audio/ogg`, `audio/flac`
  * **Image**: `image/jpeg`, `image/png`
  * **Video**: `video/mp4`
</ParamField>

<ParamField body="file_size_bytes" type="integer">
  Expected file size in bytes. Used for validation.
</ParamField>

<ParamField body="captured_at" type="string">
  ISO 8601 timestamp of when the media was captured. Used to align images/video with the recording timeline. Optional for audio.
</ParamField>

## Response

Returns a media record ID, a pre-signed S3 upload URL, required headers, and an expiration timestamp.

<ResponseExample>
  ```json 200 theme={null}
  {
    "media_id": "med_001",
    "upload_url": "https://bota-uploads.s3.amazonaws.com/proj_xxx/rec_abc123/med_001/audio.opus?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
    "expires_at": "2025-01-15T12:00:00Z",
    "headers": {
      "Content-Type": "audio/opus"
    }
  }
  ```

  ```json 404 theme={null}
  {
    "error": {
      "code": "not_found",
      "message": "Recording not found"
    }
  }
  ```
</ResponseExample>

## Response Fields

| Field        | Type   | Description                                                           |
| ------------ | ------ | --------------------------------------------------------------------- |
| `media_id`   | string | Identifier for the created media record (`med_*`)                     |
| `upload_url` | string | Pre-signed S3 URL for uploading the file via PUT request              |
| `expires_at` | string | ISO 8601 timestamp when the upload URL expires (1 hour from creation) |
| `headers`    | object | HTTP headers to include when uploading to S3 (e.g., `Content-Type`)   |

## Uploading the File

After obtaining the pre-signed URL, upload the file directly to S3:

```bash cURL theme={null}
curl -X PUT "https://bota-uploads.s3.amazonaws.com/..." \
  -H "Content-Type: audio/opus" \
  --data-binary @recording.opus
```

```javascript Node.js theme={null}
const fs = require('fs');

const audioBuffer = fs.readFileSync('recording.opus');

await fetch(upload_url, {
  method: 'PUT',
  headers: {
    'Content-Type': 'audio/opus',
  },
  body: audioBuffer,
});
```

```python Python theme={null}
with open('recording.opus', 'rb') as f:
    audio_data = f.read()

requests.put(
    upload_url,
    headers={'Content-Type': 'audio/opus'},
    data=audio_data,
)
```

## Multi-File Upload

A single recording can have multiple media files. Call this endpoint once per file:

```javascript theme={null}
// 1. Create recording
const recording = await createRecording({ device_id: 'dev_abc123' });

// 2. Upload audio
const audio = await getUploadUrl(recording.id, {
  type: 'audio',
  content_type: 'audio/opus',
});
await uploadToS3(audio.upload_url, audioBuffer, 'audio/opus');

// 3. Upload images captured during the recording
const image1 = await getUploadUrl(recording.id, {
  type: 'image',
  content_type: 'image/jpeg',
  captured_at: '2025-01-15T09:05:30Z',
});
await uploadToS3(image1.upload_url, imageBuffer1, 'image/jpeg');

const image2 = await getUploadUrl(recording.id, {
  type: 'image',
  content_type: 'image/jpeg',
  captured_at: '2025-01-15T09:15:00Z',
});
await uploadToS3(image2.upload_url, imageBuffer2, 'image/jpeg');

// 4. Mark complete — fires event for downstream processing
await completeUpload(recording.id);
```

<Note>
  Upload URLs expire after 1 hour. If expired, request a new URL. After all files are uploaded, call the [Complete Upload](/api-reference/uploads/complete) endpoint to finalize.
</Note>
