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

# MeetStream Guide: Custom S3 Storage

By default, MeetStream stores all bot media — audio, video, transcripts, screenshots, chat logs, and meeting manifests — in its own S3 bucket, and serves them to you via presigned URLs when you call the fetch endpoints.

**Custom S3 Storage** lets you direct MeetStream to upload all of that media directly into **your own S3 bucket** using credentials you provide.

Use cases:
- Compliance or data-residency requirements (your data never leaves your AWS account).
- Direct integration with your own storage pipeline or data lake.
- Full control over file lifecycle, retention, and access policies.

---

## 1) Prerequisites

1. An AWS S3 bucket in the region of your choice.
2. An IAM user (or role) with credentials that grant at least **write access** (`s3:PutObject`) to that bucket. If you also want MeetStream to serve the files back to you via its API, you'll need to additionally grant **read access** (`s3:GetObject`).
3. A MeetStream API key.

### Minimum IAM policy

Replace `your-bucket-name` with your bucket name.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/meetstream/*"
    }
  ]
}
```

If you also want the MeetStream API to serve presigned download URLs from your bucket (i.e. `access_mode: "read_write"`), add `s3:GetObject` to the same statement.

> If you set a custom `prefix` (see below), scope the `Resource` to that prefix instead — e.g. `arn:aws:s3:::your-bucket-name/recordings/meetstream/*`. If you use per-category `prefixes`, add one `Resource` entry per distinct prefix (e.g. `.../recordings/audio/*`, `.../recordings/video/*`, …).

---

## 2) Configure your S3 bucket

```http
PUT /api/v1/admin/configs?config_type=storage
Authorization: Token <YOUR_API_KEY>
Content-Type: application/json
```

```json
{
  "provider": "aws",
  "bucket_name": "your-bucket-name",
  "region": "us-east-1",
  "access_key_id": "AKIA...",
  "secret_key": "...",
  "access_mode": "read_write",
  "prefix": "recordings/meetstream",
  "prefixes": {
    "audio": "recordings/audio",
    "video": "recordings/video",
    "transcript": "transcripts",
    "metadata": "metadata"
  }
}
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `provider` | string | Yes | Storage provider. Only `"aws"` is supported. |
| `bucket_name` | string | Yes | Name of your S3 bucket. |
| `region` | string | Yes | AWS region of the bucket (e.g. `"us-east-1"`). |
| `access_key_id` | string | Yes | AWS access key ID for an IAM user with access to the bucket. |
| `secret_key` | string | Yes | AWS secret access key. |
| `access_mode` | string | No | `"read_write"` (default) or `"write_only"`. See [Access Modes](#3-access-modes). |
| `prefix` | string | No | Base key prefix under which media is stored. Defaults to `"meetstream"`. Acts as the fallback for any artifact category not given its own prefix in `prefixes`. Leading/trailing slashes are stripped and `..` segments are rejected. |
| `prefixes` | object | No | Per-artifact-category prefix overrides. Optional keys: `audio`, `video`, `transcript`, `metadata`. Each lets you store that category under its own top-level path (e.g. audio under `recordings/audio/{bot_id}/...`). Any category omitted falls back to `prefix`. Same normalization and `..` rejection as `prefix`. See [File layout](#4-file-layout-in-your-bucket). |
| `endpoint_url` | string | No | Custom S3-compatible endpoint (e.g. for MinIO or Cloudflare R2). Omit for standard AWS S3. |

### Validation

For `read_write`, MeetStream performs a live `HeadBucket` check against your bucket before saving the configuration. If the credentials are invalid or the bucket doesn't exist, the request will fail with a `400` error.

For `write_only`, MeetStream validates by writing a tiny probe object under **each distinct prefix** you configured (`{prefix}/.write_probe_*`, plus one per unique category prefix). This confirms your credentials can write without requiring read access, and works even if your IAM policy is scoped to those individual prefixes. If any probe write fails, the request returns a `400` error.

### Response

```json
{
  "message": "Storage config saved successfully",
  "provider": "aws",
  "bucket_name": "your-bucket-name",
  "region": "us-east-1",
  "access_mode": "read_write",
  "prefix": "recordings/meetstream",
  "prefixes": {
    "audio": "recordings/audio",
    "video": "recordings/video",
    "transcript": "transcripts",
    "metadata": "metadata"
  }
}
```

> Your `access_key_id` and `secret_key` are stored securely and are **never returned** in any API response.

---

## 3) Access modes

| Mode | MeetStream uploads to your bucket | MeetStream API serves files from your bucket |
|------|----------------------------------|---------------------------------------------|
| `read_write` (default) | Yes | Yes — fetch endpoints return presigned URLs backed by your bucket |
| `write_only` | Yes | No — fetch endpoints return `403` |

Use `write_only` when:
- Your bucket is private and you want to control access yourself (generate your own presigned URLs).
- Your IAM policy only grants write permissions.
- You are piping files into your own processing pipeline and don't need MeetStream to serve them.

> If you start with `write_only` and later upgrade to `read_write`, you can immediately use the fetch endpoints — no re-upload needed.

---

## 4) File layout in your bucket

MeetStream does **not** create a per-bot subfolder. Files are written directly into your configured prefix folder, with the **bot id as a filename prefix** (`{prefix}/{bot_id}_<file>`). This keeps each bot's objects grouped by name while letting many bots share one folder. With the default prefix:

```
meetstream/
  abc123_audio.wav
  abc123_meeting_recording.mp4
  abc123_participants.json
  abc123_chats.json
  abc123_manifest.json
  abc123_screenshots/
    9f2b….png
    9f2b….json
    a1c3….png
    a1c3….json
  def456_audio.wav            ← a second bot, same folder
  def456_manifest.json
```

Or, with `"prefix": "recordings/meetstream"`, the same files live under `recordings/meetstream/abc123_...`.

> Multi-file collections (`screenshots/`, and transcript JSON under `transcription/<provider>/<id>/`) keep a `{bot_id}_<collection>/` subfolder so their many files stay grouped — there is still no bare `{bot_id}/` folder.

### Per-category prefixes

Each file belongs to one of four **artifact categories**, and you can route each category to its own top-level prefix via the `prefixes` object. Any category you don't set inherits the base `prefix`.

| Category | Files | Configured by |
|----------|-------|---------------|
| `audio` | `audio.wav`, `audio.mp3` | `prefixes.audio` |
| `video` | `meeting_recording.mp4` | `prefixes.video` |
| `transcript` | `transcription/.../raw_transcript.json`, `processed_transcript.json` | `prefixes.transcript` |
| `metadata` | `manifest.json`, `participants.json`, `chats.json`, `screenshots/` | `prefixes.metadata` |

For example, with `"prefixes": {"audio": "recordings/audio", "video": "recordings/video", "transcript": "transcripts", "metadata": "metadata"}`:

```
recordings/audio/abc123_audio.wav
recordings/video/abc123_meeting_recording.mp4
transcripts/abc123_transcription/deepgram/<id>/processed_transcript.json
metadata/abc123_manifest.json
metadata/abc123_screenshots/9f2b….png
```

Prefixes isolate MeetStream files from any other objects in your bucket. You can change any prefix at any time; media written before a change remains fetchable at its original location (MeetStream records the exact bucket and key prefix used for each bot). New bots use the updated prefixes.

---

## 5) Fetching media after configuration

Once custom storage is configured, all subsequent bots write their media to your bucket. The MeetStream fetch endpoints work exactly the same as before — you do not need to change how you call them.

### Fetch endpoints and URL behaviour

| Endpoint | Behaviour with `read_write` | Behaviour with `write_only` |
|----------|-----------------------------|-----------------------------|
| `GET /bots/{bot_id}/get_audio` | Presigned URL from your bucket (1-hour expiry) | `403` |
| `GET /bots/{bot_id}/get_video` | Presigned URL from your bucket (1-hour expiry) | `403` |
| `GET /bots/{bot_id}/screenshots` | Paginated list of screenshots with presigned URLs (1-hour expiry). **Not supported for Zoom bots.** | `403` |
| `GET /bots/{bot_id}/screenshots/{id}` | Single screenshot by UUID with presigned URL (1-hour expiry). **Not supported for Zoom bots.** | `403` |
| `GET /bots/{bot_id}?type=chat` | JSON content read from your bucket | `403` |
| `GET /bots/{bot_id}?type=participants` | JSON content read from your bucket | `403` |
| `GET /bots/{bot_id}?type=transcription` | JSON content read from your bucket | `403` |
| `GET /transcript/{transcript_id}/get_transcript` | JSON content read from your bucket | `403` |

The `403` response for `write_only` includes a message indicating where the file lives:

```json
{
  "message": "Media is stored in your S3 bucket, but only write access was granted. Update your storage configuration to read+write to fetch media via the API.",
  "access_mode": "write_only"
}
```

---

## 6) Existing bots are not affected by configuration changes

Each bot's media is pinned to whichever bucket was active when its files were written (after the bot leaves the meeting). Enabling, disabling, or changing custom storage will not affect media from bots that have already been processed — those files remain in the bucket they were originally written to.

| Scenario | Where the files are |
|----------|---------------------|
| Bot processed before custom storage was enabled | Platform default bucket |
| Bot processed after custom storage was enabled | Your custom bucket |
| Custom storage disabled after a bot is processed | Files stay in your custom bucket |

---

## 7) View your current storage configuration

```http
GET /api/v1/admin/configs
Authorization: Token <YOUR_API_KEY>
```

Returns your current storage configuration. Credential fields are never included in the response.

```json
{
  "StorageConfig": {
    "aws": {
      "active": true,
      "bucket_name": "your-bucket-name",
      "region": "us-east-1",
      "access_mode": "read_write",
      "prefix": "recordings/meetstream",
      "prefixes": {
        "audio": "recordings/audio",
        "video": "recordings/video",
        "transcript": "transcripts",
        "metadata": "metadata"
      },
      "configured_at": "2025-01-15T10:00:00Z",
      "last_validated_at": "2025-01-15T10:00:00Z"
    }
  }
}
```

---

## 8) Delete your storage configuration

```http
DELETE /api/v1/admin/configs?key_name=aws
Authorization: Token <YOUR_API_KEY>
```

This removes your credentials and storage configuration. After deletion, new bots will have their media written to the MeetStream platform bucket.

Existing bots whose files were already written to your bucket are not affected — those files are not deleted from your bucket.

---

## 9) Troubleshooting

**Configuration save fails with `400`**
- Confirm the bucket name and region are correct.
- Confirm the IAM credentials have `s3:PutObject` and `s3:GetObject` (for `read_write`) on the bucket.
- Confirm `access_mode` is `"read_write"` if you want live validation.

**Fetch endpoint returns `403` with `"access_mode": "write_only"`**
- Your storage is configured as `write_only`. Either access the file directly from your bucket, or update your configuration to `read_write`.

**Fetch endpoint returns `404` for a bot that was processed**
- The file may not have been written to your bucket due to a permissions error during upload. Check that your IAM credentials are valid and have `s3:PutObject` on `arn:aws:s3:::your-bucket-name/meetstream/*` (or your configured prefix, e.g. `arn:aws:s3:::your-bucket-name/recordings/meetstream/*`).

**Presigned URL returns `403 Forbidden`**
- The URL may have expired — presigned URLs have a short lifetime (see the table in [Section 5](#5-fetching-media-after-configuration)). Call the fetch endpoint again to get fresh URLs.
- Your IAM policy may not include `s3:GetObject`. Update the policy and your storage configuration.

**Bot was processed before I enabled custom storage — files are not in my bucket**
- This is expected. Only bots processed after the configuration was saved write to your bucket. See [Section 6](#6-existing-bots-are-not-affected-by-configuration-changes).