> This page is for For Developers.

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

# Upload files

> Upload images and files for use across the Whop platform

Upload files to Whop for use in courses, forums, profiles, and more. The SDK provides a simple `upload` method that handles the entire process for you.

**`TypeScript`**

```typescript TypeScript
import fs from 'fs';

const file = await client.files.upload({
  file: fs.readFileSync('./photo.jpg'),
  filename: 'photo.jpg',
});

console.log(file.id); // file_xxxxxxxxxxxxx
console.log(file.url); // URL to access the file
```

**`Python`**

```python Python
file = client.files.upload(
    file=open("./photo.jpg", "rb").read(),
    filename="photo.jpg",
)

print(file.id)  # file_xxxxxxxxxxxxx
print(file.url)  # URL to access the file
```

**`Ruby`**

```ruby Ruby
file = client.files.upload(
  file: File.binread("./photo.jpg"),
  filename: "photo.jpg",
)

puts file.id  # file_xxxxxxxxxxxxx
puts file.url  # URL to access the file
```

The `upload` method:

1. Creates a file record with a presigned URL
2. Uploads your file to storage
3. Polls until processing is complete
4. Returns the ready file with its final URL

# Public vs private files

Every file you upload has a **visibility** that controls how it can be accessed. You choose visibility at upload time — it cannot be changed after.

| Visibility         | URL type                    | Who can access             | Use for                                                        |
| ------------------ | --------------------------- | -------------------------- | -------------------------------------------------------------- |
| `public` (default) | Permanent, unsigned CDN URL | Anyone with the link       | Product images, thumbnails, branding, marketing assets         |
| `private`          | Signed URL that expires     | Only your app, via the API | User-uploaded documents, sensitive content, AI-generated files |

> **Warning**
>
> **Choose carefully.** Public files are cached on our CDN and accessible to anyone with the URL — there is no way to revoke access. If the content is user-specific or sensitive, use `private`.

## Uploading a private file

Pass `visibility: "private"` when creating the file:

**`TypeScript`**

```typescript TypeScript
const file = await client.files.upload({
  file: fs.readFileSync('./user-document.pdf'),
  filename: 'user-document.pdf',
  visibility: 'private',
});

// file.url is a signed URL that expires — fetch it fresh via
// client.files.retrieve(file.id) whenever you need a new one
```

**`Python`**

```python Python
file = client.files.upload(
    file=open("./user-document.pdf", "rb").read(),
    filename="user-document.pdf",
    visibility="private",
)
```

**`Ruby`**

```ruby Ruby
file = client.files.upload(
  file: File.binread("./user-document.pdf"),
  filename: "user-document.pdf",
  visibility: "private",
)
```

## Accessing private files

Private file URLs expire. To get a fresh URL, retrieve the file by ID:

```typescript
const file = await client.files.retrieve(fileId);
console.log(file.url); // fresh signed URL
```

# Using uploaded files

Once uploaded, use the file ID in any API call that accepts file attachments:

```typescript
await client.courses.update({
	id: "course_xxx",
	thumbnail: { id: file.id },
});
```

# File properties

| Property        | Description                                                    |
| --------------- | -------------------------------------------------------------- |
| `id`            | Unique identifier (e.g., `file_xxxxxxxxxxxxx`)                 |
| `filename`      | Original filename                                              |
| `content_type`  | MIME type (e.g., `image/jpeg`)                                 |
| `byte_size`     | File size in bytes                                             |
| `url`           | URL to access the file (signed and expiring for private files) |
| `upload_status` | Status: `pending`, `processing`, `ready`, or `failed`          |
| `visibility`    | `public` or `private`                                          |