Wbcom Designs WPMediaVerse Docs
Back to product Buy Now

Getting Started

Install, configure, and start sharing media in minutes.

Why WPMediaVerse

WPMediaVerse is a purpose-built media platform for WordPress. Unlike plugins that bolt media features onto WordPress posts or attachments, WPMediaVerse uses its own high-performance database architecture designed from the ground up for media-heavy communities.

The Problem with WordPress Attachments

Most WordPress media plugins store user uploads as wp_posts with post_type = 'attachment'. This creates serious problems at scale:

  • wp_posts table bloat - A community with 50,000 photos adds 50,000 rows to the same table that holds your pages, blog posts, menu items, and revisions. Every WP_Query on your site slows down.
  • wp_postmeta overhead - Each media item needs 10-20 meta rows for stats, privacy, tags, and file paths. At 50K media, that is 500K-1M rows in wp_postmeta - the single biggest performance bottleneck in WordPress.
  • No native stats - WordPress attachments have no built-in view counting, reaction tracking, or engagement metrics. Plugins that add these use postmeta, compounding the bloat.
  • Mixed concerns - Admin queries for pages and posts compete with media queries. There is no way to independently optimize media storage.

How WPMediaVerse Is Different

WPMediaVerse stores all media in dedicated custom tables, completely separate from wp_posts:

Table Purpose
mvs_media_index Core media record - title, author, file URL, privacy, status, timestamps
mvs_media_meta Sparse key-value metadata (thumbnails, EXIF, groups)
mvs_media_stats Views, reactions, comments, favorites - one row per media
mvs_reactions Individual emoji reactions with user attribution
mvs_favorites User favorites / saved items
mvs_follows User follow relationships
mvs_conversations Direct message conversations
mvs_messages Individual chat messages

What This Means for You

  • Zero wp_posts bloat - 100,000 media items add zero rows to wp_posts. Your pages, menus, and blog posts are unaffected.
  • Indexed queries - Every table has purpose-built indexes. Fetching "latest 12 public photos by user X" is a single indexed query, not a WP_Query with meta joins.
  • Independent scaling - You can optimize, partition, or replicate media tables without touching the rest of WordPress.
  • No postmeta bottleneck - Stats, privacy, and metadata live in dedicated columns or a sparse meta table. No serialized arrays, no autoload bloat.
  • Real-time stats - Views, reactions, and comments are atomic counters in mvs_media_stats, not meta values that need cache invalidation.

Performance at Scale

Metric Attachment-based plugins WPMediaVerse
10K media: wp_posts rows added 10,000 0
10K media: wp_postmeta rows added 100,000-200,000 0
Query: latest 12 photos WP_Query + 3 meta joins Single indexed SELECT
Query: user's photo count COUNT on wp_posts + meta COUNT on mvs_media_index (indexed)
Stats update (view count) update_post_meta (cache bust) UPDATE mvs_media_stats SET views = views + 1

Files Are Still in wp-content

WPMediaVerse stores database records in custom tables. The actual files (images, videos, audio) are stored in the standard WordPress uploads directory:

wp-content/uploads/wpmediaverse/2026/03/photo-name.jpg
wp-content/uploads/wpmediaverse/2026/03/photo-name-300x200.jpg  (thumbnail)
wp-content/uploads/wpmediaverse/2026/03/photo-name-1024x768.jpg (large)
wp-content/uploads/wpmediaverse/2026/03/photo-name-150x150.jpg  (square)

With Pro's cloud storage, files can also be stored on Amazon S3 or BunnyCDN while database records remain local.

How It Compares

Feature rtMedia BuddyBoss Media MediaPress WPMediaVerse
Storage wp_posts + postmeta wp_posts + postmeta wp_posts + postmeta Custom tables
Requires BuddyPress Yes Yes (BuddyBoss) Yes No (optional)
Standalone mode No No No Yes
Custom privacy levels Basic Basic Basic 6 levels (Pro)
Direct messaging No Separate plugin No Built-in
Gamification No No No Challenges, battles, tournaments (Pro)
Video transcoding No No No Multi-quality + HLS (Pro)
Cloud storage No No No S3 + BunnyCDN (Pro)
AI moderation No No No OpenAI + Vision + Rekognition
Upload quotas No No No Per-user packages (Pro)
Layout modes 1 1 1 5 (grid + 4 Pro layouts)

Use Cases

Photography Community

A social network for photographers to share, discover, and compete. Users upload photos, follow each other, enter weekly challenges, and battle head-to-head. The Pinterest or Instagram layout creates a visual-first experience.

Portfolio Showcase

Designers, artists, and photographers use WPMediaVerse as their portfolio. The Dribbble layout presents work in a professional shot grid. Clients can view galleries, leave comments, and message directly.

School or University

Students submit media projects through the upload system. Teachers use collections to curate work. Privacy controls ensure only enrolled members see content. Quotas limit storage per student.

Company Intranet

Employees share photos from events, marketing assets, and training videos. Group media tabs organize content by department. AI moderation flags inappropriate uploads automatically.

BuddyPress Community

Add a rich media layer to any BuddyPress social network. Members get media tabs on profiles and groups, uploads appear in the activity stream, and followers see new content in their feed. Everything works out of the box - activate BuddyPress and the integration is automatic.

Frequently Asked Questions

Will my media appear in the WordPress Media Library?

No. WPMediaVerse media is managed through its own admin pages (Media > All Media) and frontend dashboard (My Media). This is intentional - it keeps the WordPress Media Library clean for your theme images, post attachments, and other site assets.

Can I use WPMediaVerse without BuddyPress?

Yes. WPMediaVerse is a standalone plugin. It creates its own pages (/explore-media/, /my-media/, /upload-media/) and works on any WordPress site. BuddyPress integration activates automatically when BuddyPress is installed but is completely optional.

What happens to my data if I deactivate the plugin?

Deactivation stops all plugin functionality but your data stays intact in the database. Reactivating restores everything. Deleting the plugin (uninstall) removes all custom tables and data permanently.

Can I migrate from rtMedia / MediaPress / BuddyBoss?

Yes. WPMediaVerse Pro includes WP-CLI migration tools that import media records, preserving original upload dates, author attribution, and file URLs. See Migration Tools.

How does search work?

WPMediaVerse has its own search system that queries the mvs_media_index table directly. It searches titles and descriptions. Tags and categories use WordPress taxonomies for filtering.

Does it work with page builders?

Yes. WPMediaVerse provides Gutenberg blocks (Media Grid, Explore Feed, Album Viewer) and shortcodes for Elementor, Beaver Builder, and other builders. The blocks use the WordPress Interactivity API for dynamic behavior.

Installation

Get WPMediaVerse running on your WordPress site in under five minutes - install, activate, and your community is ready to start uploading.

Requirements

  • WordPress 6.5+
  • PHP 7.4+
  • MySQL 5.7+ or MariaDB 10.4+
  • BuddyPress 12.0+ (optional, for social integration)

Installing via WordPress Admin

  1. Go to Plugins > Add New Plugin in your WordPress dashboard.
  2. Search for WPMediaVerse.
  3. Click Install Now, then Activate.

WordPress plugin search showing WPMediaVerse install button

Installing via ZIP Upload

  1. Download the wpmediaverse.zip file from wbcomdesigns.com.
  2. Go to Plugins > Add New Plugin > Upload Plugin.
  3. Choose the ZIP file and click Install Now.
  4. Click Activate Plugin.

Upload plugin screen with WPMediaVerse ZIP selected

Installing via FTP

  1. Unzip the downloaded archive.
  2. Upload the wpmediaverse folder to /wp-content/plugins/.
  3. Go to Plugins in your dashboard and activate WPMediaVerse.

What Happens on Activation

When you activate WPMediaVerse, the plugin automatically:

  • Creates custom database tables for media index, stats, reactions, comments, favorites, follows, conversations, messages, collections, access grants, and webhooks - all separate from wp_posts for maximum performance.
  • Registers the mvs_album and mvs_collection custom post types (media itself uses the custom mvs_media_index table, not wp_posts).
  • Registers the mvs_tag and mvs_category taxonomies.
  • Adds default capabilities to the Administrator, Editor, Author, Contributor, and Subscriber roles.
  • Redirects you to the Setup Wizard for initial configuration.

Deactivation and Uninstall

Deactivation stops all plugin functionality but keeps your data intact.

Uninstall (deleting the plugin) removes all plugin data including:

  • All mvs_album and mvs_collection posts
  • All custom database tables (including the mvs_media_index media records)
  • All plugin options and transients

To preserve data after uninstalling, export your media library first.

Setup Wizard

After you activate WPMediaVerse, a short wizard guides you through the only settings you need to make your first upload possible. The whole thing takes about two minutes.

Setup wizard welcome screen

Step 1: Welcome

A quick overview of what WPMediaVerse does - uploads and albums, the social layer (reactions, comments, favorites, follows), AI moderation and privacy controls, and optional BuddyPress integration. Click Let's Get Started to continue, or Skip setup to jump straight to the dashboard.

Step 2: Pages

The wizard confirms the three frontend pages that were created automatically on activation:

Page Default slug Purpose
Explore Media /explore-media/ Public gallery where visitors browse all media
My Media /my-media/ Personal dashboard for users to manage their uploads
Upload Media /upload-media/ Frontend upload form, linked from the Explore page header

If any page is missing, the wizard flags it and asks you to reactivate the plugin. Click Continue when the pages are in place.

Step 3: Display

Configure how media appears on your site.

Setup wizard display step showing grid column and items-per-page options

Option Choices Default
Grid Columns 2, 3, 4 3
Items Per Page 12, 24, 48 24
Thumbnail Style Square (cropped), Original proportions Square

You can change these any time later under Media > Settings.

Step 4: Done

The wizard marks setup as complete (the mvs_setup_complete option) and redirects you to the Media Overview dashboard.

From here you can:

  • Upload your first media file
  • Configure AI moderation settings
  • Set up BuddyPress integration (if BuddyPress is active)

Role Capabilities

The wizard itself does not configure roles. On activation, WPMediaVerse adds default media capabilities to the Administrator, Editor, Author, Contributor, and Subscriber roles. You can review and change these at any time in Media > Settings.

Skipping the Wizard

You can skip the wizard at any step using the Skip setup link, which takes you to the dashboard. Once setup is marked complete, the wizard no longer runs automatically on subsequent visits.

Your First Upload

Upload your first photo in two minutes - drag, drop, set privacy, done. Here is exactly how it works.

Option 1: Use the Upload Block or Shortcode

Add the upload form to any page using either the Gutenberg block or shortcode.

In the Block Editor: Add the WPMediaVerse: Media Upload block to your page.

In the Classic Editor or any text area:

[mvs_upload]

Frontend media upload form with drag-and-drop area and privacy selector

Option 2: Upload via REST API

Use the REST endpoint directly (for custom integrations):

curl -X POST https://yoursite.com/wp-json/mvs/v1/media \
  -H "X-WP-Nonce: YOUR_NONCE" \
  -F "file=@/path/to/image.jpg" \
  -F "title=My First Upload" \
  -F "privacy=public"

The Upload Process

When you upload a file, WPMediaVerse:

  1. Validates the MIME type against your allowed file types list.
  2. Checks the file size against your configured maximum (default: 100 MB).
  3. Scans for duplicate files using SHA-256 hash comparison.
  4. Strips EXIF GPS data from images (if enabled - on by default).
  5. Stores the file using your configured storage driver (local by default).
  6. Creates a record in the mvs_media_index table with the title, privacy level, and file metadata (media is not stored as a WordPress post).
  7. Runs AI analysis if auto-analyze is enabled (requires OpenAI API key).
  8. Runs AI moderation if auto-moderate is enabled.
  9. Records BuddyPress activity if BuddyPress is active.

Supported File Types

By default, WPMediaVerse accepts:

Type Formats
Images JPEG, PNG, GIF, WebP
Video MP4, WebM
Audio MP3 (MPEG), OGG

You can customize allowed file types in Media > Settings > General.

Setting Privacy on Upload

Each file can have one of 6 privacy levels:

Level Who Can See It
Public Everyone, including logged-out visitors
Members Only Any logged-in WordPress user
Friends BuddyPress friends of the uploader (requires BuddyPress)
Group Members of a specific BuddyPress group (requires BuddyPress)
Private Only the uploader and administrators
Custom A specific list of user IDs (managed via API or access rules)

The default privacy level is set in Media > Settings > General > Default Privacy Level.

After Uploading

Your uploaded media appears:

  • On the media archive page (/wp-json/mvs/v1/media in the API)
  • In the Media > All Media list in your admin dashboard
  • In the Media tab on your BuddyPress profile (if BuddyPress is active)
  • In your media dashboard (use [mvs_dashboard] shortcode)

Free vs Pro Comparison

WPMediaVerse Free is a full-featured media platform. WPMediaVerse Pro unlocks advanced tools for professional communities, monetization, and engagement.

Free and Pro release in lockstep and share the same version number. See the changelog for what shipped in each release, or Pro feature overview for the current Pro feature set.

Quick Comparison

Feature Free Pro
Media Upload & Management
Drag & drop upload (photo, video, audio) Yes Yes
Bulk upload Yes Yes
Title, description, tags, categories Yes Yes
EXIF stripping & duplicate detection Yes Yes
Thumbnail generation (3 sizes) Yes Yes
Custom storage path Yes Yes
Amazon S3 cloud storage -- Yes
BunnyCDN cloud storage -- Yes
Feed Layouts
Default grid layout Yes Yes
Instagram layout (square grid + stories) -- Yes
Pinterest layout (masonry cards) -- Yes
Flickr layout (justified gallery) -- Yes
Dribbble layout (portfolio shots) -- Yes
Social Features
Follow / unfollow users Yes Yes
Emoji reactions on media Yes Yes
Threaded comments Yes Yes
Favorites (save for later) Yes Yes
@Mentions in comments Yes Yes
Share to social media Yes Yes
Direct messages (1-on-1 chat) Yes Yes
Voice messages in DMs Yes Yes
Media sharing in DMs Yes Yes
Message requests & privacy controls Yes Yes
Content Organization
Albums with cover photos Yes Yes
Collections (curated boards) Yes Yes
Stories (24-hour ephemeral posts, seen-by receipts) -- Yes
Gallery groups (multi-photo posts) Yes Yes
Privacy & Moderation
Public / Members Only / Private Yes Yes
Friends Only privacy level Yes (needs BuddyPress Friends) Yes
Group Members Only privacy level Yes (enforced) Yes (picker UI)
Custom privacy (specific users) Yes (enforced) Yes (picker UI)
Album-level privacy inheritance Yes Yes
Privacy presets -- Yes
Bulk privacy updates -- Yes
AI content moderation (OpenAI) Yes Yes
Google Cloud Vision moderation -- Yes
AWS Rekognition moderation -- Yes
Claude (Anthropic) moderation -- Yes
User reporting Yes Yes
User blocking Yes Yes
GDPR data export & erasure Yes Yes
Video
Video upload & playback Yes Yes
Multi-quality transcoding (720p/480p/360p) -- Yes
HLS adaptive streaming -- Yes
Video chapter markers -- Yes
Resume playback (pick up where you left off) -- Yes
Auto-captions via OpenAI Whisper -- Yes
Video analytics & heatmaps -- Yes
Image Processing
Basic text watermarking Yes Yes
Logo watermarking with positioning -- Yes
Watermark opacity & font control -- Yes
Gamification
Photo Challenges (themed competitions) -- Yes
1v1 Photo Battles -- Yes
Single-elimination Tournaments -- Yes
Media Boosts (spend points for visibility) -- Yes
Upload Streaks with milestones -- Yes
Weekly Autopilot (auto-create challenges) -- Yes
XP integration with wb-gamification -- Yes
Quotas & Monetization
Per-user upload quotas (count + storage) -- Yes
Quota packages (Free, Premium, etc.) -- Yes
MemberPress integration -- Yes
Paid Memberships Pro integration -- Yes
WooCommerce integration -- Yes
Credit transaction log -- Yes
User Profiles
Public profile page (/media/@username/) Yes Yes
Follow / Message buttons Yes Yes
Custom avatar upload Yes Yes
Inline profile editing -- Yes
Admin Tools
Media overview dashboard Yes Yes
Media list with bulk actions Yes Yes
Settings (general, display, social, AI) Yes Yes
Competitions dashboard -- Yes
Challenge manager with theme library -- Yes
Tournament bracket manager -- Yes
Battle monitor -- Yes
Video analytics dashboard -- Yes
Quota & credits management -- Yes
Media stats & insights -- Yes
BuddyPress Integration
Profile media tab Yes Yes
Group media tab Yes Yes
Activity stream media Yes Yes
Notifications (likes, comments, follows) Yes Yes
Gutenberg Blocks
Core blocks (Media Grid, Player, Album, Upload, Stats, Explore Feed, Lock Overlay, Member Photos, PDF Viewer) Yes (9) Yes (inherited)
Pro feature blocks (Tournament, Challenge, Battle, Leaderboard, Compete Hub) -- Yes (5)
Pro feed-layout blocks (Instagram, Flickr, Pinterest, Dribbble) -- Yes (4)
Pro list blocks (Tournaments List, Challenges List, Battles Active) -- Yes (3)
Mix layouts per-page -- Yes
Developer
REST API (100+ endpoints, mvs/v1 namespace) Yes Yes
Pro REST API (37 additional endpoints, mvs-pro/v1 namespace) -- Yes
Lightbox download + share REST endpoints Yes Yes
150+ action and filter hooks Yes Yes
Template override system Yes Yes
Custom storage driver API Yes Yes
WP-CLI commands (18 commands, wp mvs namespace) Yes Yes
wp mvs generate-video-thumbnails (poster backfill) Yes Yes
Migration tools (rtMedia, MediaPress, BuddyBoss) -- Yes
MigrationPage admin (per-platform cards) -- Yes
Accessibility
WCAG 2.1 AA pass on customer-facing UI Yes Yes
aria-label on icon buttons + aria-pressed on toggles Yes Yes
:focus-visible outlines + keyboard navigation Yes Yes
Theme dark-mode support (BuddyX Pro / generic class-based) Yes Yes
Integrations
BuddyPress 12+ Yes Yes
wb-gamification -- Yes
MemberPress -- Yes
Paid Memberships Pro -- Yes
WooCommerce -- Yes
OpenAI (moderation + captions) Yes Yes
Google Cloud Vision -- Yes
AWS Rekognition -- Yes
Amazon S3 -- Yes
BunnyCDN -- Yes

A note on the privacy rows

Privacy is the one area where "Free vs Pro" is not a clean on/off split, so it is worth stating precisely:

  • Free enforces all six privacy levels. PrivacyService gates viewing for public, members, friends, group, private and custom. If a media item carries one of those levels - set by an import, the REST API, an album inheriting its privacy, or Pro before a licence lapsed - Free honours it. Your members' private media does not become visible because Pro is inactive.
  • Free's upload form exposes three of them, plus Friends Only when the BuddyPress Friends component is active: Public, Members, Friends (conditional) and Only Me. Group and Custom are enforced but have no picker in the stock Free upload form.
  • Pro adds the interface, not the enforcement: a picker for all six levels with plain-language descriptions, saved presets, and bulk privacy updates across many items at once.
  • Album-level privacy inheritance is Free. When media is added to an album, AlbumService clamps each item to the more restrictive of the two, filterable via mvs_album_inherit_privacy.

There is no "Followers Only" media privacy level in either plugin. followers appears only as a direct-message access setting (mvs_dm_access), which controls who may open a conversation with you - not who can see your media.

What You Get Free

WPMediaVerse Free is not a stripped-down trial. It is a complete media platform with:

  • Full upload system with drag & drop, bulk upload, and duplicate detection
  • Albums, collections, and gallery groups
  • Complete social layer: follows, reactions, comments, favorites, mentions, sharing
  • Built-in direct messaging with voice messages, file sharing, and read receipts
  • User profiles with follow/message buttons and media grids
  • AI content moderation via OpenAI
  • BuddyPress integration (profiles, groups, activity, notifications)
  • Full REST API with 100+ endpoints
  • GDPR compliance (data export + erasure)
  • User blocking and reporting

What Pro Adds

WPMediaVerse Pro is for sites that need professional-grade features:

  • Visual identity - Choose from Instagram, Pinterest, Flickr, or Dribbble layouts to match your community's style
  • Scale - Offload media to S3 or BunnyCDN for global CDN delivery and unlimited storage
  • Video intelligence - Multi-quality transcoding, adaptive streaming, chapters, auto-captions, and engagement analytics
  • Engagement - Gamification system with challenges, battles, tournaments, boosts, and streaks that keep users coming back
  • Monetization - Quota packages with MemberPress/WooCommerce integration let you sell tiered upload plans
  • Privacy - A picker UI for all six privacy levels, plus saved presets and bulk privacy management
  • AI - Google Vision, AWS Rekognition, and Claude (Anthropic) for auto-tagging and advanced content moderation
  • Migration - Import from rtMedia, MediaPress, or BuddyBoss with one WP-CLI command

Upgrading

  1. Purchase a Pro license at wbcomdesigns.com
  2. Upload and activate the wpmediaverse-pro.zip plugin
  3. Enter your license key at Media > License
  4. Pro features activate immediately - no data migration needed, no settings lost

Integrations

WPMediaVerse connects with 12+ third-party services and plugins out of the box. No custom code, no middleware - configure credentials in settings and the integration activates.

Integrations Admin Page (1.8.0)

Go to WPMediaVerse > Integrations for a visual view of the Wbcom plugin family - the products designed to work alongside WPMediaVerse. Each card shows the product logo, a short "why you'd want this" description, and a status badge (Connected, Installed, activate, or Not installed).

  • Install free - installs and activates the companion plugin's free version in one click, without leaving the page.
  • Learn more - links out to the product's page on the Wbcom store.
  • Every companion plugin works standalone - installing one from this page does not tie it to WPMediaVerse, and WPMediaVerse simply lights up the matching integration when it detects the companion is active.

This page lists the family (WB Gamification, BuddyX/BuddyNext, and other Wbcom products); it's separate from the third-party service integrations (AI providers, cloud storage) documented below, which are configured under Settings.

Integration Map

Integration Plugin What It Does Free Pro
BuddyPress Free Profile media tabs, group media, activity stream, notifications Yes Yes
BuddyNext Free Enhanced member directory and profile blocks Yes Yes
WB Gamification Free (separate plugin) XP points, badges, leaderboards for competitions (optional - competitions run without it, only points need it) -- Yes
Amazon S3 Pro Store all media files on S3 with CDN delivery -- Yes
BunnyCDN Pro Store and deliver media via BunnyCDN edge network -- Yes
OpenAI Free AI content moderation, auto-tagging, description generation Yes Yes
Google Cloud Vision Pro Advanced image labeling, object detection, safe search -- Yes
AWS Rekognition Pro Face detection, content moderation, celebrity recognition -- Yes
OpenAI Whisper Pro Automatic video/audio transcription to WebVTT captions -- Yes
MemberPress Pro Auto-assign quota packages based on membership level -- Yes
Paid Memberships Pro Pro Auto-assign quota packages based on PMPro level -- Yes
WooCommerce Pro Sell upload quota packages as WooCommerce products -- Yes
WordPress Webhooks Free Send real-time HTTP notifications on media events Yes Yes

Community & Social

BuddyPress

WPMediaVerse is the most complete media solution for BuddyPress communities. The integration activates automatically when BuddyPress is detected - no configuration needed.

What users get:

  • A Media tab on every member profile showing their uploads in a grid
  • A Media tab in every group where members upload and share within the group
  • Media uploads appear as activity items in the BuddyPress activity stream with thumbnails
  • Notifications when someone likes, comments on, or shares your media
  • One-click media sharing from the lightbox directly into BuddyPress activity

What admins get:

  • Zero configuration - activate BuddyPress and the integration works
  • Media tab visibility follows BuddyPress privacy settings
  • Activity items follow BuddyPress moderation rules
  • Compatible with BuddyPress 12.0+

See BuddyPress Integration for full details.

BuddyNext

If you use the BuddyNext theme, WPMediaVerse detects it automatically and enhances the member directory with media counts and the profile layout with media grid blocks. As of 2.0.0, BuddyNext also changes how individual media links behave - see Activity Stream Media for details.

wb-gamification (Pro)

This integration requires the separate, free WB Gamification plugin. Points (XP) are only earned or spent when WB Gamification is installed and active. Without it, the competition features still run fully - members can create and enter Challenges, Battles, and Tournaments, vote, and see winners - but no points are awarded for wins or streaks, and the point-spending controls (such as Media Boosts) stay hidden. Every MediaVerse award/spend path is guarded, so Pro works correctly whether or not WB Gamification is present.

When WB Gamification is active, WPMediaVerse Pro feeds it points for these competition outcomes via the wb_gam_points_for_action filter:

Action When Default XP
Win a challenge (1st) First place in a challenge 200
Win a challenge (2nd) Second place 100
Win a challenge (3rd) Third place 50
Enter a challenge Submit an entry that meets the rules 10
Win a tournament round Advance one bracket round configurable
Win a tournament Tournament champion configurable
Reach a streak milestone Hit a daily-upload streak threshold configurable

Challenge XP values are configured per competition when you create a Challenge (1st / 2nd / 3rd / participation), not as a single global table. WB Gamification handles the points ledger, badges, leaderboards, and leveling - WPMediaVerse Pro only tells it which competition outcome occurred.

Get the free plugin: WB Gamification.

Cloud Storage

Amazon S3 (Pro)

Offload every upload to an S3 bucket. Files are served from S3 directly (or via CloudFront if configured).

What you get:

  • Unlimited storage (pay-as-you-go with AWS)
  • Global CDN delivery when paired with CloudFront
  • Signed URLs for private/members-only media
  • Automatic retry (3 attempts) on upload failure
  • Connection test button in admin to verify credentials

Setup: Paste your bucket name, region, access key, and secret key into Media > Settings > Storage > Amazon S3. Click "Test Connection" to verify. New uploads go to S3 immediately.

Security: Store credentials in wp-config.php instead of the database:

define( 'MVS_PRO_AWS_ACCESS_KEY', 'AKIA...' );
define( 'MVS_PRO_AWS_SECRET_KEY', '...' );

See Cloud Storage for IAM policy and full setup guide.

BunnyCDN (Pro)

Store and deliver media through BunnyCDN's global edge network.

What you get:

  • 114 edge locations worldwide
  • Automatic image optimization
  • Per-request pricing (no minimum commitment)
  • Simpler setup than AWS (one API key)

Setup: Enter your storage zone name, API key, and CDN hostname into Media > Settings > Storage > BunnyCDN.

Custom Storage Drivers

WPMediaVerse uses a StorageDriverInterface that any developer can implement. Build drivers for Google Cloud Storage, DigitalOcean Spaces, Wasabi, Backblaze B2, or any S3-compatible service.

See Custom Storage Drivers for the interface spec.

AI & Machine Learning

OpenAI (GPT + Vision)

Built into the free plugin. Uses the OpenAI API for:

  • Content moderation - Automatically flag inappropriate uploads before they appear on the site
  • Auto-tagging - AI suggests relevant tags based on image content
  • Description generation - Generate alt text and descriptions for accessibility
  • Monthly budget cap - Set a dollar limit to prevent unexpected API costs

Setup: Paste your OpenAI API key into Media > Settings > AI & Moderation.

Google Cloud Vision (Pro)

Adds Google's image analysis capabilities:

  • Label detection - Identify objects, locations, activities in photos
  • Safe search - Detect explicit, violent, or medical content
  • Text detection - Extract text from images (OCR)
  • Circuit breaker pattern prevents API hammering on failures

Setup: Paste your Google Cloud API key into Media > Settings > AI & Moderation > Google Vision.

AWS Rekognition (Pro)

Adds Amazon's image and video analysis:

  • Object and scene detection - Identify objects with confidence scores
  • Face detection - Detect faces with attributes (smile, glasses, age range)
  • Content moderation - Flag suggestive, violent, or explicit content
  • Circuit breaker pattern with automatic recovery

Setup: Uses the same AWS credentials as S3 storage, or set separate credentials in Media > Settings > AI & Moderation > AWS Rekognition.

OpenAI Whisper (Pro)

Automatic speech-to-text transcription for video and audio uploads:

  • Generates WebVTT caption files
  • Captions are searchable and displayed as subtitles in the video player
  • Process runs asynchronously via Action Scheduler
  • Supports 50+ languages

Setup: Enable at Media > Settings > Video > Auto-Captions (uses the same OpenAI API key).

Monetization

MemberPress (Pro)

Automatically assign upload quota packages based on MemberPress membership levels.

How it works:

  1. Create quota packages in Media > Quotas (e.g., "Free: 50 photos", "Premium: unlimited")
  2. Map each MemberPress membership to a quota package
  3. When a user purchases or is assigned a membership, their quota updates automatically
  4. When a membership expires, the user reverts to the default package

Same automatic package assignment, but using PMPro membership levels instead of MemberPress.

WooCommerce (Pro)

Sell upload quota packages as WooCommerce products:

How it works:

  1. Create quota packages in Media > Quotas
  2. Create a WooCommerce product and map it to a quota package
  3. When a customer completes checkout, their quota package activates
  4. If the order is refunded or cancelled, the package reverts to default

This lets you sell storage tiers directly from your WooCommerce store.

Webhooks

WPMediaVerse can send real-time HTTP POST notifications to external services when events occur:

Event Payload
Media uploaded Media ID, file URL, author, type, privacy
Media deleted Media ID
Comment posted Comment ID, media ID, author, content
Reaction added Media ID, user ID, reaction type
Report submitted Media ID, reporter ID, reason
Moderation changed Media ID, old status, new status

Use cases:

  • Notify a Slack channel when new media is uploaded
  • Trigger a Zapier workflow on media events
  • Sync media metadata to an external CMS or DAM
  • Log moderation actions to an audit system

Setup: Add webhook URLs at Media > Settings > Webhooks. Each webhook can filter by event type.

See Webhooks for payload formats and authentication.

WordPress Core

Site Health

WPMediaVerse registers 3 custom tests in Tools > Site Health:

  • Database tables - Verifies all custom tables exist and have the expected schema
  • Upload directory - Checks that the wpmediaverse upload directory is writable
  • Required pages - Confirms Dashboard, Explore, and Upload pages are assigned

GDPR / Privacy Tools

  • Export Personal Data - Exports all user media, comments, reactions, favorites, DMs, and follow relationships
  • Erase Personal Data - Removes all of the above when processing an erasure request
  • Privacy Policy - Suggests privacy policy text via WordPress's built-in privacy policy tool

See GDPR & Privacy Compliance.

REST API

WPMediaVerse exposes 50+ REST endpoints in the free plugin and 30+ additional endpoints in Pro, all under the mvs/v1 and mvs-pro/v1 namespaces. Any external application, mobile app, or headless frontend can consume the full API.

See REST API Reference and Pro REST API Reference.

WP-CLI

CLI commands for automation, migration, and maintenance:

wp mvs stats              # Show media stats
wp mvs migrate            # Run database migrations
wp mvs reindex            # Rebuild media index
wp mvs cache-flush        # Flush all caches
wp mvs prune-views        # Clean old view records
wp mvs cleanup-expired    # Remove expired stories/tokens
wp mvs moderation-stats   # Show moderation queue stats
wp mvs optimize           # Losslessly optimize an image
wp mvs migrate-storage    # Migrate files between storage drivers

See WP-CLI Commands.

Gutenberg Blocks

5 blocks for the block editor:

Block Description
Media Grid Display a filtered grid of media items
Explore Feed Full explore page with search, tags, and pagination
Album Viewer Display an album's contents
Lock Overlay Restrict content visibility with a paywall-style overlay
Profile Edit Inline profile editing block

See Gutenberg Blocks.

Shortcodes

For classic editor and page builders:

Shortcode Description
[mvs_gallery] Media grid with filtering
[mvs_upload] Upload form
[mvs_dashboard] User's media dashboard
[mvs_profile_edit] Inline profile editing form

See Shortcodes.

What's New in 2.3.0

WPMediaVerse 2.3.0 is a correctness release. Photos taken on a phone stop landing sideways, media added to a private album stops being publicly visible, the archived-conversations tab arrives in chat, and members can sign in to the mobile app with the password they already have.

Included in Free. Every item on this page applies to the free version unless marked Pro. Paired with WPMediaVerse Pro 2.3.0 - install and test both together.

Phone photos keep their orientation

Images uploaded from a phone are rotated to match their EXIF Orientation tag before they are stored, so portrait shots no longer appear sideways in the grid, the lightbox, or an activity post. This runs on the normal upload path and on Replace file, and the stored width and height are re-recorded after rotation so grid layouts reserve the right space.

Sites that already normalise orientation upstream - some CDNs and phone-upload apps do - can skip the extra re-encode with the new mvs_apply_exif_orientation filter.

Media added to a private album inherits its album's privacy

A member who creates a Private album and uploads into it expects the contents to be private. Adding media to an album now clamps each item to the more restrictive of the two privacy levels, so a public photo added to a private album becomes private, and a private photo added to a public album stays private.

Nothing is ever made more public by this: the clamp only tightens. Album privacy inheritance is controllable with mvs_album_inherit_privacy, and each clamp fires mvs_media_privacy_clamped_by_album for auditing.

Album-level privacy inheritance is a Free feature. See Free vs Pro for exactly which parts of privacy are Free and which parts Pro adds.

Archived conversations

Chat gains a proper Archived tab. Archiving a conversation moves it out of your main list without deleting it, and it comes back on its own when the other person writes to you again - controllable via mvs_dm_unarchive_on_activity.

Alongside it, two long-standing chat annoyances are fixed:

  • Tab counts are honest. The message-requests badge counts actual pending requests. Previously requests were counted from the main list, so the badge could read 0 while requests were waiting.
  • Day separators and message times are grouped by day with Today / Yesterday headings.

Sign in to the app with your existing password

Members can now sign in to a native WPMediaVerse client with their normal WordPress password, and the site issues an Application Password behind the scenes - no more copying a generated credential by hand.

The exchange endpoint is public by necessity (there is nothing to authenticate with before you have a credential) and is guarded accordingly: a site-owner switch, a TLS requirement, uniform failure responses, the suspension gate, rate limiting applied before any credential is read, and a 409 rather than a silent two-factor bypass. See POST /auth/app-password.

Fixes you will notice

  • Tall portraits are no longer clipped in the lightbox - they scale to fit instead of being cropped.
  • The activity feed no longer distorts single images. Aspect ratios are preserved rather than force-cropped to a fixed height.
  • Double-clicking Post no longer publishes the same comment twice. Duplicate comments within a short window are rejected; the window is filterable via mvs_comment_duplicate_window.
  • Album counts ignore trashed media, so an album no longer advertises photos that are in the trash.
  • No more phantom "sent you a message" notification when a conversation is merely opened.
  • Demo data can be removed and re-imported. Previously the importer refused to run once demo data had been deleted, and reported that refusal as a success.
  • Album dropzones are visible outside BuddyPress, and album assets load correctly under client-side navigation.

Pro 2.3.0

  • Competition transitions no longer fire twice on sites upgraded from an earlier version, so challenges and tournaments do not double-advance.
  • Turning Boosts off unschedules its expiry job instead of leaving a cron entry behind.
  • Quota widget polish - the dashboard quota widget is spaced properly, and it no longer appears at all for members who have no quota package and no limits, where it previously showed a "No Package" heading above three "Unlimited" rows and an offer to contact an administrator for more storage. Site owners who use it to advertise upgrades still get it, and mvs_pro_quota_widget_visible forces it back on.
  • Autopilot "Run Now" sits in a proper action row on the Competitions dashboard.

For developers

  • The complete option list now lives in Settings Reference.
  • New filters: mvs_apply_exif_orientation, mvs_album_inherit_privacy, mvs_comment_duplicate_window, mvs_dm_unarchive_on_activity, mvs_pro_quota_widget_visible (Pro).
  • New action: mvs_media_privacy_clamped_by_album.
  • The moderation queue route is GET /moderation. Earlier revisions of the REST reference showed /moderation/queue, which returns 404.
  • Four watermark hooks that appeared in older revisions of the hooks reference (mvs_watermark_config, mvs_generate_watermark, mvs_watermark_invalidated, mvs_watermarks_invalidated_all) do not exist in the code and have been removed from the docs. The real extension points are mvs_watermark_enabled, mvs_watermark_stamp_file and, in Pro, mvs_watermark_font_path.

Compatibility

Free and Pro release in lockstep. Install WPMediaVerse 2.3.0 and WPMediaVerse Pro 2.3.0 together - Pro 2.3.0 expects the Free 2.3.0 service surface.

No database migration is required beyond the automatic Migrator run on activation, and no settings are lost on upgrade.

What's New in 2.2.0

WPMediaVerse 2.2.0 is a polish and privacy release: the whole REST surface can now follow your community's privacy setting, chat gets live reactions and a cleaner attachment experience, and a batch of visible UX fixes across profiles, the lightbox, Explore layouts, and admin settings.

Included in Free. Every item on this page applies to the free version unless marked Pro. Paired with WPMediaVerse Pro 2.2.0 - install and test both together.

Private-community REST gate

When the host community is private (for example a BuddyNext site in private mode), the entire REST surface now requires a logged-in user - including public reads, and including the Pro mvs-pro/v1 namespace (tournament brackets and other public Pro reads) - so nothing leaks to logged-out visitors through the API. Unauthenticated requests get 401 mvs_community_private. The gate is off by default and developer-controllable via the mvs_rest_require_auth and mvs_rest_can_access filters. See REST API: Authentication.

Messaging improvements

  • Live reactions - a reaction added to a message you already received now appears for you in real time, without a page reload.
  • Media-only attachments - chat attachments now accept images, video, and audio only. Documents (PDF, DOC, ZIP) are rejected with a clear message, client- and server-side. Site owners who need documents back can extend the mvs_dm_allowed_file_types filter. See Direct Messages.
  • Upload feedback - attaching a video or audio file shows an "Uploading..." chip with a spinner, and Send waits until the upload finishes - a message can no longer go out without its file.

Profile and lightbox fixes

  • Live follower counts - the follower and following numbers on a member profile update the moment you press Follow or Unfollow, on the default layout and on every Pro layout.
  • Favorite heart - the lightbox Favorite button now fills and turns red when favorited (the state saved correctly before, but the color was stuck).

Explore empty states (Pro layouts)

The Flickr, Pinterest, and Dribbble Explore layouts now show a proper empty state when a search has no results or a tag does not exist - a heading naming the term, a "Browse all media" button, and popular-tag chips - instead of a generic message or, for unknown tags, a silently unfiltered feed. Matches the behavior the default layout has always had.

Admin polish

  • The "Settings saved." notice can be dismissed, fades out on its own after a few seconds, and no longer follows you across settings sections.

Compatibility

  • BuddyBoss and older BuddyPress - fixed a fatal error where bp_get_group_url() does not exist.
  • Requires and pairs with WPMediaVerse Pro 2.2.0 (Pro is a lockstep release: shared competition-media formatting internals, no Pro-only feature changes).

What's New in 2.0.0

WPMediaVerse 2.0.0 is a major release: upload watermarking, a full pass to make the entire frontend translation-ready, privacy hardening across albums, collections, and protected media, and a batch of activity, moderation, and image-pipeline fixes.

Included in Free. Every item on this page applies to the free version. Paired with WPMediaVerse Pro 2.0.0 - install and test both together.

Upload watermarking engine

Every upload now flows through a single, consistent watermark stamp point - at upload time and at file-replace time, so there's no way to bypass it by replacing a file after the fact. WPMediaVerse Free ships the engine and the option schema; WPMediaVerse Pro adds the Settings UI (Watermark Type, Logo, Position, Opacity) and the renderer that actually draws the mark. See Watermarking: Free vs Pro. Watermark stamping now fails closed - if the renderer can't draw the mark, the upload is logged as an error instead of silently serving an un-watermarked original.

Full frontend translation readiness

Every Interactivity API store, the shared-UI store, classic scripts, and the messaging module now ship translatable strings through a standard i18n delivery pipeline. Previously, roughly 165 JavaScript strings inside Interactivity modules were English-locked regardless of the site's language setting - the entire customer-facing frontend is translation-ready now.

Privacy hardening: albums, collections, protected media

  • Album and collection privacy is enforced at the database level, so visitor pagination counts stay correct and private items never leak into a count or a listing.
  • Protected media shows a proper login gate in the media slot instead of a 404, while still returning the correct 403 status to unauthorized requests.
  • Private collection single views and their REST responses are gated - previously they could leak the collection's title and structure to unauthorized viewers.
  • Album and collection privacy-stub rows no longer show up as broken tiles on Explore, the media grid, or the media feed, and their index row is purged on delete.

Explore feed scope filters

The media listing endpoint gains scope=followers and scope=self, working across every layout (grid and every Pro feed layout) - not just the default. See Explore Feed Scope Filters.

Album creation from the upload modal

You can create a new album directly from the upload modal instead of navigating away to create one first. See Media Upload.

Activity, lightbox, and notification fixes

  • Favorite, Save, Share, and Download buttons work in the media lightbox inside the BuddyPress activity stream.
  • Batch uploads to an album now show a "View all photos" link in the activity entry.
  • BuddyPress notifications are cleaned up when media is deleted, and commenting on activity media no longer sends a duplicate notification. See BuddyPress Notifications.
  • Direct-message reactions can be removed and are correctly attributed.
  • Moderation actions no longer corrupt unrelated posts.

Lossless GPS/EXIF strip

GPS location data is stripped from photos without a lossy re-encode, preserving image quality and the colour profile.

Moderation default changed to Flag

The When AI Flags Content default changed from Delete permanently to Flag for review, so a fresh install can never auto-delete uploads before an admin has chosen otherwise. See AI & Moderation Settings.

Admin menu rename: "Media Moderation"

The admin sidebar label Moderation is now Media Moderation, to avoid ambiguity with WordPress's general moderation terminology. See AI & Moderation Settings.

App pages use the theme's no-sidebar template

App pages (Dashboard, Explore, Upload, and similar plugin-owned pages) now use the active theme's no-sidebar page template for a cleaner full-width layout, when the theme provides one.

BuddyNext: media links open their activity post

When BuddyNext is active, media links and /media/{slug}/ URLs redirect to the BuddyPress activity entry the media was posted in, instead of rendering as a separate public page. A BuddyNext-side settings toggle switches back to dedicated media pages if you want a standalone page per item. See Activity Stream Media.

Under the hood

New filter mvs_suppress_bp_comment_notification controls the BuddyPress comment-notification bridge (#bp-notifications-mvs)). MediaRepositoryInterface now declares get_url_for_viewer() for viewer-aware URLs to the full original file. Filterable avatar (mvs_user_avatar_url) and profile-link (mvs_user_profile_url) seams were added for BuddyNext integration. The dead watermark preview/serve system and the 1.9.0 admin access-rules UI were both removed - access-rule enforcement continues in the backend via the REST API.

Upgrade notes

  • Pairs with WPMediaVerse Pro 2.0.0 - install and test both together.
  • No manual action needed for the privacy or watermark changes - they apply automatically on update.
  • If you relied on the 1.9.0 access-rules admin screen, switch to managing access rules via the Privacy & Access Control REST API.
  • If your site prefers standalone media pages under BuddyNext, use BuddyNext's settings toggle after updating (the new default sends media links to the activity feed).

What's New in 1.9.0

WPMediaVerse 1.9.0 adds interests-based onboarding, a big native-app readiness push across the REST API, and several messaging and profile improvements, alongside a batch of QA-driven fixes.

Included in Free. Every item on this page applies to the free version. Pro picks up these improvements automatically because Pro builds on top of free.

Interests & personalized onboarding

New members can choose interests, get a ranked list of suggested people to follow, and request an interest-aware feed. See Interests & Personalized Onboarding for the full REST API.

Native-app readiness

A broad pass across mvs/v1 makes the API fully drivable without a browser session: the /app/config endpoint, the interest endpoints, and viewer-aware fields on media responses. This is the foundation the official MediaVerse app and any headless client build on.

Direct messages: search and moderation

  • Per-conversation message search - search message content within a single conversation via GET /conversations/{id}/messages/search.
  • Content moderation on DMs - messages now pass through the same mvs_message_content_check filter seam that guards other content types, so a host running a word-blocklist for posts and comments can apply it to DMs too.

See Direct Messages.

Followers/following modal + profile overflow menu

Follower and following counts on a profile now open a modal listing the users, instead of requiring a separate page. Member profiles also gain an overflow menu with Report.

Manage blocked members from Edit Profile

Blocked members can now be reviewed and unblocked from the Edit Profile screen, in addition to the REST API. See User Blocking & Reporting.

"Also share as a story" on upload

The upload flow gains an option to also share the upload as a story at the same time, instead of uploading and then separately posting to Stories.

Access-rules admin screen (retired in 2.0.0)

1.9.0 added an admin screen for managing per-media access rules, with the watermark display wired to the site's configured logo. This screen was retired in 2.0.0 - access-rule enforcement stays in the backend (REST API and AccessRulesService), and rules are managed via the Privacy & Access Control REST API rather than a dedicated admin UI. If you're running 1.9.0, this page still exists; update to 2.0.0 and use the REST API going forward.

AI moderation controls (carried from 1.8.0)

The Anthropic (Claude) provider, owner-configurable AI Flag Criteria, and Custom Flag Terms shipped in 1.8.0 - see What's New in 1.8.0 and AI & Moderation Settings.

Masonry default (carried from 1.8.0)

The Original-proportions masonry grid became the default Thumbnail Style in 1.8.0; 1.9.0 confirms this as the default Explore grid style with the same escape hatch back to the fixed square grid (Settings > Display, or the mvs_default_thumbnail_style filter).

Theme templates and child themes

Child themes can now override MediaVerse templates - member partials, messages, and layout pages resolve theme-first before falling back to the plugin's own templates. See Template Overrides.

When BuddyNext is active, media links open the activity they were posted in by default instead of a standalone media page. See Activity Stream Media (documented in full under the 2.0.0 lockstep behavior, since the filter this relies on is unchanged across both releases).

Smaller fixes

  • Media comments no longer leak onto unrelated posts or pages (existing comments repaired automatically on update).
  • Competition pages (Compete, Battles, Challenges, Tournaments) keep their MediaVerse styling when BuddyNext is active.
  • Smart collections report their real item count in the list.
  • AI moderation now runs when only Auto-Moderate is enabled (it was being skipped before).
  • Theme-defense fixes for BuddyX and Reign (tab text, lightbox open); Instagram action buttons hardened against theme button styles.
  • Cloud storage display and migration are more robust across admin operations.

Under the hood

New wp mvs cert functional certification engine - a boot smoke test across every REST route plus dead-toggle oracles, gated at 100% coverage in CI. All WordPress.org Plugin Check errors resolved (30 to 0).

Upgrade notes

  • Pairs with WPMediaVerse Pro 1.9.0 - install both updates together.
  • If you use the 1.9.0 access-rules admin screen, plan to move to the REST API workflow before or during your 2.0.0 update.

What's New in 1.8.0

WPMediaVerse 1.8.0 adds configurable AI moderation with a new Claude provider, working image and text watermarks, a masonry media grid as the new default, member blocking from the frontend, an Integrations page for the Wbcom plugin family, and a long list of QA-driven fixes across storage, video, and theme compatibility. No database schema change.

Included in Free. Every item on this page applies to the free version unless marked Pro. Pro picks up these improvements automatically because Pro builds on top of free.

Integrations page

A new WPMediaVerse > Integrations admin page lists the Wbcom plugin family with product logos, short descriptions, and store links, plus a one-click companion installer that installs and activates a companion plugin without leaving the page. See Integrations.

Member blocking from the frontend

Members can block or unblock another member directly from their profile - a Block toggle next to Follow and Message. Blocking already hid a blocked member's media and refused their follows and DMs; this release adds the missing frontend control to actually do it. See User Blocking & Reporting.

Configurable AI moderation + Claude provider

  • AI Flag Criteria checkboxes let you choose which categories the AI flags (nudity, violence, hate, self-harm, drugs, spam), plus a Custom Flag Terms field for your own terms (e.g. weapons, gambling, political content). All categories are on by default.
  • Claude (Anthropic) joins OpenAI, Google Vision, and AWS Rekognition as a selectable AI provider.
  • Delete permanently is now an option for When AI Flags Content - removes the media and its files from local and cloud storage.
  • The AI settings tab shows only the selected provider's credentials, and the per-call cost is plugin-managed instead of a manual field.

See AI & Moderation Settings.

Masonry grid is now the default

The Explore and media-grid feed now shows every image at its native aspect ratio by default (masonry, no cropping or gaps). Grid tiles also serve a larger thumbnail so they stay sharp on HiDPI/retina screens. Sites that prefer the uniform square crop can restore it in Settings > Display, or site-wide with the mvs_default_thumbnail_style filter. See Display Settings.

Watermark fixes

Selecting Watermark Type "Image" now applies the configured logo (it was being dropped before). Restricted (gated) images show the watermarked preview to visitors without access, instead of a plain blurred thumbnail - watermarking applies to images only, never video or audio.

Video posters everywhere

Video tiles now show the video's real first frame - instead of a generic placeholder - on My Media, the explore-feed block, and the Pinterest/Flickr/Dribbble/Instagram layout grids, including Load More, matching the Explore grid. All client-side thumbnail renderers now follow the same video-first contract as the server.

Storage display and migration fixes

  • Media uploaded before a cloud service was connected now displays from where it actually lives, instead of a fabricated CDN URL that 404'd.
  • Migrating storage moves a media's thumbnails and other variants alongside the original, and only flips the media to the new service once the full set has transferred.
  • "Free up server space" never deletes a local file whose cloud copy can't be verified.
  • Storage paths left inconsistent by earlier versions are repaired automatically in the background after update - small batches, only affected media. Disable with the mvs_storage_repair_enabled filter or run manually with wp mvs repair-storage.

Theme and UI fixes

  • The Explore lightbox now opens in place instead of stacking over the previous screen.
  • Explore and the dashboard share one REST client with document-level event delegation, so Load More and other actions keep working after client-side navigation.
  • Dark mode follows the active BuddyX/Reign theme toggle.
  • Like, comment, bookmark, share, and other action buttons on the Instagram layout run their action instead of opening the lightbox.
  • The dashboard, create-modal, chat tabs, and lightbox action buttons keep their flat, readable styling under themes that restyle plain buttons (BuddyX, Reign).
  • Lightbox Share now works on non-HTTPS sites via a fallback copy method.
  • Paginated Explore and member profile pages (page 2+) return HTTP 200 instead of a soft 404, so search engines keep them indexed.

Smaller fixes

  • Smart collections with more than one rule of the same type now match with OR instead of AND (e.g. tag Image OR tag Video), so they return items instead of resolving to zero.
  • "Followers only" online-status visibility now actually hides presence from non-followers.
  • The story viewer plays video and audio stories correctly instead of showing a broken image.
  • Direct-message attachments are scoped to their conversation so only participants can view them.
  • REST POST /conversations accepts an as_request flag so a message can open as a pending request instead of landing directly in the inbox.

Under the hood

New filters: mvs_ai_moderation_terms, mvs_ai_cost_per_call, mvs_storage_repair_enabled, mvs_default_thumbnail_style, mvs_strip_dead_bp_links, mvs_dead_bp_link_patterns, mvs_dm_denial_message, mvs_dm_denial_reason, mvs_collections_enabled. New UploadService::sideload_external_file() for bringing outside files into the library. The frontend was refactored onto a shared window.mvsRest client plus a router store for client-side navigation. All WordPress.org Plugin Check errors resolved (30 to 0, continued into 1.9.0).

Upgrade notes

  • Pairs with WPMediaVerse Pro 1.8.0 - install both updates together.
  • No database migration runs for this release.
  • If your site relies on the square-crop grid, set Thumbnail Style back to Square in Settings > Display after updating (the default flipped to Original proportions).

What's New in 1.5.0

WPMediaVerse 1.5.0 closes two privacy and access bugs around non-public media, heals video posters that were stored with the wrong path before this version, and unifies the upload-and-serve pipeline so the underlying bug pattern cannot recur. There are no new customer-facing features; this is a correctness and robustness release.

Included in Free. Every item on this page applies to the free version. Pro picks up these fixes automatically because Pro builds on top of free - Pro 1.5.0 itself ships no code changes, it is a lockstep version bump. Install both updates together when running Pro.

Non-public uploads render their own thumbnails

Members, Friends, Only Me, Group, and Custom-access media no longer return a 403 for their own thumbnails after upload. The owner, and any viewer who has been granted access, now sees the thumbnail correctly - on local storage and on every cloud driver (BunnyCDN, S3, R2, Spaces).

Private uploads leave zero public footprint

Private media is now fully invisible to everyone except its owner:

  • No BuddyPress activity entry is created for a private upload.
  • The profile activity tab no longer surfaces broken thumbnail cards for private items.
  • The profile media tab badge no longer counts private items when someone else is viewing.
  • The Explore grid stays clean of private media.

Other non-public levels (Members, Friends, Group, Custom) keep their normal audience-discovery behavior - this change is specifically about media marked Private.

Legacy video posters heal automatically

Videos and audio uploaded before 1.5.0 could store a poster path pointing at the wrong subdirectory, which showed up as a broken still frame on cards, in the lightbox, and in feed previews. Database migration v15 runs once on the first page load after updating and re-derives the correct poster path from the on-disk file location. It is idempotent and safe to let run.

For cloud-storage sites, the migration is cloud-aware: it respects CDN-authoritative paths so existing cloud video posters keep resolving. If a poster still looks broken after the update, run wp mvs cloud-thumbs-backfill to push any local-only poster variants to the active cloud driver.

One unified read path for media URLs

Theme overrides and shortcode users can now call the same Core\MediaUrl helper that the plugin's own templates use, so custom integrations no longer have to know anything about the signed-URL plumbing. See Template Overrides for how to use it from a theme.

One unified upload pipeline

Image variants, video posters, audio cover art, and WebP/AVIF siblings now all flow through a single writer, so the file layout is identical for every media type. WebP and AVIF generation in particular was collapsed into one shared publisher, removing a duplicate-write footgun where the two paths could disagree about the destination directory. Adding a new output format in the future is now one extension point instead of five.

Thumbnails embedded in long-lived surfaces (notification emails, RSS) are minted with a one-hour access TTL. Sites that cache those surfaces at the CDN for longer can widen the window with the new mvs_broadcast_thumbnail_ttl filter.

Under the hood

The upload and read pipeline is now backed by five focused services - MediaUrl, VariantSpec, StorageRouter, MediaVariantWriter, and PosterService. Existing methods are kept as shims for at least two releases per the deprecation policy, so custom code calling the old paths keeps working.

Upgrade notes

  • Update Free to 1.5.0 first, then Pro to 1.5.0 if you run it.
  • Migrator v15 runs automatically on the first request after the update. No manual action is needed.
  • Cloud-storage sites with pre-1.5.0 video uploads: if any poster still appears broken after the update, run wp mvs cloud-thumbs-backfill.

What's New in 1.4.0

WPMediaVerse 1.4.0 is a robustness release that hardens cloud storage, gives site owners precise control over AI spend, and finishes a full mobile and translation pass. This page also folds in the headline additions from 1.3.0 (the image optimization pipeline) so you have a single place to catch up.

Included in Free. Every item on this page applies to the free version unless marked Pro. Pro picks up these improvements automatically because Pro builds on top of free.

Storage that follows the media, not a toggle

Each media item is now served from where it is actually stored, combined with its privacy. Switching the active storage driver, or turning a cloud service on, no longer breaks media you uploaded earlier. Public media stored on cloud serves directly from the CDN. Local media serves through the plugin's own delivery route. There is nothing to toggle: the old "Serve public cloud media directly" checkbox has been retired because direct CDN serving for public media is now automatic. See Cloud Storage for the full behavior.

Private media stays on local disk (Pro)

Only public media is eligible for the cloud. Private and restricted uploads, including their thumbnails and generated variants, never leave your local disk. Each item is effectively "cloud or local" based on its own privacy, so a private photo is never copied to a CDN.

Tighter access checks on private media

The delivery route re-verifies the viewer's permission on every request for non-public media, so a shared link cannot be used to leak a private item. If a Cloudflare R2 bucket has no public domain configured, WPMediaVerse will not emit a raw bucket URL (those are never public) and falls back to serving locally.

Per-feature control over AI

AI is opt-in and off by default, and you now choose exactly which AI features run and what they cost:

  • Generate Descriptions and Generate Tags are independent toggles under Auto-Analyze, so you can run one without the other.
  • The AI Provider dropdown correctly switches between OpenAI, Google Vision, and AWS Rekognition (Pro). Earlier builds could silently fall back to OpenAI.
  • The Monthly AI Budget now caps moderation calls too, not just analysis and tagging. A fresh install ships with a conservative $10 cap so AI never runs against an unbounded bill before you set a budget.

See AI & Moderation Settings for the full matrix.

Image optimization pipeline (added in 1.3.0)

Uploads are now optimized losslessly: file sizes shrink with no visible quality loss, and an original is never made larger (if there is no gain, the original is kept). Animated GIFs are detected and left untouched. WebP copies are generated by default and AVIF copies are available as an opt-in, and browsers that support those formats receive the smaller file automatically across the explore grid, BuddyPress activity, dashboard, single media, and the lightbox. The All Media admin screen gains an Optimization column with savings badges and per-row Optimize and Details actions. See Image Optimization.

Reliable moderation queue actions

The per-row Approve and Reject buttons in the Moderation Queue work correctly again. They were previously dead because of nested forms in the markup.

100% mobile ready

Every customer-facing surface was verified at a 390px viewport. Explore tag chips, the upload privacy selector, and the lightbox close and gallery navigation controls were all raised to the 44px touch target floor, and the layouts produce zero horizontal overflow on a phone.

100% translation ready

Every user-facing string across the chat and messaging templates is wrapped for translation, the BuddyPress activity media script is bridged for JavaScript translations, and the translation template files were regenerated. The plugin is ready for full localization out of the box.

What's New in 1.2.0

WPMediaVerse 1.2.0 is a UX completeness release focused on filling gaps surfaced by real-world use. Twelve customer-visible improvements ship together, plus a full WCAG 2.1 AA accessibility pass.

Included in Free - Every item on this page applies to the free version. Pro picks up these improvements automatically because Pro builds on top of free.

Member Photos block + shortcode

A new mvs/member-photos Gutenberg block and matching [mvs_member_photos] shortcode render any member's media grid. The block auto-detects the right user - explicit userId attribute first, then the BuddyPress displayed user, then the post author, then the current user - so the same block can be dropped into a profile template, a member-specific page, or an author archive without any wiring.

PDF Viewer block + shortcode

A new mvs/pdf-viewer block and [mvs_pdf_viewer] shortcode embed PDFs using the browser-native viewer (the #view=FitH URL fragment). Configurable height, optional toolbar, five distinct empty states (no media, wrong type, missing file, access denied, generic). No third-party JS, no plugins to install, no licensing fuss.

Sort options on Media Grid

The Media Grid block and [mvs_gallery] shortcode now support four new sort orders - Most Popular, Most Viewed, Most Reactions, and Random - alongside the existing Date and Title sorts. A new direction toggle (ascending / descending) and a per-author filter (userId) make the grid usable as a "Top media by this member" surface.

Search autocomplete on Explore

The Explore page (and [mvs_explore_feed] shortcode) now shows a type-ahead dropdown as you type in the search box - top eight title matches, debounced, with full keyboard navigation (Arrow keys, Enter, Escape) and ARIA combobox semantics for screen reader users.

The lightbox toolbar gains two new buttons - Download (writes to media stats; gated by the new global Allow Downloads toggle) and Fullscreen (also bound to the F keyboard shortcut). The Share button's third-popup window.prompt() fallback is gone; it's now navigator.share → clipboard copy → toast on failure.

Per-media Edit modal

The settings cog on dashboard cards opens a prefilled edit modal - change title, description, privacy level, and per-media Allow Downloads in place. No more clicking through to a separate edit screen for a quick rename.

Open Graph + Twitter Card meta

/media/{slug}/ URLs now emit og:* and twitter:card=summary_large_image meta tags. Paste a media URL into Slack, Twitter, LinkedIn, or Discord and it unfurls with title, description, and the cover image - no extra plugin, no SEO setup.

Upload modal polish

Preview tiles in the upload modal now show the filename (truncated tastefully) and a per-tile remove (×) button. Audio files get a proper audio-fallback icon instead of a broken thumbnail. The tags input gains a row of eight popular tag pills below it - click to append, no duplicates.

Bulk Actions in admin All Media

The Media > All Media admin screen now supports multi-select. Tick the rows you want, pick Move to Trash / Restore / Delete permanently from the bulk-actions dropdown, and confirm. Context-aware - the available actions change based on whether you're viewing trashed or live media.

Chat panel visibility setting

A new Chat Panel Visibility dropdown under Media > Settings > Social lets you scope where the floating chat panel renders: Everywhere (default), WPMediaVerse pages only, BuddyPress pages only, or Disabled. For code-level overrides there's the mvs_should_render_chat_panel filter.

Global Allow Downloads toggle

A single switch under Media > Settings > Display hides the lightbox Download button site-wide and refuses the download REST endpoint. Pair with the per-media Allow Downloads checkbox in the Edit modal for fine-grained control.

WCAG 2.1 AA pass

Every interactive surface added or touched in 1.2.0 was audited against WCAG 2.1 AA - aria-label (sentence-form) on icon buttons, aria-pressed on toggles, :focus-visible outlines on every focusable element, role="tablist" + role="tab" on segmented toggles, decorative emoji marked aria-hidden. Keyboard navigation works everywhere mouse navigation works.

Settings

Configure every aspect of your media platform.

General Settings

Access these settings at WPMediaVerse > Settings > General.

General settings tab

General Section

Option Default Description
Max Upload Size 100 MB Maximum file size per upload. Enter value in MB. The plugin reads this setting server-side - WordPress's upload_max_filesize PHP ini value also applies.
Allowed File Types image/jpeg, image/png, image/gif, image/webp, video/mp4, video/webm, audio/mpeg, audio/ogg Comma-separated list of allowed MIME types.
Default Privacy Level Public The privacy level assigned to new uploads when the user does not choose one. Options: Public, Members Only, Private.
Duplicate Detection Warn (allow upload) What to do when a user uploads a file with a SHA-256 hash matching an existing media item. Options: Warn (allow upload), Skip (reject duplicate), Allow (no check).
Strip EXIF Data Enabled When enabled, removes GPS coordinates and device information from uploaded JPEG images before storing them.

Storage Section

Option Default Description
Storage Driver Local (WordPress uploads) Where uploaded files are stored. Local stores files in wp-content/uploads/wpmediaverse/YYYY/MM/. Amazon S3 and BunnyCDN require WPMediaVerse Pro.
Signed URL Expiry 3600 (1 hour) How long temporary signed URLs remain valid for private media files. Set in seconds.

Changing the storage driver only affects new uploads. Existing files remain in their original storage location. A notice appears after saving if the driver is changed.

Pages Section

Assign existing WordPress pages to WPMediaVerse page roles. WPMediaVerse uses these assignments to generate links in the navigation, chat panel, and notification emails.

Option Option Key Description
Dashboard Page mvs_page_dashboard The main logged-in user dashboard. Displays the follow feed, quick upload, and activity summary.
Explore Page mvs_page_explore The public media browse archive. Used as the landing page for non-logged-in visitors.
Upload Page mvs_page_upload The dedicated upload form page. Linked from the dashboard and the navigation bar.

Create a standard WordPress page for each role, then select it from the corresponding dropdown. Each page should contain only the matching WPMediaVerse shortcode ([mvs_dashboard], [mvs_explore], [mvs_upload]) with no other content.

If a page assignment is empty, WPMediaVerse falls back to the site home URL for that link. Set all three pages to avoid broken navigation.

Display Settings

Access these settings at WPMediaVerse > Settings > Display.

Display settings tab showing grid and thumbnail options

Media Display Section

Option Default Description
Grid Columns 3 Number of columns in the media grid. Applies to [mvs_gallery], the Media Grid block, and the explore archive. Options: 2, 3, 4, 5 columns.
Items Per Page 12 Number of media items loaded per page. Options: 12, 24, 48.
Thumbnail Style Original proportions (masonry) Controls the aspect ratio of grid thumbnails. Square crops images to a 1:1 ratio. Original proportions preserves the file's native aspect ratio, packed into a masonry layout. The default flipped from Square to Original proportions in 1.8.0; restore the old square-crop default site-wide with the mvs_default_thumbnail_style filter.
Allow Downloads On Master toggle for the lightbox Download button. When off, the button is hidden site-wide and the /mvs/v1/media/{id}/download REST endpoint refuses requests. Per-media Allow Downloads (set in the per-media Edit modal) is still honoured when this master toggle is on.

The full-screen lightbox includes a toolbar with three quick-action buttons.

Button Behaviour
Download Streams the original file to the user and increments the mvs_media_stats.downloads counter. Hidden when Allow Downloads is off (either site-wide or per-media). Rate-limited to 30 requests per minute per user.
Fullscreen Expands the lightbox to fill the viewport using the browser Fullscreen API. Also bound to the F keyboard shortcut.
Share Uses navigator.share where supported, falls back to copying the URL to the clipboard, falls back to a toast error. No window.prompt() fallback.

All three buttons carry aria-label text and :focus-visible outlines for keyboard users.

Lightbox with toolbar showing download, fullscreen, share, and reaction controls

How Display Settings Interact with Shortcodes

The [mvs_gallery] shortcode deliberately uses the backend settings for Grid Columns and Items Per Page rather than letting shortcode attributes override them. This ensures consistent display across your site.

The [mvs_album] shortcode allows you to override the column count directly:

[mvs_album id="123" columns="4"]

Thumbnail Generation

WPMediaVerse generates thumbnails for uploaded images using WordPress's built-in image editor. Thumbnails are stored alongside the original file in the wpmediaverse/YYYY/MM/ upload directory.

For video files, a poster is extracted from the first frame (ffmpeg, with a getID3 cover-atom fallback). As of 1.8.0, every grid, feed, and layout (My Media, Explore, the explore-feed block, and the Pinterest/Flickr/Dribbble/Instagram layouts, including Load More) shows this real first-frame poster instead of a generic video placeholder icon.

Watermarking: Free vs Pro

WPMediaVerse Free ships the watermark engine: WatermarkService resolves whether an upload should be stamped and fires the mvs_watermark_stamp_file filter at upload time and at file-replace time (so there's no bypass via the replace endpoint), before any thumbnail or WebP/AVIF variant is cut. The underlying option schema (watermark type, text, logo, position, opacity) also ships in Free with safe defaults, all off by default.

What Free does not include is the Settings UI to configure those options, or the GD code that actually draws the mark into the image. WPMediaVerse Pro adds both: the Watermark section on Media > Settings > Display, where you pick Enable Watermark, Apply to (all uploads or specific roles), Watermark Type (Text or Image), Watermark Text/Image, Position, Opacity, and Text Size/Color - and the renderer that hooks mvs_watermark_stamp_file to draw it. Without Pro active, the engine has nothing registered to draw with, so uploads are never watermarked.

Permissions

Access these settings at WPMediaVerse > Settings > Permissions.

Permissions tab showing role-capability matrix

Custom Capabilities

WPMediaVerse registers the following custom WordPress capabilities:

Capability Description
upload_mvs_media Upload new media files
edit_mvs_media Edit own media posts
edit_others_mvs_media Edit media posts created by other users
delete_mvs_media Delete own media posts
delete_others_mvs_media Delete media posts created by other users
moderate_mvs_media Access the moderation queue and approve/reject media
manage_mvs_settings Manage WPMediaVerse settings (finer-grained grant checked by settings actions; the admin menu itself gates on core manage_options)
manage_mvs_access Manage custom access grants for private media
read_mvs_media View media (used for private media visibility checks)
publish_mvs_media Publish media posts immediately (without pending review)

Default Role Assignments

Role upload edit own delete own moderate manage settings
Administrator Yes Yes Yes Yes Yes
Editor Yes Yes Yes Yes No
Author Yes Yes Yes No No
Contributor Yes Yes Yes No No
Subscriber Yes No No No No

Changing Permissions

  1. Go to Media > Settings > Permissions.
  2. Check or uncheck capabilities for each role.
  3. Click Save Permissions.

The page shows how many roles were updated. If no changes were needed, it shows "No changes were needed."

Granting Capabilities Programmatically

You can grant or revoke capabilities in your theme's functions.php or a custom plugin:

// Grant moderation capability to a custom role.
$role = get_role( 'shop_manager' );
if ( $role ) {
    $role->add_cap( 'moderate_mvs_media' );
    $role->add_cap( 'manage_mvs_access' );
}

Important Notes

  • The moderate_mvs_media capability grants access to the moderation queue and the ability to see all media regardless of privacy level.
  • The WPMediaVerse admin menu pages (Settings, Moderation, Logs, etc.) are registered against the core manage_options capability. The manage_mvs_settings capability is a finer-grained grant some settings actions and dashboard links check, so it can be assigned to non-administrator roles that should manage WPMediaVerse without full manage_options access.
  • Capabilities are stored in the WordPress wp_user_roles option and persist after plugin deactivation.

AI & Moderation Settings

Access these settings at WPMediaVerse > Settings > AI & Moderation.

AI and Moderation settings tab

AI Features Section

AI is opt-in and off by default. Nothing calls an AI provider until you (1) supply an API key and (2) turn on at least one of the toggles below. There is no separate "enable AI" master switch because a missing key already disables every AI feature - you stay in full control of what runs and what it costs.

Option Default Description
AI Provider OpenAI (GPT-4 Vision) The AI service used for image analysis, tagging, and moderation. Free version: OpenAI only. Pro adds Google Vision, AWS Rekognition, and Claude (Anthropic) (added in 1.8.0, selectable from this dropdown).
OpenAI API Key (empty) Your OpenAI API key. You can also define MVS_OPENAI_API_KEY in wp-config.php instead.
OpenAI Model GPT-4o Mini Model used for analysis calls. GPT-4o Mini is cheaper; GPT-4o provides higher quality results.
Auto-Analyze Uploads Off Master switch for the two per-feature toggles below. When off, neither descriptions nor tags are generated on upload.
Generate Descriptions On When Auto-Analyze is on, use AI to generate a description / alt text for each upload. Turn off to skip description calls and only generate tags.
Generate Tags On When Auto-Analyze is on, use AI to suggest tags for each upload. Turn off to skip tag-suggestion calls.
Auto-Apply Tags Off When enabled, AI-suggested tags are automatically assigned to the mvs_tag taxonomy. Requires Generate Tags to be on.
Auto-Moderate Uploads Off When enabled, each new upload is checked for policy violations. The action taken depends on the When AI Flags Content setting below.
Monthly AI Budget ($) $10 Hard cap on AI spend per calendar month, covering analysis, tagging and moderation calls. When the cap is reached, all AI calls stop until the next month. Set to 0 for unlimited spend - recommended only after you configure a billing alert on the provider account itself.

Estimated cost per call is not a settings-page field - it is a developer-only default ($0.01) used for budget tracking, overridable via the mvs_ai_cost_per_call filter.

AI Flag Criteria and Custom Flag Terms (New in 1.8.0)

Option Default Description
AI Flag Criteria All 6 categories on Checkbox group controlling which content categories the AI flags: Nudity / sexual content, Violence / gore, Hate / harassment, Self-harm, Drugs, Spam. Unchecking all categories restores every category (the rule can never go blank).
Custom Flag Terms (empty) Optional comma-separated terms the AI should also flag beyond the built-in categories - e.g. weapons, gambling, political content, competitor logos. Narrated to the AI alongside the categories above.

Choosing exactly which AI features run

Each toggle is independent so a site owner enables only what they want to pay for:

  • Descriptions only - Auto-Analyze on, Generate Descriptions on, Generate Tags off.
  • Tags only - Auto-Analyze on, Generate Descriptions off, Generate Tags on (add Auto-Apply Tags to write them to the taxonomy automatically).
  • Moderation only - leave Auto-Analyze off and turn on Auto-Moderate; uploads are scanned for policy violations without generating descriptions or tags.

The budget cap applies to all of the above, so moderation calls also stop once the monthly cap is hit.

Setting the API Key via wp-config.php

// wp-config.php
define( 'MVS_OPENAI_API_KEY', 'sk-your-key-here' );

When this constant is defined, the settings page field is disabled and shows a notice.

Moderation Section

Option Default Description
When AI Flags Content Flag for review What happens when AI detects a policy violation. Options: Flag for review (keeps media visible but adds it to the moderation queue), Hide (sets media to private), Reject (moves media to draft), Delete permanently (removes the media and its files from local and cloud storage - cannot be undone). The default changed from Delete permanently to Flag for review in 2.0.0, so a fresh install can never auto-delete uploads before an admin has chosen otherwise.
Auto-Hide Threshold 3 Number of user reports required to automatically hide a media item. The media is set to private and added to the moderation queue. Set to 0 to disable automatic hiding.

Moderation Queue

Administrators with the moderate_mvs_media capability can review flagged media at WPMediaVerse > Media Moderation (renamed from "Moderation" in 2.0.0 to avoid ambiguity with the general WordPress term).

Moderation queue with pending media items

The queue shows:

  • Media flagged by AI
  • Media that reached the auto-hide threshold from user reports
  • Media manually flagged by moderators

Log Viewer

The AI & moderation activity log is available at WPMediaVerse > Logs. It shows each AI call, the result, estimated cost, and any action taken.

AI log viewer showing analysis results

Budget Alerts

When monthly AI spend reaches 80% of your budget, WPMediaVerse adds an admin notice. When the budget is fully consumed, all AI calls - analysis, tagging, and moderation - are suspended and a warning appears on the settings page. Because a fresh install ships with a conservative $10 default cap, AI never silently runs against an unbounded bill before you have chosen a budget.

Webhooks

Access these settings at WPMediaVerse > Settings > Webhooks.

Webhooks allow WPMediaVerse to send signed HTTP POST notifications to external services when media events occur.

Webhooks settings tab showing webhook URL list

Supported Events

Event Triggered When
media.uploaded A new media file is successfully uploaded
media.updated An existing media post is saved/updated
media.deleted A media post is permanently deleted
media.moderated A media item's moderation status changes (approved, flagged, rejected)
media.reaction A user adds a reaction to a media item
media.comment A user posts a comment on a media item

Adding a Webhook

  1. Go to WPMediaVerse > Settings > Webhooks.
  2. Click Add Webhook.
  3. Enter the URL that should receive the POST request.
  4. Select which events should trigger this webhook.
  5. Optionally enter a secret key for signature verification.
  6. Click Save Webhooks.

Add webhook form with URL field and event checkboxes

Payload Format

All webhook payloads are sent as JSON via HTTP POST with these headers:

Content-Type: application/json
X-MVS-Event: media.uploaded
X-MVS-Delivery: <uuid>
X-MVS-Signature: sha256=HMAC_SIGNATURE

The X-MVS-Signature header is only sent when a secret key is configured for the webhook.

Example: media.uploaded Payload

{
  "event": "media.uploaded",
  "timestamp": "2025-03-27T12:00:00Z",
  "site_url": "https://yoursite.com",
  "data": {
    "media_id": 123,
    "title": "My Photo",
    "author": 1,
    "url": "https://yoursite.com/media/my-photo/",
    "file_url": "https://yoursite.com/wp-content/uploads/wpmediaverse/2025/03/photo.jpg",
    "file_type": "image",
    "privacy": "public",
    "status": "publish"
  }
}

Verifying Webhook Signatures

Use the secret key you configured to verify that requests are from WPMediaVerse:

$payload   = file_get_contents( 'php://input' );
$signature = $_SERVER['HTTP_X_MVS_SIGNATURE'] ?? '';
$secret    = 'your-webhook-secret';

$expected = 'sha256=' . hash_hmac( 'sha256', $payload, $secret );

if ( ! hash_equals( $expected, $signature ) ) {
    http_response_code( 401 );
    exit( 'Invalid signature' );
}

Retry Behavior

Failed webhook deliveries (5xx HTTP response or connection error) are retried up to 2 times via Action Scheduler, after 5 minutes and then 10 minutes. 4xx client-error responses are not retried. After the final attempt the failure is recorded in the mvs_webhook_failures option.

Social & Messaging Settings

Access these settings at WPMediaVerse > Settings > Social.

Social and Messaging settings tab

These settings control who can send direct messages and online presence visibility.


Direct Messages

Option Key Default Description
Who Can Send DMs mvs_dm_access everyone Controls who is allowed to send direct messages to other users on the site. Options: everyone, followers (people who follow the recipient; others go to Requests), mutual (only mutual followers), nobody (DMs disabled).
Minimum Account Age (days) mvs_dm_min_age 0 Number of days a user account must exist before that user can initiate new conversations. Set to 0 to disable the restriction.
Online Status Visibility mvs_show_online_status everyone Who can see when a user is currently online in the messaging interface. Options: everyone, followers, nobody.
Chat Panel Visibility mvs_chat_panel_visibility everywhere Where the floating chat panel renders. Options: everywhere, mvs_pages (WPMediaVerse pages only - Explore, Dashboard, Albums, Member Profiles), bp_pages (BuddyPress member + group pages only), disabled. For code-level overrides, hook into the mvs_should_render_chat_panel filter - return false to suppress the panel on a specific request.

Setting Who Can Send DMs here applies site-wide. Users cannot override this individually at this time.


Defining Options via wp-config.php

You can lock any Social setting as a constant to prevent admin changes:

// wp-config.php
define( 'MVS_DM_ACCESS', 'followers' );        // Lock DM access to followers only.
define( 'MVS_DM_MIN_AGE', 7 );                 // Require 7-day-old accounts to send DMs.
define( 'MVS_SHOW_ONLINE_STATUS', false );      // Disable online status site-wide.

When a constant is detected, the corresponding field on the settings page is shown as read-only.

Settings Reference

Every option WPMediaVerse and WPMediaVerse Pro register, with its option key, type and default.

The other pages in this section explain settings in the order you meet them in the admin screens. This page is the complete index: use it when you need to set an option in code, seed a site with WP-CLI, or check what a value defaults to before you change it.

Option keys are stored as normal WordPress options, so anything here can be read or written with get_option() / update_option() or the WP-CLI equivalents:

wp option get mvs_default_privacy
wp option update mvs_items_per_page 24

Defaults apply only when the option row is absent. Saving a settings screen writes every field on that screen, including the ones you did not touch. After the first save an option holds a real stored value, and changing the default in a later release will not move it.

Free options

Registered by Admin\Settings\SettingsRegistrar and Admin\Settings\AiSettingsRegistrar.

Uploads and storage

Option key Setting label Type Default
mvs_max_upload_size Max Upload Size integer 104857600 (100 MB, in bytes)
mvs_allowed_file_types Allowed File Types string image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,audio/mpeg,audio/ogg
mvs_duplicate_action Duplicate Detection string warn
mvs_strip_exif Strip EXIF Data boolean true
mvs_storage_driver Storage Driver string local
mvs_signed_url_ttl Signed URL Expiry (seconds) integer 3600
mvs_cloud_direct_public_urls (no settings field - set in code) boolean false

mvs_duplicate_action accepts warn (allow the upload and warn), skip (reject the duplicate) or allow (skip the hash check entirely). mvs_strip_exif removes GPS and device data from JPEGs at upload time; it does not rewrite files already stored.

Privacy and permissions

Option key Setting label Type Default
mvs_default_privacy Default Privacy Level string public
mvs_allow_user_privacy Allow Users to Set Privacy boolean true
mvs_allow_downloads Allow Downloads boolean true
mvs_app_password_login App Sign-In boolean true

mvs_app_password_login controls whether members may exchange their WordPress password for an Application Password to sign in to a mobile app. Turn it off when you require everyone through the interactive login - for example when you enforce two-factor authentication. Also filterable at runtime via mvs_app_password_login_enabled.

mvs_default_privacy is the level applied when the uploader makes no choice. When mvs_allow_user_privacy is off, the privacy select is removed from the upload form and every upload takes the default.

Display

Option key Setting label Type Default
mvs_grid_columns Grid Columns integer 3
mvs_items_per_page Items Per Page integer 12
mvs_thumbnail_size Thumbnail Quality string medium
mvs_thumbnail_style Thumbnail Style string original
mvs_large_image_size Large Image Size integer 1024
mvs_lightbox_image_source Lightbox Image Size string original

mvs_large_image_size is the long-edge pixel width of the generated large variant. mvs_lightbox_image_source chooses which file the lightbox opens: original for full quality, or the large variant to save bandwidth on image-heavy pages.

Social and messaging

Option key Setting label Type Default
mvs_dm_access Who Can Send DMs string everyone
mvs_dm_min_age Minimum Account Age (days) integer 0
mvs_chat_panel_visibility Chat Panel Visibility string everywhere
mvs_show_online_status Online Status Visibility string everyone
mvs_comment_edit_window (no settings field - filterable) integer 900 (15 minutes)

mvs_dm_access accepts everyone, followers, mutual or nobody. This is the only place the word "followers" appears as a setting value - it controls who may open a conversation with a member, and has nothing to do with media privacy.

mvs_comment_edit_window is the number of seconds after posting during which a member may still edit their own comment.

Moderation and reporting

Option key Setting label Type Default
mvs_enable_reports Member Reporting boolean true
mvs_report_auto_hide_threshold Auto-Hide Threshold integer 3
mvs_abuse_contact_email Abuse Contact Email string (empty)
mvs_moderation_auto_action When AI Flags Content string (empty)

mvs_report_auto_hide_threshold is the number of distinct reports after which a media item is hidden automatically pending review. Set mvs_abuse_contact_email if you want abuse notifications sent somewhere other than the site admin address.

AI

Option key Setting label Type Default
mvs_ai_provider AI Provider string openai
mvs_openai_api_key OpenAI API Key string (empty)
mvs_openai_model OpenAI Model string gpt-4o-mini
mvs_ai_auto_analyze Auto-Analyze Uploads boolean false
mvs_ai_auto_tag Generate Tags boolean true
mvs_ai_auto_apply_tags Auto-Apply Tags boolean false
mvs_ai_auto_describe Generate Descriptions boolean true
mvs_ai_auto_moderate Auto-Moderate Uploads boolean false
mvs_ai_moderation_categories AI Flag Criteria array nudity, violence, hate, self-harm, drugs, spam
mvs_ai_moderation_custom_terms Custom Flag Terms string (empty)
mvs_ai_monthly_budget Monthly AI Budget ($) string (empty)
mvs_ai_cost_per_call (no settings field - filterable) number 0.01

Note the pairing: mvs_ai_auto_tag decides whether tags are generated, and mvs_ai_auto_apply_tags decides whether they are attached to the media item automatically rather than suggested for review. Generating without applying is the safe starting configuration.

mvs_ai_moderation_categories falls back to the full category list when stored empty, so an empty value never silently disables flagging. mvs_ai_monthly_budget combined with mvs_ai_cost_per_call drives the spend estimate and the budget cutoff.

Webhooks

Option key Setting label Type Default
mvs_webhooks Webhook Configuration array (empty array)

See Webhooks for the payload format and the event list.

Pro options

Registered by Admin\ProSettings and Admin\GamificationSettings. All Pro options are inert unless WPMediaVerse Pro is active.

Feature toggles

Option key Setting label Type Default
mvs_stories_enabled Stories string 0
mvs_connectors_enabled Enable Connectors string 0
mvs_streaks_enabled Enable Streaks string 0
mvs_watermark_enabled Enable Watermark boolean false
mvs_pro_transcode_enabled Enable Transcoding boolean false
mvs_autopilot_enabled Enable Autopilot string 0

Toggles stored as strings compare against '1'. When a feature is off its service is never constructed, so its REST routes are not registered and its scheduled actions do not run - which is why a disabled feature's endpoints return 404 rather than 403.

The gamification toggles mvs_battles_enabled, mvs_challenges_enabled, mvs_tournaments_enabled and mvs_boosts_enabled follow the same pattern and are covered in Gamification.

Video

Option key Setting label Type Default
mvs_pro_transcode_auto Auto-Transcode on Upload boolean true
mvs_pro_transcode_presets Quality Presets array 720p, 480p
mvs_pro_transcode_hls Generate HLS Stream boolean false
mvs_pro_transcode_thumbnail Generate Video Thumbnail boolean true
mvs_pro_ffmpeg_path FFmpeg Binary Path string (empty)

mvs_pro_transcode_auto only takes effect while mvs_pro_transcode_enabled is on. Leave mvs_pro_ffmpeg_path empty to use the binary on the system PATH.

AI providers

Option key Setting label Type Default
mvs_pro_google_vision_key Google Cloud API Key string (empty)
mvs_pro_anthropic_key Anthropic API Key string (empty)
mvs_pro_anthropic_model Claude Model string claude-haiku-4-5-20251001

Gamification

Option key Setting label Type Default
mvs_pro_battle_win_xp Battle Win Reward (XP) integer 100
mvs_pro_boost_cost_per_100 Points per 100 Impressions integer 50
mvs_pro_boost_max_impressions Max Impressions per Boost integer 5000
mvs_pro_boost_expiry_days Boost Expiry (Days) integer 7
mvs_streak_freezes_enabled Allow Streak Freezes string 0
mvs_pro_streak_freeze_cost Freeze Cost (Points) integer 100

Challenge autopilot

Option key Setting label Type Default
mvs_autopilot_day Day of Week string monday
mvs_autopilot_hour Time integer 9
mvs_autopilot_entry_days Entry Period (Days) integer 7
mvs_autopilot_voting_days Voting Period (Days) integer 3
mvs_autopilot_max_entries Max Entries per User integer 1

Autopilot creates the next themed challenge from the Theme Library on the configured day and hour. mvs_autopilot_entry_days and mvs_autopilot_voting_days set the length of each phase, so a challenge's full cycle is the sum of the two.

Connectors and layout

Option key Setting label Type Default
mvs_pro_connector_flickr_app_key Flickr Plugin API Key string (empty)
mvs_pro_connector_flickr_app_secret Flickr Plugin API Secret string (empty)
mvs_pro_feed_layout Explore Page Layout string grid

Other

Option key Setting label Type Default
mvs_pro_webhook_secret Webhook Secret string (empty)
mvs_pro_settings (container option) array (empty array)

mvs_pro_settings is a single array option holding assorted Pro values that have no dedicated key. Read it with get_option( 'mvs_pro_settings', array() ) rather than assuming a shape.

Storing credentials in wp-config.php

API keys and cloud credentials can be defined as constants instead of being stored in the database. When a constant is defined, the matching settings field renders locked and shows "Defined in wp-config.php", and the stored option is ignored.

This keeps secrets out of database dumps and out of staging copies. See Cloud Storage for the Cloudflare R2, S3 and DigitalOcean Spaces constant names.

Features

Uploads, albums, social features, AI moderation, and more.

Media Upload

Free + Pro - Core functionality is included free. Features marked with (Pro) require WPMediaVerse Pro.

Share photos, videos, and audio with your community - no admin access needed, straight from any page on your site.

What You Can Do

  • Drag and drop files directly onto the upload zone
  • Paste an image from your clipboard to upload instantly
  • Select multiple files at once for bulk upload
  • Set a title, description, and tags for each file before submitting
  • Choose who can see each file: public, members only, friends, or private
  • Upload JPEG, PNG, GIF, WebP images; MP4 and WebM videos; MP3 and OGG audio

How It Works (for Users)

  1. Go to the upload page your site administrator set up (often /upload/ or /media/upload/)
  2. Drag your files onto the upload zone, or click Browse to pick files from your device
  3. To paste a screenshot or copied image, click inside the upload zone and press Ctrl+V (Cmd+V on Mac)
  4. For each file, enter a title and optional tags. Select a privacy level from the dropdown
  5. Click Upload - a progress bar shows each file uploading
  6. When finished, your files appear in your media dashboard and your profile page
  7. Find all your uploads anytime under My Media or your profile's Media tab

Frontend upload form with drag-and-drop zone and privacy dropdown

For Site Owners

  1. Go to Media > Settings > General and confirm the allowed file types match what your community needs
  2. Add the upload form to any page: in the block editor, insert the WPMediaVerse: Media Upload block; in the classic editor, add [mvs_upload]
  3. Set the default privacy level and maximum file size for your site
  4. Enable Strip EXIF Data (on by default) to automatically remove GPS coordinates from photos before storage
  5. Users see the upload form immediately on that page when logged in

Supported File Formats

Type Formats
Images JPEG, PNG, GIF, WebP
Video MP4, WebM
Audio MP3 (MPEG), OGG

Customize allowed types in Media > Settings > General.

Upload Modal (1.9.0, 2.0.0)

The upload modal no longer asks you to pick a media type tab first - drop in an image, video, or audio file and WPMediaVerse auto-detects the type (1.9.0).

When uploading into an album, you can create a brand-new album right from the upload modal instead of leaving to create one first, then switching back to upload into it (2.0.0).

Bulk Album Upload (1.2.0)

When uploading multiple files at once into an album, WPMediaVerse now creates one activity entry for the whole batch ("Varun uploaded 3 photos to album Portrait Series") with a thumbnail grid, instead of one separate activity per file. Single-file uploads and ad-hoc photo posts retain their existing per-post behaviour.

Per-Media Edit Modal (1.2.0)

The settings cog on dashboard cards now opens a prefilled edit modal - change title, description, privacy level, and per-media Allow Downloads in place. Save updates the row live without a page reload. The matching PUT /mvs/v1/media/{id} REST endpoint accepts an allow_download boolean.

When you change the title, the URL slug stays stable by default. Tick the new Update URL slug checkbox if you want the slug regenerated from the new title - the page redirects to the new URL automatically so existing tabs and bookmarks don't 404.

What Happens After Upload

  • Thumbnails are generated automatically for images and videos (videos: ffmpeg poster extraction; run wp mvs generate-video-thumbnails to backfill any uploaded before 1.2.0)
  • EXIF GPS data is stripped from JPEG files (other metadata like camera model is kept)
  • A SHA-256 hash is computed to detect duplicate files
  • If AI moderation is enabled, the file is queued for automatic review
  • BuddyPress activity is recorded if BuddyPress is active on your site

Adding the Upload Form

Gutenberg Block: Add the WPMediaVerse: Media Upload block to any page or post.

Shortcode:

[mvs_upload]
[mvs_upload max_files="5" show_privacy="true"]
Attribute Default Description
max_files 10 Maximum number of files the user can upload at once
show_privacy true Whether to show the privacy level selector

Upload Validation

Every upload goes through these checks in order:

  1. MIME type check - file content is inspected (not just the extension) against your allowed types list.
  2. File size check - measured server-side against mvs_max_upload_size (default 100 MB).
  3. Extension block list - blocks PHP, shell, and other executable extensions even if the MIME passes.
  4. Double extension block - rejects filenames like photo.php.jpg.
  5. Duplicate detection - computes a SHA-256 hash and compares against existing uploads. Behavior depends on your Duplicate Detection setting (warn, skip, or allow).

EXIF Stripping

When Strip EXIF Data is enabled (default), GPS coordinates and device metadata are removed from JPEG images before storage. Non-GPS EXIF data (camera model, focal length) is retained.

Storage Path

Files are stored at:

wp-content/uploads/wpmediaverse/YYYY/MM/filename.ext

WordPress's wp_unique_filename() prevents filename collisions.

Programmatic Upload

Use the UploadService to upload files from code:

$upload_service = WPMediaVerse\Core\Plugin::container()->get( 'upload' );

$result = $upload_service->handle(
    $_FILES['my_file'],
    get_current_user_id(),
    array(
        'title'       => 'My File',
        'privacy'     => 'public',
        'description' => 'Optional description',
    )
);

if ( is_wp_error( $result ) ) {
    // Handle error.
} else {
    $media_id = $result; // int - new mvs_media post ID.
}

Actions Fired During Upload

  • mvs_before_media_insert - fires before the mvs_media post is created.
  • mvs_before_upload_form - fires before the upload form HTML is rendered (used by Pro for quota display).
  • mvs_media_uploaded - fires after the post is created and indexed. Passes $media_id (int), an array of file metadata (file_url, file_path, file_size, file_type, file_hash, media_type, privacy, etc.), $user_id (int), and $media_type (string).

Media Metadata

Media metadata is stored in WPMediaVerse's own mvs_media_meta table (keyed by media ID), not in WordPress post meta. Access it through the MediaRepository (->get( $media_id, $key )). Common keys:

Meta Key Type Description
file_url string Public URL of the stored file
file_path string Relative storage path
media_type string image, video, audio, or document
file_type string Full MIME type (e.g., image/jpeg)
file_size int File size in bytes
privacy string Privacy level (see Privacy & Access Control)
file_hash string SHA-256 hash for duplicate detection
group_id int BuddyPress group ID (for group privacy)

Albums

Included in Free - This feature is available in the free version of WPMediaVerse.

Group your photos into beautiful collections - tell a story, document a trip, or organize your portfolio with a single shareable album.

What You Can Do

  • Create albums to organize related photos and videos together
  • Add photos from your existing media library to any album
  • Set a cover photo that represents the album
  • Control album privacy independently from individual photo privacy
  • Share albums with friends or keep them private
  • Embed any album on a page using a block or shortcode

How It Works (for Users)

  1. Go to your media dashboard and click Create Album
  2. Give your album a title and optional description
  3. Choose a privacy level: public, members only, friends, or private
  4. Click Add Media to pick photos from your uploads - select as many as you like
  5. Drag photos in the album to reorder them. Click the star on any photo to set it as the cover
  6. Click Save Album - your album is live and appears on your profile
  7. To share your album, copy the album link from the album page and send it to anyone

Album creation form with title field and privacy selector

For Site Owners

  1. Albums are enabled by default once WPMediaVerse is activated
  2. To embed a specific album on a page, use the WPMediaVerse: Album Viewer block in the block editor - select the album from the block sidebar
  3. Or use the shortcode [mvs_album id="123"] where 123 is the album's post ID
  4. Users manage their own albums from their media dashboard
  5. Admins can view and delete any album from Media > Albums in wp-admin

Album Privacy

Albums have their own privacy level, and media added to a non-public album inherits it.

When you add a media item to an album, its privacy is clamped to the more restrictive of the two. Adding a public photo to a Private album makes that photo private, so it stops appearing in Explore and in other members' feeds. The clamp only ever tightens: an item that is already more restrictive than its album keeps its own setting, and a public album never changes anything.

Two things this does not do:

  • Changing an album's privacy later does not re-apply to items already inside it. Only the moment of adding clamps.
  • Removing an item from an album does not restore its previous privacy.

Site owners who want album and item privacy to stay fully independent (the behaviour before 2.3.0) can return false from the mvs_album_inherit_privacy filter.

If a user can see the album but not a specific media item (because the item's privacy is more restrictive still), that item is hidden from the album view.

Displaying an Album

Gutenberg Block: Add the WPMediaVerse: Album Viewer block, then select an album from the block settings.

Shortcode:

[mvs_album id="123"]
[mvs_album id="123" columns="4" show_title="true" show_description="true"]
Attribute Default Description
id (required) Album post ID
columns 3 Grid columns to display
show_title true Show the album title above the grid
show_description true Show the album description

REST API Endpoints for Albums

Method Endpoint Description
GET /mvs/v1/albums List albums
POST /mvs/v1/albums Create album
GET /mvs/v1/albums/{id} Get album
PUT /mvs/v1/albums/{id} Update album
DELETE /mvs/v1/albums/{id} Delete album
GET /mvs/v1/albums/{id}/items List album media
POST /mvs/v1/albums/{id}/items Add media to album
DELETE /mvs/v1/albums/{id}/items/{media_id} Remove media from album

Creating an Album via API

curl -X POST https://yoursite.com/wp-json/mvs/v1/albums \
  -H "X-WP-Nonce: YOUR_NONCE" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Summer 2025",
    "description": "Photos from our trip",
    "privacy": "public"
  }'

Adding Media to Albums via API

curl -X POST https://yoursite.com/wp-json/mvs/v1/albums/ALBUM_ID/items \
  -H "X-WP-Nonce: YOUR_NONCE" \
  -H "Content-Type: application/json" \
  -d '{"media_ids": [101, 102, 103]}'

This fires the mvs_album_items_added action, which triggers BuddyPress activity updates if BuddyPress is active.

BuddyPress Activity

When media is added to an album and BuddyPress is active, the mvs_album_items_added action updates the upload activity item to reference the album. This replaces the generic "uploaded media" activity with "uploaded media to [Album Name]".

Collections

Included in Free - This feature is available in the free version of WPMediaVerse.

Save and curate media from anyone on your site into personal boards - like Pinterest boards, but for your community's photos and videos.

What You Can Do

  • Save any public media item to a personal collection with one click
  • Create multiple collections for different themes or moods (e.g., "Travel Inspiration", "Black and White")
  • Curate manually by hand-picking individual photos, or let smart rules auto-fill a collection
  • Smart collections stay fresh automatically - tag a rule once and the collection updates itself
  • Share collections publicly or keep them private
  • Browse your saved collections from your media dashboard

How It Works (for Users)

  1. When you find a photo you love, click the Save button (bookmark icon) below it
  2. Choose an existing collection from the dropdown, or click New Collection to create one
  3. Give your new collection a name and choose a privacy level, then click Create
  4. The photo is added instantly - you'll see the bookmark icon turn solid to confirm
  5. Find all your collections under My Media > Collections in your dashboard
  6. To manage a collection, open it and click Edit to rename it, reorder items, or remove ones you no longer want
  7. To share a collection, copy the link from the collection page

Media item with bookmark/save button and collection picker

For Site Owners

  1. Collections are available to all users with upload access once WPMediaVerse is activated
  2. To embed a collection on any page, use [mvs_collection id="456"] or the WPMediaVerse: Collection Viewer block
  3. Smart collections are especially useful for curated showcase pages - create a smart collection filtered by a tag and embed it on your homepage
  4. Manage all collections from Media > Collections in wp-admin
  5. Use the Collection Settings meta box on any collection post to switch between manual and smart mode and configure smart rules

Collection Types

Type Description
Manual You add specific media items by hand. Items are stored in the wp_mvs_favorites table with the collection ID.
Smart Rules-based. The collection resolves its item list dynamically at request time based on criteria like tag, category, media type, or date range.

Displaying a Collection

Shortcode:

[mvs_collection id="456"]
[mvs_collection id="456" columns="3" per_page="20"]
Attribute Default Description
id (required) Collection post ID
columns 3 Grid columns to display
per_page 20 Maximum number of media items to show

If no id is provided, the shortcode outputs an error message. If the collection post is not published, the shortcode shows "Collection not found."

REST API Endpoints for Collections

Method Endpoint Description
GET /mvs/v1/collections List collections
POST /mvs/v1/collections Create collection
GET /mvs/v1/collections/{id} Get collection with resolved items
PUT /mvs/v1/collections/{id} Update collection
DELETE /mvs/v1/collections/{id} Delete collection

Creating a Smart Collection via API

curl -X POST https://yoursite.com/wp-json/mvs/v1/collections \
  -H "X-WP-Nonce: YOUR_NONCE" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Nature Photos",
    "type": "smart",
    "rules": {
      "tags": ["nature", "landscape"],
      "media_type": "image"
    },
    "privacy": "public"
  }'

Collection Meta Box

In the WordPress admin, each mvs_collection post has a Collection Settings meta box that lets you set the collection type and define smart rules without using the API.

Collection meta box in WordPress admin

Stories

Pro feature. Stories (time-limited, ephemeral posts) ship in WPMediaVerse Pro. The free plugin provides the upload-flow entry point described below, but story creation, the viewer, and view receipts all require Pro.

What's in Free

The Media Upload block includes an "Also share as a story" checkbox next to the tag input, but only when the mvs_stories_enabled option is on:

$mvs_stories_on = ( '1' === get_option( 'mvs_stories_enabled', '0' ) );

mvs_stories_enabled defaults to off and is set by Pro when it registers the Stories feature - so on a free-only install this toggle stays hidden and no story code runs. Free no longer ships a StoryService; the class was relocated to Pro in 1.9.0 (it was built ahead of the create-flow and viewer UI, then moved wholesale once those shipped).

What Pro Adds

See Stories (Pro) for the full feature: 24-hour default expiry (1-168h configurable per story), a tap-to-advance viewer, "seen by" receipts, and the mvs-pro/v1 REST routes that drive it (GET /stories, POST /media/{id}/story, DELETE /media/{id}/story, POST /stories/{id}/view, GET /stories/{id}/viewers).

Hooks

mvs_story_created and mvs_story_expired are Pro hooks (fired from WPMediaVersePro\Stories\StoryService). See Hooks & Filters - Access & Privacy.

Social Features

Included in Free - This feature is available in the free version of WPMediaVerse.

React, comment, follow, and share - WPMediaVerse turns your media site into a living community where every photo starts a conversation.

What You Can Do

  • React to any photo or video with emoji reactions (like, love, wow, and more)
  • Comment on media and edit your comment within 15 minutes of posting
  • Follow photographers you love - their new uploads appear in your feed
  • Save media to your favorites for quick access later
  • @mention other members in comments to notify them directly
  • Share any public media item to social networks or copy a direct link
  • Report inappropriate content directly from the media page

How It Works (for Users)

Following Someone

  1. Visit any member's profile page
  2. Click Follow - their public uploads now appear in your feed
  3. To stop following, click Unfollow on their profile
  4. See everyone you follow under My Media > Following

Reacting to Media

  1. Open any media item
  2. Click the reaction bar below the photo
  3. Choose your reaction - like, love, wow, haha, sad, or angry
  4. Your reaction is shown instantly. Click the same reaction to remove it
  5. Click a different reaction to change yours

Media item with reaction bar showing emoji counts

Commenting

  1. Scroll to the comments section below any media item
  2. Type your comment in the text box and press Enter or click Post
  3. To @mention someone, type @ followed by their username - they receive a notification
  4. To edit your comment, click the pencil icon next to it (available within 15 minutes of posting)
  5. To delete your comment, click the trash icon

Saving to Favorites

  1. Click the heart icon on any media item
  2. The item is added to your favorites list
  3. Find all your favorites under My Media > Favorites in your dashboard
  4. To save to a named collection instead, click the bookmark icon and choose or create a collection

Sharing Media

  1. Open any public media item
  2. Click Share to see options: copy the direct link, or share to social networks
  3. The share link respects the media's privacy - private media returns an error for unauthorized viewers

For Site Owners

  1. All social features are enabled by default
  2. Configure reaction types and moderation in Media > Settings > Social
  3. Set the comment edit window (default: 15 minutes) in Media > Settings > Social
  4. Set the auto-hide threshold: when a media item receives a certain number of reports, it is automatically hidden and sent to the moderation queue
  5. BuddyPress notifications fire automatically for reactions, comments, and mentions when BuddyPress is active

Explore Feed Scope Filters (2.0.0)

The media listing REST endpoint accepts a scope parameter that narrows results to a viewer's own network, available across every layout (default grid and every Pro feed layout):

Value Shows
public Public media only
followers Media from people the current viewer follows
self Only the current viewer's own uploads

followers and self require a logged-in viewer and fall back to the public-only privacy gate for anonymous requests.

Reactions

WPMediaVerse uses a custom reactions system stored in a dedicated database table, separate from WordPress post meta.

REST API

Method Endpoint Description
GET /mvs/v1/media/{id}/reactions Get reaction counts for a media item
POST /mvs/v1/media/{id}/reactions Add or change your reaction
DELETE /mvs/v1/media/{id}/reactions Remove your reaction

Action: mvs_reaction_added

Fires when a reaction is added. BuddyPress integration uses this to send notifications.

do_action( 'mvs_reaction_added', $media_id, $user_id, $reaction_type );

Comments

WPMediaVerse uses a custom comments system stored in a dedicated table, separate from WordPress's wp_comments.

Comment section below a media item

REST API

Method Endpoint Description
GET /mvs/v1/media/{id}/comments List comments on a media item
POST /mvs/v1/media/{id}/comments Post a new comment
PUT /mvs/v1/media/{id}/comments/{comment_id} Edit a comment
DELETE /mvs/v1/media/{id}/comments/{comment_id} Delete a comment

Action: mvs_comment_created

do_action( 'mvs_comment_created', $media_id, $user_id, $comment_id, $content, $source );

BuddyPress integration uses this to record activity and send notifications.

Favorites

Users can save media items to their favorites or to a named collection.

REST API

Method Endpoint Description
POST /mvs/v1/media/{id}/favorite Add to favorites
DELETE /mvs/v1/media/{id}/favorite Remove from favorites
GET /mvs/v1/me/favorites List the current user's favorites

Follows

Users can follow other users to see their public media in a feed.

REST API

Method Endpoint Description
POST /mvs/v1/users/{id}/follow Follow a user
DELETE /mvs/v1/users/{id}/follow Unfollow a user
GET /mvs/v1/users/{id}/followers List a user's followers
GET /mvs/v1/users/{id}/following List who a user follows

Mentions

Users can @mention each other in comments. Mentioned users receive a notification.

Action: mvs_mentions_created

do_action( 'mvs_mentions_created', $media_id, $mentioned_user_ids, $context, $comment_id );

Sharing

The ShareService provides share URL generation for media items. Shareable links respect the media's privacy level - private media returns a 403 when accessed via a share link by an unauthorized user.

Reports

Users can report inappropriate media content.

REST API

Method Endpoint Description
POST /mvs/v1/media/{id}/report Submit a content report

When the number of reports for a single media item reaches the Auto-Hide Threshold (set in AI & Moderation settings), the media is automatically hidden and added to the moderation queue.

Interests & Personalized Onboarding

Included in Free - This feature is available in the free version of WPMediaVerse.

New members pick a handful of interests, get a ranked list of creators to follow, and see a feed that leans toward what they picked - instead of landing on an empty explore page with nobody to follow and nothing personalized to see.

What You Can Do

  • Choose interests from the site's existing media categories (mvs_category taxonomy) during first-session activation
  • See a ranked "people you may know" list, boosted toward creators who post in your chosen interests
  • Request an interest-aware media feed that favors your picks over a plain chronological/popular feed
  • Skip the picker entirely and mark onboarding done without choosing anything

How It Works

  1. On first login, a client shows the available interest chips - up to 40 of the site's most-used categories, each with a cover photo and post count
  2. The member taps the interests they care about and saves
  3. The site immediately has a ranked follow-suggestion list ready, weighted toward creators active in those interests
  4. Requesting the feed with the interest-aware flag on returns media favoring the member's saved interests first

Today this ships as a REST API surface (built for the official MediaVerse app and for headless/custom frontends) rather than a bundled on-page picker screen in the default web templates - any client that speaks REST can build the picker UI on top of it.

For Site Owners

Nothing to configure - interests reuse the categories you already use for tagging media (mvs_category), so there's no separate taxonomy to maintain. The candidate pool for follow suggestions and the public interest list are both cached (default 1 hour) so this stays cheap on busy sites.

REST API

Base URL: /wp-json/mvs/v1/

GET /app/interests

Public (no authentication). Returns the available interest chips, ranked by post count.

Response:

[
  { "id": 12, "name": "Landscape", "slug": "landscape", "count": 340, "cover_url": "https://yoursite.com/..." }
]

Up to 40 categories. Cache TTL is filterable via mvs_app_interests_cache_ttl (default 1 hour, seconds; 0 disables caching).


GET /me/interests

Requires a logged-in user. Returns the current user's saved interest IDs.

Response:

{ "interest_ids": [12, 45] }

POST /me/interests

Requires a logged-in user. Save the member's interest picks.

Body:

{ "interest_ids": [12, 45] }

Only real mvs_category term IDs are kept; anything else is silently dropped. Saving also marks the member as onboarded (see below).


POST /me/onboarding/complete

Requires a logged-in user. Flags the viewer as onboarded without requiring them to pick any interests - the "skip" path.

Response:

{ "onboarded": true }

The onboarded flag is also surfaced on the member's own profile response so a client only shows the activation flow once.


GET /users/suggested

Requires a logged-in user (anonymous requests fall back to pure popularity ranking). Returns "people you may know" - creators ranked by follower count, boosted toward creators posting in the viewer's saved interests, excluding the viewer, anyone already followed, and anyone blocked.

Parameters:

Parameter Type Default Description
limit int 20 Maximum suggestions (1-50)

Candidate pool cache TTL is filterable via mvs_suggestions_cache_ttl (default 1 hour, seconds; 0 disables caching).


Interest-aware media feed

Pass interests=auto on the media listing endpoint (GET /mvs/v1/media) to narrow results toward the logged-in viewer's saved interest categories. Requires a logged-in user with at least one saved interest; anonymous viewers and members with no saved interests are unaffected (the feed never comes back empty because of this parameter).

Direct Messages

Included in Free - This feature is available in the free version of WPMediaVerse.

Send private messages, share photos, record voice notes, and have real conversations - all without leaving your media community.

What You Can Do

  • Send a private message to any member directly from their profile
  • Attach photos, videos, or audio, or share a media item from the gallery straight into chat
  • Record and send short voice messages
  • React to individual messages with any emoji
  • See typing indicators and read receipts in real time
  • Mute noisy conversations, pin important ones, or archive old chats
  • Search across all your messages to find anything instantly
  • Control who can message you: everyone, followers only, mutual followers only, or nobody

How It Works (for Users)

Starting a Conversation

  1. Visit any member's profile page
  2. Click Message - the chat panel opens in the bottom-right corner with that conversation ready
  3. Type your message and press Enter to send

To start a conversation without visiting a profile:

  1. Click the chat icon at the bottom of any page
  2. Click the compose icon inside the chat panel
  3. Search for the member by name or username and select them

Sending a Voice Message

  1. Open a conversation
  2. Click the microphone icon in the message bar
  3. Hold to record your message, then release to send
  4. The recipient sees a playable audio clip in the conversation

Sharing a Photo in Chat

  1. Open a conversation
  2. Click the media icon (photo frame) in the message bar
  3. Browse your uploaded media and click any item to share it directly into the chat

Managing Conversations

Action How to do it
Mute Open the conversation menu and click Mute - notifications are suppressed
Pin Click Pin to keep the conversation at the top of your list
Archive Click Archive to hide it from the main list. Find archived chats under the Archive tab
Search Click the search icon in the panel header and type any word or phrase

Message Requests

If a member has restricted who can message them to followers only, your message goes to their Requests tab rather than their main inbox. They can accept or decline the request.

  • Accept - Your conversation moves to their main inbox and messaging continues normally
  • Decline - The request is removed. You are not notified of the decline

Chat panel with message requests

For Site Owners

  1. Go to Media > Settings > Social to configure site-wide DM defaults
  2. Set who can send DMs by default: everyone, followers, mutual followers, or nobody
  3. Set a minimum account age (in days) to prevent new accounts from sending DMs
  4. Users can override their own DM access setting from their account settings
  5. The chat panel appears automatically at the bottom of every page for logged-in users - no shortcode needed
  6. To adjust how often the chat checks for new messages, set the polling interval (default: 3 seconds)

Database Tables

Table Purpose
mvs_conversations One row per conversation, stores metadata (muted, pinned, archived state per participant)
mvs_conversation_participants Maps users to conversations (supports future group chat expansion)
mvs_messages Individual messages with content, type, read status, and soft-delete flag
mvs_message_reactions Emoji reactions attached to individual messages

Opening a Conversation

There are three ways to start or open a conversation:

  • From a profile page - Click the Message button on any user's profile. The chat panel opens with that conversation pre-loaded. No searching required.
  • From the chat panel - Click the compose icon inside the chat panel and search by username or display name.
  • Deep links - Link directly to a conversation or user using a URL fragment:
Fragment Opens
#mvs-chat/{conversationId} A specific conversation by ID
#mvs-chat/user/{userId} The conversation with a specific user (creates one if none exists)

Message button on a user profile card

Chat Panel Features

Feature Description
Text messages Plain text with @mention support
Media attachments Attach images, video, or audio. MediaVerse is a media platform, so documents (PDF, DOC, ZIP) are not accepted in chat as of 2.2.0 - a developer can extend the list with the mvs_dm_allowed_file_types filter. While a file uploads, the composer shows an "Uploading..." chip and Send waits for the upload to finish
Media sharing Share a WPMediaVerse media item directly into a conversation
Voice messages Record and send short audio clips
Emoji reactions React to individual messages with any emoji
Typing indicators Shows a live indicator when the other user is typing
Read receipts Delivered and read timestamps shown per message
Message deletion Delete your own messages (content replaced with "This message was deleted")

Chat panel showing a conversation with media share and reactions

Conversation Management

Action How
Mute Suppress notifications for a conversation without leaving it
Pin Keep a conversation at the top of the conversation list
Archive Hide a conversation from the main list; accessible via the Archive tab
Search Full-text search across all your messages via the search icon in the panel header

Privacy Settings

Each user controls their DM privacy from their account settings. Admins set the site-wide defaults at Media > Settings > Social.

Option Key Values
Who can message me mvs_dm_access everyone, followers, mutual, nobody
Minimum account age mvs_dm_min_age Integer (days). Prevents newly registered accounts from sending DMs.
Show online status mvs_show_online_status everyone, followers, or nobody

When mvs_dm_access is set to nobody, the Message button is hidden on that user's profile.

Content Moderation (1.9.0)

Every outgoing DM passes through the mvs_message_content_check filter before it is stored, the same seam other content types on the site already use to reject disallowed words. On its own, WPMediaVerse Free doesn't ship a word-blocklist for messages - the filter is what lets a host (a full community stack like BuddyNext, or your own mu-plugin) block a message the moment it's sent, so a member can't route banned words through a DM that would otherwise be caught in a comment or activity post. A blocked send returns an error instead of saving the message.

add_filter( 'mvs_message_content_check', function( $result, $content, $sender_id, $conversation_id ) {
    if ( str_contains( strtolower( $content ), 'bannedword' ) ) {
        return new WP_Error( 'content_blocked', 'That word is not allowed.' );
    }
    return $result; // true lets the message through.
}, 10, 4 );

Transport

The chat panel polls the REST API to fetch new messages. There are three intervals (in milliseconds), chosen by context: an open conversation polls fastest, the conversation list slower, and a closed panel slowest. The defaults are active 3000, list 10000, and background 30000.

Filter the intervals with mvs_messaging_poll_intervals:

add_filter( 'mvs_messaging_poll_intervals', function( $intervals ) {
    $intervals['active'] = 2000; // Poll the open conversation every 2 seconds.
    return $intervals;
} );

REST API

Base URL: /wp-json/mvs/v1/

All endpoints require a logged-in user. Pass the X-WP-Nonce header with a nonce from wp_create_nonce( 'wp_rest' ).

POST /conversations

Start a new conversation.

Body:

{ "recipient_id": 42 }

Response: 201 Created with the new conversation object, or 200 OK with the existing conversation if one already exists between the two users.


GET /me/conversations

List all conversations for the current user. Excludes archived conversations unless ?include_archived=1 is passed.

Parameters:

Parameter Type Default Description
include_archived int 0 Set to 1 to include archived conversations
per_page int 20 Conversations per page
page int 1 Page number

GET /conversations/{id}/messages

List messages in a conversation. The current user must be a participant.

Parameters:

Parameter Type Default Description
per_page int 30 Messages per page
before int 0 Return messages with ID lower than this value (for pagination). 0 means no cursor.

POST /conversations/{id}/messages

Send a message. The current user must be a participant and must satisfy the recipient's mvs_dm_access setting.

Body (JSON or multipart/form-data for file attachments):

Field Required Description
content No Text content. Defaults to empty string.
message_type No Message type: text (default), media_share, image, video, audio, voice, file, system. Unknown values fall back to text.
media_id No WPMediaVerse media ID when message_type=media_share
attachment_id No WordPress attachment ID from a prior POST /messages/upload, used for image/video/audio/voice/file messages
parent_id No Message ID to reply to. Creates a threaded reply visible under the parent message.
metadata No Object of extra data (e.g. { "duration": 12 } for voice messages).

Response: 201 Created with the new message object.


GET /conversations/{id}/messages/search

Search message content within a single conversation (1.9.0). The current user must be a participant.

Parameters:

Parameter Type Default Description
q string (required) Search term matched against message content
per_page int 50 Matching messages per page

Response: Array of matching message objects, same shape as GET /conversations/{id}/messages.


POST /messages/upload

Upload a file to use as a DM attachment. Returns the WordPress attachment ID to pass as attachment_id in a subsequent POST /conversations/{id}/messages call.

Body: multipart/form-data with a file field.

Allowed types (2.2.0): media only - image/jpeg, image/png, image/gif, image/webp, audio/webm, audio/mp4, audio/mpeg, audio/ogg, video/mp4, video/webm. The real MIME is sniffed server-side (finfo), so renaming a file does not bypass the check; anything else returns 400 File type not allowed. Extend the list with the mvs_dm_allowed_file_types filter. (The file message type remains for messages sent before 2.2.0.)

Response: 200 OK

{ "id": 123, "source_url": "https://yoursite.com/...", "thumbnail": "https://yoursite.com/..." }

PATCH /conversations/{id}

Update conversation state for the current user.

Body:

Field Type Description
is_muted bool Set to true to mute, false to unmute
is_pinned bool Set to true to pin, false to unpin
is_archived bool Set to true to archive, false to restore

Response: 200 OK with the updated conversation object.


DELETE /conversations/{id}

Delete a conversation for the current user. The conversation remains visible to the other participant.

Response: 204 No Content


DELETE /messages/{id}

Soft-delete a message. Requires ownership of the message. The message record is kept but its content is replaced and is_deleted is set to 1.


DELETE /messages/{id}/unsend

Hard-delete a message. Only available within the edit window (default: 5 minutes after sending). Requires ownership of the message. The message record is permanently removed.

Response: 204 No Content


GET /me/messages/unread-count

Return the total number of unread messages across all conversations for the current user. Used to update the chat panel badge.

Response:

{ "unread": 4 }

POST /messages/{id}/reactions

Add or change a reaction on a message.

{ "emoji": "❤️" }

To remove a reaction, call DELETE /messages/{id}/reactions.


Actions and Filters

mvs_message_sent

Fires after a message is successfully saved.

add_action( 'mvs_message_sent', function( $message_id, $conversation_id, $sender_id, $recipient_ids ) {
    // React to the new message.
}, 10, 4 );

mvs_message_types

Filter the list of allowed message types accepted by the send endpoint.

add_filter( 'mvs_message_types', function( $types ) {
    return $types; // Default: text, media_share, image, video, audio, voice, file, system.
} );

mvs_messaging_poll_intervals

Filter the client-side polling intervals (in milliseconds) delivered to the front end. See the Transport section above.

mvs_message_content_check

Filter fired before a message is stored. Return a WP_Error to block the send; return the passed-in true (or anything not a WP_Error) to let it through. See the Content Moderation section above.

Privacy & Access Control

Free + Pro - Core functionality is included free. Features marked with (Pro) require WPMediaVerse Pro.

WPMediaVerse provides 6 privacy levels for media items, albums, and collections. Access checks run on every REST API call and on the explore archive query.

Privacy Levels

Level Value Who Can View
Public public Everyone, including logged-out visitors
Members Only members Any logged-in WordPress user
Friends friends BuddyPress friends of the media owner (requires BuddyPress active)
Group group Members of a specific BuddyPress group (requires BuddyPress active)
Private private Only the media owner and users with moderate_mvs_media
Custom custom A specific list of user IDs defined via access grants

Owners and Moderators Always Have Access

Media owners (the post_author) and users with the moderate_mvs_media capability bypass all privacy checks. They can view all media regardless of its privacy level.

Setting Privacy on Upload

Set the privacy field when creating media via REST API:

curl -X POST https://yoursite.com/wp-json/mvs/v1/media \
  -H "X-WP-Nonce: NONCE" \
  -F "file=@photo.jpg" \
  -F "privacy=friends"

For group privacy, also include group_id:

  -F "privacy=group" \
  -F "group_id=42"

Changing Privacy After Upload

curl -X PUT https://yoursite.com/wp-json/mvs/v1/media/123 \
  -H "X-WP-Nonce: NONCE" \
  -H "Content-Type: application/json" \
  -d '{"privacy": "private"}'

Custom Access Grants

For custom privacy, grant access to specific users:

curl -X POST https://yoursite.com/wp-json/mvs/v1/media/123/grant \
  -H "X-WP-Nonce: NONCE" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": 55,
    "expires_at": "2026-01-01T00:00:00Z"
  }'

Access grants can have optional expiry dates. Expired grants are cleaned up via wp mvs cleanup-expired or via cron.

Signed URLs for Private Files

For media stored with a non-public privacy level, WPMediaVerse can generate time-limited signed URLs:

curl https://yoursite.com/wp-json/mvs/v1/media/123/signed-url \
  -H "X-WP-Nonce: NONCE"

Response:

{
  "url": "https://yoursite.com/wp-content/uploads/wpmediaverse/2025/03/photo.jpg?token=abc123&expires=1743000000",
  "expires_at": "2025-03-27T13:00:00Z"
}

The signed URL TTL defaults to 3600 seconds (1 hour) and is configurable in Media > Settings > Storage > Signed URL Expiry (seconds).

Filtering Privacy Access in Code

Use the mvs_privacy_can_view filter to extend or override access logic:

add_filter( 'mvs_privacy_can_view', function( $result, $media_id, $user_id, $privacy ) {
    // Grant access to premium subscribers regardless of privacy level.
    if ( null === $result && wcs_user_has_subscription( $user_id, '', 'active' ) ) {
        return true;
    }
    return $result;
}, 10, 4 );

Return null to let the built-in logic run. Return true or false to override it.

Explore Archive Privacy Filtering

On the explore archive (/media/), WPMediaVerse applies automatic privacy filtering through a SQL clause on its own mvs_media_index table (a privacy IN (...) OR post_author = current gate shared by every explore surface), not a WordPress posts_where filter:

  • Logged-out users see only public media.
  • Logged-in non-moderators see public, members media, and their own media (any privacy level).
  • Moderators (moderate_mvs_media capability) see all media.

AI Moderation

Free + Pro - Core functionality is included free. Features marked with (Pro) require WPMediaVerse Pro.

WPMediaVerse integrates with OpenAI Vision (GPT-4o or GPT-4o Mini) to automatically analyze and moderate uploaded media. WPMediaVerse Pro adds support for Google Vision, AWS Rekognition, and Claude (Anthropic).

AI moderation result on a media post

How AI Analysis Works

When Auto-Analyze Uploads is enabled, WPMediaVerse sends each uploaded image to OpenAI immediately after storage. The analysis returns:

  • A natural-language description of the image
  • Suggested tags (comma-separated keywords)
  • Safety categories with confidence scores (adult content, violence, etc.)

The description is saved to the ai_description media metadata key. If Auto-Apply Tags is enabled, the suggested tags are assigned to the mvs_tag taxonomy.

How AI Moderation Works

When Auto-Moderate Uploads is enabled, WPMediaVerse checks the AI safety scores against configurable thresholds. If a policy violation is detected, the action defined in When AI Flags Content is applied:

Action What Happens
Flag for review Media stays published but appears in the moderation queue
Hide Media's privacy is set to private
Reject Media's post_status is set to draft
Delete permanently Media and its files are removed from local and cloud storage. Cannot be undone.

Moderation status changes fire the mvs_moderation_changed action hook ($media_id, $status, $old_status, $user_id).

Triggering Analysis Manually

Site administrators can re-analyze any media item from the moderation queue or via the REST API:

curl -X POST https://yoursite.com/wp-json/mvs/v1/moderation/123/analyze \
  -H "X-WP-Nonce: NONCE"

Approving or Rejecting from the Moderation Queue

  1. Go to Media > Media Moderation (renamed from "Moderation" in 2.0.0).
  2. Review the flagged item and its AI analysis result.
  3. Click Approve to publish or Reject to move to draft.

Moderation queue item with approve and reject buttons

Budget Control

Set a monthly spending cap at Media > Settings > AI & Moderation > Monthly AI Budget ($). WPMediaVerse tracks estimated spending per call and stops making AI calls when the budget is reached.

To check current spending:

wp mvs stats

Registering a Custom AI Provider (Developer)

Implement the AIProviderInterface and register your provider:

use WPMediaVerse\Services\AIProviderInterface;

class MyCustomProvider implements AIProviderInterface {

    public function get_id(): string {
        return 'my_provider';
    }

    public function is_available(): bool {
        return (bool) get_option( 'my_provider_api_key' );
    }

    public function analyze( int $media_id ): array|WP_Error {
        // Return array with 'description', 'tags', 'flags'.
    }
}

add_action( 'mvs_ai_providers', function( $ai_service ) {
    $ai_service->register_provider( new MyCustomProvider() );
} );

Moderation & AI Metadata

These keys live in WPMediaVerse's mvs_media_meta table (accessed via the MediaRepository), not in WordPress post meta. moderation_status is also a column on the mvs_media_index table.

Meta Key Values Description
moderation_status approved, pending, flagged, rejected Current moderation status
ai_description string AI-generated description
ai_tags string[] AI-suggested tags
ai_confidence float Confidence score for the AI analysis

Shortcodes

Included in Free - This feature is available in the free version of WPMediaVerse.

WPMediaVerse provides 12 shortcodes for embedding media features in pages, posts, and classic editor content.

[mvs_gallery]

Displays a filterable media grid. Columns and items-per-page come from Media > Settings > Display and cannot be overridden by shortcode attributes.

[mvs_gallery]
[mvs_gallery type="image" category="nature" tag="summer" orderby="date"]
Attribute Default Description
type (all types) Filter by media type: image, video, or audio
category (all) Filter by mvs_category slug
tag (all) Filter by mvs_tag slug
user_id (all authors) Filter to a single author. Pair with orderby="popular" for a "Top media by this member" embed.
orderby date Sort order: date, title, views, popular, reactions, or random.
order desc Sort direction: asc or desc.

[mvs_upload]

Displays the frontend file upload form. The form is only functional for logged-in users.

[mvs_upload]
[mvs_upload max_files="5" show_privacy="true"]
Attribute Default Description
max_files 10 Maximum number of files selectable at once
show_privacy true Show privacy level selector on the form

This shortcode fires the mvs_before_upload_form action before rendering.

[mvs_album]

Displays a single album by ID.

[mvs_album id="123"]
[mvs_album id="123" columns="4" show_title="true" show_description="false"]
Attribute Default Description
id (required) Album post ID
columns 3 Grid columns
show_title true Show album title
show_description true Show album description

[mvs_player]

Embeds a single media item in an interactive player.

[mvs_player id="456"]
[mvs_player id="456" autoplay="false" loop="false" download="false"]
Attribute Default Description
id (required) Media post ID
autoplay false Start playback automatically (audio/video)
loop false Loop the media after it finishes
download false Show a download button

[mvs_stats]

Displays site-wide media statistics.

[mvs_stats]
[mvs_stats views="true" downloads="true" reactions="true" top="true" top_count="5"]
Attribute Default Description
views true Show total view count
downloads true Show total download count
reactions true Show total reaction count
top true Show the top media list
top_count 5 Number of items in the top media list

[mvs_dashboard]

Displays a personal media dashboard for the logged-in user. Shows the user's own uploads, albums, and stats. Redirects to login if the user is not logged in.

[mvs_dashboard]

No configurable attributes. The dashboard uses the dashboard-view Interactivity API block store.

[mvs_collection]

Displays a collection by ID. Works for both manual and smart collections.

[mvs_collection id="789"]
[mvs_collection id="789" columns="3" per_page="20"]
Attribute Default Description
id (required) Collection post ID
columns 3 Grid columns
per_page 20 Maximum items to show

[mvs_profile_edit]

Displays a profile edit form for the logged-in user, allowing them to update their first name, last name, display name, bio, and avatar. Redirects to login if not logged in.

[mvs_profile_edit]

No configurable attributes. The form is powered by the mvs/profile-edit Interactivity API store and saves to /mvs/v1/profile.

Profile edit form with avatar upload and name fields

[mvs_explore_feed]

Embeds the explore archive - infinite-scroll public media feed with filter chips and the new search autocomplete dropdown. Use this when you want the explore experience on a custom page rather than the dedicated archive route.

[mvs_explore_feed]
[mvs_explore_feed layout="grid" columns="3" per_page="12" filters="true" search="true"]
Attribute Default Description
layout grid Feed layout.
columns 3 Grid columns.
per_page 12 Items per page.
filters true Show the filter chips.
search true Show the search input with autocomplete.

[mvs_lock_overlay]

Renders a privacy lock overlay for a single media item. If the current user has access, the overlay falls through and renders the player or image inline. If they do not, the overlay shows the configured restriction message.

[mvs_lock_overlay id="456"]
[mvs_lock_overlay id="456" blur="20" overlay_opacity="60" unlock_label="Restricted Content"]
Attribute Default Description
id (required) Media post ID to evaluate access against.
blur 20 Blur amount applied to the locked preview.
overlay_opacity 60 Overlay opacity (0-100).
unlock_label (empty) Custom restriction label.

[mvs_member_photos]

Renders a member's media grid. Auto-resolves the user - explicit user_id attribute first, then the BuddyPress displayed user, then the post author, then the current user - so the same shortcode works on profile pages, member-specific landing pages, and author archives.

Added in 1.2.0.

[mvs_member_photos]
[mvs_member_photos user_id="42" columns="3" per_page="12" type="image" show_header="true" actions="true"]
Attribute Default Description
user_id (auto-detect) Force a specific user. Leave empty to use the four-step resolution chain.
columns 3 Grid columns.
per_page 12 Items per page.
type (all types) Filter by media type: image, video, or audio.
show_header true Show the member header above the grid.
actions true Show per-item action controls.

[mvs_pdf_viewer]

Embeds a PDF using the browser-native viewer (the #view=FitH URL fragment). No third-party JS, no licensing concerns.

Added in 1.2.0.

[mvs_pdf_viewer id="123"]
[mvs_pdf_viewer id="123" height="800" toolbar="false"]
Attribute Default Description
id (required) Media ID of the PDF.
height 600 Viewer height in pixels. Range: 200-1400.
toolbar true Show or hide the browser PDF toolbar. true to show, false to hide.

Usage History

Free. Requires a logged-in visitor - renders nothing for guests.

Shows the current member's own upload usage ledger. Useful on an account page beside the quota widget when you sell upload packages with Pro.

[mvs_usage_history]
[mvs_usage_history limit="50"]
Attribute Default Description
limit 20 Number of ledger rows to show, most recent first.

Pro: Compete shortcodes

These require WPMediaVerse Pro, and each one needs its feature toggle enabled. With the toggle off the shortcode renders nothing rather than an error.

Every block in the Compete set has a matching shortcode, so you can place competitions on a classic-editor page or inside a page builder.

Shortcode Feature toggle Purpose
[mvs_pro_compete_hub] any Compete feature The combined hub - challenges, battles, tournaments and leaderboard in one tabbed surface.
[mvs_pro_challenge id="12"] mvs_challenges_enabled One challenge, with its entry or voting interface.
[mvs_pro_challenges_list] mvs_challenges_enabled All challenges, grouped by state.
[mvs_pro_battle id="8"] mvs_battles_enabled One head-to-head battle with its voting UI.
[mvs_pro_battles_active] mvs_battles_enabled Every battle currently open for voting.
[mvs_pro_tournament id="3"] mvs_tournaments_enabled One tournament bracket.
[mvs_pro_tournaments_list] mvs_tournaments_enabled All tournaments, grouped by state.
[mvs_pro_leaderboard] - Top creators ranking.

The single-item shortcodes (mvs_pro_challenge, mvs_pro_battle, mvs_pro_tournament) take a required id attribute and render nothing when it is missing or zero.

Leaderboard attributes

[mvs_pro_leaderboard source="reactions" window="month" per-page="25"]
Attribute Default Description
source reactions What to rank by.
window all Time window for the ranking. all counts everything.
per-page 10 Number of creators listed.

Presentation attributes

All eight Compete shortcodes accept the same optional presentation attributes, which map to the equivalent block settings. Leave them empty to inherit your theme's styling.

Attribute Description
padding-desktop, padding-tablet, padding-mobile Per-breakpoint padding.
margin-desktop, margin-tablet, margin-mobile Per-breakpoint margin.
border-width, border-color, border-radius Border styling.
shadow-enabled, shadow-offset-x, shadow-offset-y, shadow-blur Drop shadow.

Use hyphens in shortcode attributes (padding-desktop); they are converted to the camelCase block attributes internally.

Pro: Layout feed shortcodes

Require WPMediaVerse Pro. See Layout Modes for what each layout looks like.

Shortcode Layout
[mvs_pro_instagram_feed] Instagram-style square grid
[mvs_pro_flickr_feed] Flickr-style justified rows
[mvs_pro_pinterest_feed] Pinterest-style masonry columns
[mvs_pro_dribbble_feed] Dribbble-style cards with stats

These accept the same presentation attributes as the Compete shortcodes.

Gutenberg Blocks

Included in Free - This feature is available in the free version of WPMediaVerse.

WPMediaVerse registers 9 Gutenberg blocks under the WPMediaVerse block category. All blocks use the WordPress Interactivity API for reactive front-end behavior without a separate JavaScript framework.

Gutenberg block inserter showing the WPMediaVerse block category

Block List

Block Name Handle Description
Media Upload mvs/media-upload Frontend file upload form with drag-and-drop
Media Grid mvs/media-grid Filterable, paginated media gallery grid
Media Player mvs/media-player Single-item audio/video player with controls
Album Viewer mvs/album-viewer Displays a single album's media in a grid
Member Photos mvs/member-photos Auto-detects the user (explicit userId → BuddyPress displayed user → post author → current user) and renders their media grid
PDF Viewer mvs/pdf-viewer Browser-native PDF embed using the #view=FitH URL fragment. Configurable height, optional toolbar, five distinct empty states
Media Stats mvs/media-stats Site-wide or per-user media statistics
Explore Feed mvs/explore-feed Infinite-scroll explore feed (all public media) with search autocomplete
Lock Overlay mvs/lock-overlay Paywall/restriction overlay for any block

Story Viewer is not in this list of 9. The block source and the server-side StoryService that used to ship here were relocated to Pro in 1.9.0 along with a complete Stories feature (create-flow toggle, viewer, REST API, view receipts). See Stories (Pro).

Interactivity API Architecture

Each block ships with a view.js module registered via wp_enqueue_script_module(). Blocks communicate through shared state in the mvs interactivity store. The shared-ui module provides common utilities (REST fetching, nonce management) shared by all blocks.

Using Blocks in Templates

All registered blocks can be used in Full Site Editing (FSE) templates, template parts, and block patterns. They are fully compatible with query blocks and block themes.

Media Upload Block

Media Upload block in the editor

Block Settings:

  • Max Files per Upload (default: 10)
  • Show Privacy Selector (default: on)

Upload modal polish (1.2.0):

  • Preview tiles show the filename (truncated tastefully) and a per-tile remove (×) button.
  • Audio files render with a dedicated audio fallback icon instead of a broken thumbnail.
  • A row of eight popular tag pills appears below the tags input - click to append, no duplicates.

Media Grid Block

Media Grid block showing filter controls and grid layout

Block Settings:

  • Media Type Filter (image/video/audio/all)
  • Category Filter
  • Tag Filter
  • Sort Order - date, title, popular, views, reactions, random
  • Sort Direction - asc / desc
  • User ID filter (userId) - restrict the grid to a single author
  • Lightbox (default: on)
  • Show Reactions (default: on)

Grid columns and pagination inherit from Media > Settings > Display.

Sorting (1.2.0): Most Popular and Most Reactions sorts join the mvs_media_stats table to rank by aggregate engagement. Random sort reshuffles each page load.

Media Player Block

Block Settings:

  • Media ID (required - use the picker to select)
  • Autoplay (default: off)
  • Loop (default: off)
  • Show Download Button (default: off)

Album Viewer Block

Block Settings:

  • Album (select from a dropdown of your albums)
  • Columns override
  • Show Title / Show Description

Story Viewer (Pro)

Stories moved to Pro in 1.9.0 as a complete feature: the mvs/pro-stories block (stories bar + fullscreen viewer), a create-from-the-bar upload tile, mvs-pro/v1 REST routes, and view receipts. It is a Pro-registered block, not one of the 9 free blocks above. See Stories (Pro) for the full reference.

Member Photos Block

The Member Photos block renders a single member's media grid. The block resolves the user automatically using a four-step fallback chain so the same block works in profile templates, member-specific pages, author archives, and BuddyPress profile tabs without per-page wiring.

Resolution order:

  1. Explicit userId attribute on the block
  2. BuddyPress displayed user (when on a BP profile)
  3. Post author (when in a single-post context)
  4. Current logged-in user

Block Settings:

  • User ID (optional - leave empty to auto-detect)
  • Items Per Page
  • Sort Order - same options as Media Grid

PDF Viewer Block

The PDF Viewer block embeds a PDF using the browser's native viewer via the #view=FitH URL fragment - no third-party JS, no licensing concerns.

Block Settings:

  • Media ID (required - pick a PDF media item)
  • Height (200-1400 px, default 600)
  • Show Toolbar (default: on)

Empty states: the block emits five distinct empty states - no media selected, wrong media type, missing file, access denied, and a generic fallback.

Media Stats Block

Block Settings:

  • Show Views (default: on)
  • Show Downloads (default: on)
  • Show Reactions (default: on)
  • Show Top Media (default: on)
  • Top Media Count (default: 5)

Explore Feed Block

The Explore Feed block provides an infinite-scroll feed of all public media. It supports URL-based filtering via ?mvs_tag=slug and ?s=search-term query parameters.

Search autocomplete (1.2.0): the search input now shows a type-ahead dropdown - top eight title matches, debounced 250 ms, full keyboard navigation (Arrow keys, Enter, Escape) and ARIA combobox semantics for screen reader users.

Lock Overlay Block

The Lock Overlay block wraps any other block content and shows a restriction message to users who do not meet access criteria. Configure access rules via the Access Control REST API.

Internal blocks (not in the inserter)

Four registered blocks are deliberately hidden from the block inserter. They carry no markup of their own - each one registers an Interactivity API store that the plugin's templates bind to, which is how a page rendered by a shortcode still gets a fully reactive interface.

Block Purpose
mvs/dashboard-view Dashboard store for the My Media, Albums and Favorites tabs
mvs/explore-view Explore page tag-cloud store
mvs/media-social Social interactions store for single media and album pages
mvs/shared-ui Shared UI store - toasts, confirm dialogs, tag autocomplete

Each declares "supports": { "inserter": false } in its block.json, so it never appears in the editor. You do not add these to a page and there is nothing to configure. They are listed here so that a block audit of the site does not report them as stray registrations.

Do not remove these from a custom build. The dashboard, explore and single-media templates bind to these stores by name. Dropping one leaves the matching interface inert - buttons render but nothing responds.

GDPR & Privacy Compliance

Included in Free - This feature is available in the free version of WPMediaVerse.

WPMediaVerse integrates with the WordPress privacy tools built into Tools > Export Personal Data and Tools > Erase Personal Data. No configuration is required. The integration is active whenever the plugin is active.

The privacy functionality lives in GDPRService.php.

What Gets Exported

When an administrator runs a personal data export for a user, WPMediaVerse adds the following data groups to the export ZIP:

Data Group What Is Included
Media uploads File URLs, titles, descriptions, privacy level, upload date
Comments All comments the user posted on media items
Reactions Each reaction (emoji, media item, timestamp)
Favorites Each media item the user saved as a favorite
Direct messages All conversation participants and message content
Follow relationships List of users the subject follows and users following the subject

The export respects the WordPress standard format. Each group appears as a named section in the downloadable HTML and JSON files.

What Gets Erased

When an administrator runs a personal data erasure for a user, WPMediaVerse removes:

  • All media items uploaded by that user (files and database records)
  • All comments posted by that user on media items
  • All reactions and favorites
  • All direct message conversations and messages where the user is a participant
  • All follow relationships (both directions)

Erasure is permanent and cannot be undone. Media items that belong to BuddyPress groups are also removed.

Before erasing, WordPress asks the user to confirm the request via email. WPMediaVerse erasure only runs after that confirmation is received.

Privacy Policy Text

WPMediaVerse registers suggested privacy policy text via wp_add_privacy_policy_content(). The suggestion appears in the Privacy Policy Guide at Settings > Privacy > Privacy Policy Guide.

The suggested text describes:

  • What media metadata is collected and stored
  • How direct messages are stored and for how long
  • What follow data is retained
  • How to request export or erasure

You are not required to use the suggested text verbatim. Review it and incorporate the relevant parts into your site's privacy policy.

Developer Notes

WPMediaVerse registers its exporters and erasers through the standard WordPress privacy hooks. It does not add its own wrapper filters - extend the export/erase flow with the WordPress-core filters directly.

Adding Custom Data to the Export

Register your own exporter via WordPress core's wp_privacy_personal_data_exporters filter:

add_filter( 'wp_privacy_personal_data_exporters', function( $exporters ) {
    $exporters['my-extension'] = [
        'exporter_friendly_name' => __( 'My Extension Data', 'my-extension' ),
        'callback'               => 'my_extension_export_callback',
    ];
    return $exporters;
} );

Hooking into Erasure

Register your own eraser via WordPress core's wp_privacy_personal_data_erasers filter:

add_filter( 'wp_privacy_personal_data_erasers', function( $erasers ) {
    $erasers['my-extension'] = [
        'eraser_friendly_name' => __( 'My Extension Data', 'my-extension' ),
        'callback'             => 'my_extension_erase_callback',
    ];
    return $erasers;
} );

User Blocking & Reporting

Included in Free - This feature is available in the free version of WPMediaVerse.

WPMediaVerse includes a blocking system that prevents specific users from interacting with you and a reporting system that lets users flag abusive content or accounts for moderator review.

User profile page showing Block User and Report User options

Blocking a User

You can block a user from two places:

  • From their profile page - Open the action menu on the user's profile card and select Block User.
  • From the report flow - After submitting a report against a user, you are offered the option to also block them.

What Blocking Does

When you block a user, they cannot:

  • View your media items (media items are hidden from them on all browse pages and their direct URLs return a 403)
  • Send you direct messages (your profile shows no Message button for them; any existing conversation is locked)
  • Follow you (the Follow button is removed for them)
  • Comment on your media
  • React to your media

Blocking is one-directional. You can still view the blocked user's public media unless you also choose to hide it.

Blocks are stored per-user and do not require any admin action.

Confirmation dialog after blocking a user

Unblocking a User

Go to Account Settings > Blocked Users to see your full block list and remove individual blocks. You can also unblock via the REST API.

User Reporting

Reporting a user notifies the site moderators. It does not automatically take any action on the reported account.

To report a user, open the action menu on their profile card and select Report User. Choose a reason from the dropdown and optionally add details.

Report Reasons

The same reason whitelist applies to both user and media reports:

Value
spam
harassment
nudity
violence
copyright
misinformation
other

Reports appear in Media > Media Moderation under the Users tab. Moderators with the moderate_mvs_media capability can review, dismiss, or act on reports.

Media Reporting

Any logged-in user can report a media item from the media card or the media detail page using the flag icon.

Report Reasons

Media reports use the same whitelist as user reports: spam, harassment, nudity, violence, copyright, misinformation, other.

When a media item accumulates reports equal to the Auto-Hide Threshold set in Media > Settings > AI & Moderation, it is automatically hidden and added to the moderation queue.

Media card showing the report flag icon

REST API

Base URL: /wp-json/mvs/v1/

All endpoints require a logged-in user. Pass the X-WP-Nonce header with a nonce from wp_create_nonce( 'wp_rest' ).

POST /users/{id}/block

Block a user.

Response: 200 OK

{ "blocked": true }

DELETE /users/{id}/block

Unblock a user.

Response: 200 OK

{ "blocked": false }

GET /me/blocked

List users blocked by the current user.

Parameters:

Parameter Type Default Description
per_page int 20 Blocked users per page
page int 1 Page number

Response: Array of user objects with id, name, and avatar_url.


POST /users/{id}/report

Report a user account.

Body:

Field Required Description
reason Yes One of: spam, harassment, nudity, violence, copyright, misinformation, other
details No Optional free-text note visible to moderators

Response: 200 OK

{ "reported": true }

POST /media/{id}/report

Report a media item.

Body:

Field Required Description
reason Yes One of: spam, harassment, nudity, violence, copyright, misinformation, other
details No Optional free-text note

Response: 200 OK

{ "reported": true }

Actions and Filters

mvs_user_blocked

Fires after a user is blocked.

add_action( 'mvs_user_blocked', function( $blocker_id, $blocked_id ) {
    // Perform additional cleanup or logging.
}, 10, 2 );

mvs_report_submitted

Fires after a report (user or media) is submitted. The $target_type is user or media.

add_action( 'mvs_report_submitted', function( $report_id, $reporter_id, $target_type, $target_id, $reason ) {
    // Notify a Slack channel, send to an external moderation service, etc.
}, 10, 5 );

Image Optimization

Included in Free - This feature is available in the free version of WPMediaVerse.

WPMediaVerse automatically reduces image file sizes at upload time. No extra plugin is required. Originals are never made larger: if re-encoding produces no gain, the original file is kept untouched.

What It Does

When a JPEG, PNG, or GIF is uploaded:

  1. The file is re-encoded at high quality with embedded camera metadata stripped.
  2. The result is compared to the original. If it is smaller, the smaller file is committed. If not, the original is kept.
  3. A WebP copy is generated alongside the original and every thumbnail size (on by default).
  4. An AVIF copy can optionally be generated for even smaller file sizes (off by default; slower to encode).

Animated GIFs are detected and skipped from the lossless re-encode step. Their frames are preserved.

How Browsers Receive Images

WPMediaVerse uses a <picture> element with progressive fallback. Browsers receive the most efficient format they support:

  1. AVIF (if enabled and available)
  2. WebP (if enabled and available)
  3. Original JPEG/PNG/GIF (always served as a fallback)

This format negotiation applies across the explore grid, BuddyPress activity stream, the media dashboard, single media pages, and the lightbox. No JavaScript is required; it is handled by native browser behavior.

Settings

Access these settings at Media > Settings > Storage.

Setting Default Description
Compress uploaded images On Re-encodes JPEG, PNG, and GIF originals to remove camera metadata and reduce file size. Works alongside EWWW, Imagify, Smush, and ShortPixel.
Create WebP copies for faster loading On Saves a WebP copy next to every original and thumbnail. WebP files are typically 25 to 35 percent smaller than JPEG.
Create AVIF copies for the smallest possible files Off Saves an AVIF copy next to every original and thumbnail. AVIF is around 30 to 50 percent smaller than WebP. Encoding is much slower than WebP. Requires Imagick with libheif, or GD on PHP 8.1+ with libavif.

Toggling any setting takes effect on the next upload. Settings do not retroactively process existing media.

Admin: Optimization Column

The Optimization column on the Media > All Media list shows the result for each image:

Badge Meaning
-23% (green) The original was re-encoded and reduced by that percentage.
WebP ready (green) No lossless gain on the original, but a WebP copy was generated.
No lossless gain (grey) Re-encoding did not reduce the file and WebP was not generated.
Not optimized The image has not been through the optimization pipeline yet.

Each image row also has two row actions: Optimize (re-runs the optimization pipeline for that image) and Details (opens a detail page at ?page=mvs-media&view=details&media_id=N showing file sizes, savings percentage, and variant URLs).

Bulk Optimization and WP-CLI

To optimize images uploaded before 1.3.0, use WP-CLI:

wp mvs optimize <id>
wp mvs optimize-bulk

Both commands are resume-safe. See the WP-CLI reference for all available options.

For Developers

To replace the built-in optimizer with an external service (EWWW, Imagify, Smush, ShortPixel, or a custom compressor), hook into the mvs_optimize_image filter. See the Hooks and Filters reference for the full signature and examples.

BuddyPress Integration

Profile tabs, group media, activity feed, and notifications.

BuddyPress Integration Overview

Included in Free - WPMediaVerse is the most complete media solution for BuddyPress communities. Integration is optional - the plugin works standalone on any WordPress site, but when BuddyPress is active, it unlocks profile tabs, group media, activity stream, and notifications automatically.

WPMediaVerse integrates with BuddyPress to add media features directly into the BuddyPress social layer. The integration is automatic - no configuration needed. It activates when BuddyPress is active and gracefully skips all BP-specific code when BuddyPress is not installed.

Requirements

  • BuddyPress 12.0+
  • WPMediaVerse 1.0.0+

What the Integration Adds

BuddyPress Component Feature Added
Activity Records activity on media upload, comment, and album additions
Member Profiles Adds a Media tab showing the member's uploads
Groups Adds a Media tab in group navigation
Notifications Sends notifications for reactions, comments, and @mentions
Activity Post Form Adds a media attach button to the activity update form

Activation Check

The integration loads via BuddyPressManager::init(), which is called on plugins_loaded. Every feature check begins with:

if ( ! function_exists( 'buddypress' ) ) {
    return;
}

Individual features additionally check bp_is_active( 'activity' ), bp_is_active( 'groups' ), and bp_is_active( 'notifications' ) before hooking.

Multisite Compatibility

The BuddyPress integration works on WordPress Multisite networks. Activity and notifications are scoped to the site where BuddyPress is installed. Media uploaded on a subsite is recorded in that subsite's activity stream.

Profile Media Tab

Included in Free - WPMediaVerse is the most complete media solution for BuddyPress communities. Integration is optional - the plugin works standalone on any WordPress site, but when BuddyPress is active, it unlocks profile tabs, group media, activity stream, and notifications automatically.

When BuddyPress is active, WPMediaVerse adds a Media tab to every user's BuddyPress profile page.

BuddyPress member profile with Media tab active

Tab Location

The tab appears in the main BuddyPress profile navigation at:

/members/{username}/media/

It is added via bp_setup_nav at priority 100.

What the Tab Shows

The Media tab displays the profile owner's published media items in a paginated grid. Privacy filtering applies:

  • Visitors see only the profile owner's public media.
  • Logged-in users see public and members-only media.
  • BuddyPress friends see public, members-only, and friends media.
  • The profile owner sees all their own media.
  • Moderators (moderate_mvs_media) see all media.

Media tab content showing grid of photos with privacy badges

Profile URL Pattern

WPMediaVerse also registers a standalone media profile URL:

/media/@{username}/
/media/@{username}/page/2/

These URLs are handled by the TemplateLoader and use the explore.php template, filtered to the specific user.

User Profile Edit Page

Logged-in users can edit their avatar and profile details at:

/media/edit-profile/

Or use the [mvs_profile_edit] shortcode on any page.

Disabling the Profile Tab

To remove the profile tab without disabling the entire integration:

remove_action( 'bp_setup_nav', array( $integration, 'add_profile_tab' ), 100 );

Since $integration is not easily accessible externally, the cleaner approach is to use the tab's existence to skip rendering:

add_action( 'bp_setup_nav', function() {
    // Remove the WPMediaVerse media nav item from BP.
    bp_core_remove_nav_item( 'media' );
}, 200 );

Group Media Tab

Included in Free - WPMediaVerse is the most complete media solution for BuddyPress communities. Integration is optional - the plugin works standalone on any WordPress site, but when BuddyPress is active, it unlocks profile tabs, group media, activity stream, and notifications automatically.

When BuddyPress Groups is active, WPMediaVerse adds a Media tab to every BuddyPress group.

BuddyPress group page with Media tab

Tab Location

The tab appears in the group navigation at:

/groups/{group-slug}/media/

It is registered via bp_setup_nav at priority 100, conditional on bp_is_active( 'groups' ).

What the Tab Shows

The group media tab displays media that was:

  • Uploaded with privacy=group and group_id={this-group-id}.
  • Reassigned to the group via the mvs_media_group_assigned action.

Privacy applies within the group: only group members can view the tab content.

Assigning Media to a Group

When uploading, set the privacy to group and provide the group_id:

curl -X POST https://yoursite.com/wp-json/mvs/v1/media \
  -H "X-WP-Nonce: NONCE" \
  -F "file=@photo.jpg" \
  -F "privacy=group" \
  -F "group_id=42"

This sets the media row's privacy to group (a column in mvs_media_index) and stores group_id=42 (in mvs_media_meta). Media is tracked in WPMediaVerse's custom tables, not as a WordPress post.

Group Activity Integration

Media uploaded to a group fires the mvs_media_group_assigned action:

do_action( 'mvs_media_group_assigned', $media_id, $group_id );

The BuddyPress integration listens to this action and re-scopes the upload's activity item from the member component to the groups component, so it appears in the group's activity stream rather than the member's personal stream.

BuddyPress group activity stream with media upload

Group Activity in the Activity Post Form

When a group member uses the BP activity post form inside a group, the Attach Media button (added by WPMediaVerse) automatically assigns uploaded media to that group.

Activity Stream Media

Included in Free - WPMediaVerse is the most complete media solution for BuddyPress communities. Integration is optional - the plugin works standalone on any WordPress site, but when BuddyPress is active, it unlocks profile tabs, group media, activity stream, and notifications automatically.

WPMediaVerse records media events as BuddyPress activity items and enhances existing activity with media thumbnails and inline video players.

Activity Types Registered

WPMediaVerse registers two custom activity action types:

Action Type Component Label
mvs_media_upload wpmediaverse Media Uploads
mvs_media_upload groups Group Media Uploads
mvs_comment wpmediaverse Media Comments

These appear in the BuddyPress activity filter dropdown.

When Activity Is Recorded

Event Action Hook Activity Recorded
Media uploaded via UploadService mvs_media_uploaded "Username uploaded a new [type]" with thumbnail
Comment posted on media mvs_comment_created "Username commented on [media title]" (synced as an activity comment)
Media added to an album mvs_album_items_added Updates existing upload activity to reference the album
Media assigned to a group mvs_media_group_assigned Reassigns activity to the group component
Bulk album upload mvs_album_items_added One grouped activity for all files in the same upload action - see below

Bulk album upload activity grouping (1.2.0)

When a user uploads multiple files at once via the album upload modal, WPMediaVerse emits one grouped activity entry for the whole batch instead of one entry per file:

Username uploaded 3 photos to album Portrait Series - with a 3-thumbnail grid

The mechanism: the upload modal sends ?album_upload=1 with each per-file POST /media request. The mvs_media_uploaded listener records a skip flag on the activity row, suppressing per-media activity creation. After the JS link call finishes, mvs_album_items_added fires once with the bundled media IDs and the listener emits a single grouped bp_activity_add with the thumbnail grid as content. Single-file album uploads still produce a per-photo activity (no bundling needed); ad-hoc photo posts via the activity composer keep their existing one-row-per-post behaviour.

Activity Format

BuddyPress activity item showing a media upload

Upload activities use the format:

Username uploaded a new [type]

When the media belongs to an album:

Username uploaded a new [type] to album [Album Name]

For group uploads:

Username uploaded a new [type] in the group [Group Name]

Thumbnail Injection

WPMediaVerse injects media thumbnails into activity items in two ways:

  1. bp_get_activity_content_body filter (priority 0) - transforms activity content to include an image tag.
  2. bp_activity_entry_content action - injects thumbnails for activities with empty content (common for imported media).

Inline video players are injected for video media via inject_video_player_in_activity.

Attaching Media via the Activity Post Form

The Attach Media button appears in the BuddyPress activity post form for members and inside groups.

BuddyPress activity post form with Attach Media button

Users select previously uploaded media or upload new files inline. The media IDs are attached to the activity via bp_activity_posted_update / bp_groups_posted_update.

BuddyNext: Media Links Open Their Activity Post (2.0.0)

When the BuddyNext theme is active, WPMediaVerse media stops behaving like a separate public page by default. Clicking a media item, or visiting its /media/{slug}/ URL directly, redirects (HTTP 301) to the BuddyPress activity entry the media was originally posted in - so a photo lives in the community feed, not as a standalone URL alongside it.

This is implemented through a single filter WPMediaVerse exposes for any host to use:

apply_filters( 'mvs_single_media_redirect', '', $media_id, $slug );

Return a URL to redirect /media/{slug}/ there instead of rendering the native single-media page; return '' (the default) to render the native page. Standalone WPMediaVerse installs (no BuddyNext) always return '', so this only changes behavior when BuddyNext is active and hooks the filter.

If you prefer a dedicated, standalone page per media item even with BuddyNext active, BuddyNext ships its own settings toggle to switch back to that mode - check BuddyNext's own settings screen for the exact option, since it lives in the theme, not in WPMediaVerse.

WPMediaVerse also exposes filterable seams so BuddyNext (or any profile system) can point member links and avatars at its own profile/avatar URLs instead of WPMediaVerse's standalone /media/@handle/ profile:

  • mvs_user_profile_url ($url, $user_id) - overrides the profile link used across grids, cards, notifications, and share text.
  • mvs_user_avatar_url ($url, $user_id, $size) - overrides the avatar image URL.

Allowed HTML Tags in Activity

WPMediaVerse extends the bp_activity_allowed_tags filter to allow its custom HTML attributes (data-mvs-*, data-wp-*) to pass through BP's kses filter without being stripped.

BuddyPress Notifications

Included in Free - WPMediaVerse is the most complete media solution for BuddyPress communities. Integration is optional - the plugin works standalone on any WordPress site, but when BuddyPress is active, it unlocks profile tabs, group media, activity stream, and notifications automatically.

When BuddyPress Notifications is active, WPMediaVerse sends in-app notifications for media social events.

Notification Types

Event NotificationService Type BP Component Action Who Gets Notified
Someone reacts to your media media_reaction mvs_new_reaction Media owner
Someone comments on your media media_comment mvs_new_comment Media owner
Someone @mentions you in a comment media_mention mvs_new_mention Each mentioned user

NotificationIntegration subscribes to the single mvs_notification_created signal emitted by NotificationService::create() and mirrors these three types into BuddyPress via bp_notifications_add_notification(). It does not listen on raw plugin hooks (which previously caused duplicate notifications).

Notification Registration

WPMediaVerse registers wpmediaverse as a BuddyPress notification component via the bp_notifications_get_registered_components filter.

Notification format strings are registered via:

add_filter( 'bp_notifications_get_notifications_for_user', ... );

Notification Format

Notifications appear in the BuddyPress notification bell with these formats:

Type Format
Reaction Username reacted to your media
Comment Username commented on your media
Mention Username mentioned you

BuddyPress notification dropdown showing WPMediaVerse notifications

Notification Filters (BP Nouveau)

In BuddyPress Nouveau, notification filter links are registered via bp_nouveau_notifications_init_filters to allow users to filter their notification list by WPMediaVerse notifications.

Reading Notifications via REST API

curl https://yoursite.com/wp-json/mvs/v1/me/notifications \
  -H "X-WP-Nonce: NONCE"

This returns WPMediaVerse-specific notifications. For the full BuddyPress notification list, use the BP REST API.

Marking Notifications as Read

curl -X POST https://yoursite.com/wp-json/mvs/v1/me/notifications/read \
  -H "X-WP-Nonce: NONCE" \
  -H "Content-Type: application/json" \
  -d '{"ids": [123]}'

Notification Count

The frontend reads the unread count from GET /mvs/v1/me/notifications/count, which returns {"count": N} for the current user, without requiring WebSockets.

2.0.0 update - no double-notify on activity comments

When a media comment is posted from inside a linked BuddyPress activity (an upload shared to the activity stream), BuddyPress already fires its own native "replied to your update" notification. Before 2.0.0, WPMediaVerse also mirrored its own media_comment notification for the same comment, so the media owner saw two bell entries for one comment.

NotificationIntegration now detects this case (a comment whose media has a linked bp_activity_id that BuddyPress itself will notify on) and skips the BP-mirrored media_comment notification. The native MVS in-app notification is unaffected - GET /mvs/v1/me/notifications and any REST/app client still see it; only the duplicate BuddyPress bell entry is suppressed.

Restore the old double-notify behavior with a filter, if a site wants both:

add_filter( 'mvs_suppress_bp_comment_notification', '__return_false' );

See mvs_suppress_bp_comment_notification in the hooks reference.

1.2.0 update - single notification surface

When BuddyPress is active, every WPMediaVerse notification is mirrored to BuddyPress via bp_notifications_add_notification, and the standalone dashboard .mvs-notification-bell markup is suppressed. This means BP-active sites see one bell - the BP nav bell - instead of two competing bells rendering the same notifications.

This is automatic. No setting to flip, no filter to add. If BuddyPress is deactivated, the standalone WPMediaVerse bell returns automatically.

Pro Features

Layout modes, cloud storage, video, quotas, and advanced privacy.

WPMediaVerse Pro

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro extends the free plugin with advanced layout modes, cloud storage, video processing, AI providers, quota management, and granular privacy controls.

WPMediaVerse Pro license key entry screen

Requirements

  • WPMediaVerse (free) 2.0.0 or higher installed and activated (Pro halts with an admin notice on older Free)
  • WordPress 6.5+
  • PHP 7.4+
  • MySQL 5.7+ or MariaDB 10.4+

Installation

  1. Install and activate the free WPMediaVerse plugin first.
  2. Go to Plugins > Add New Plugin > Upload Plugin.
  3. Upload the wpmediaverse-pro.zip file and click Install Now.
  4. Click Activate Plugin.
  5. Go to Media > License and enter your license key.
  6. Click Activate License.

Media License settings page with activation status

All Pro features run as soon as the plugin is activated. The license key is used only to unlock automatic updates - it does not gate any feature, so every Pro capability works regardless of license state.

Updating Pro

When an update is available, WordPress shows it in the standard Plugins screen. The updater requires a valid active license. If your license has expired, your installed Pro features keep working - only automatic updates are paused. Download the latest ZIP from your account at wbcomdesigns.com and upload it manually.

1.2.0: All Pro features now Gutenberg blocks

Every Pro feature now ships as a first-class Gutenberg block - drop-in, configurable in the editor, no shortcodes-as-the-only-option. Each block has a matching [mvs_pro_*] shortcode for classic editor and template-tag use.

Block Handle Notes
Tournament mvs/pro-tournament Configurable tournamentId attribute.
Tournaments List mvs/pro-tournaments-list Lists active tournaments.
Challenge mvs/pro-challenge Configurable challengeId attribute.
Challenges List mvs/pro-challenges-list Lists active and upcoming challenges.
Battle mvs/pro-battle Configurable battleId attribute.
Battles Active mvs/pro-battles-active Currently running battles.
Instagram Feed mvs/pro-instagram-feed Instagram-style square grid layout.
Flickr Feed mvs/pro-flickr-feed Flickr-style justified rows layout.
Pinterest Feed mvs/pro-pinterest-feed Pinterest-style masonry layout.
Dribbble Feed mvs/pro-dribbble-feed Dribbble-style card grid layout.
Leaderboard mvs/pro-leaderboard Top performers across competitions.
Compete Hub mvs/pro-compete-hub Combined challenges + battles + tournaments dashboard.
Stories mvs/pro-stories 24-hour ephemeral stories bar + fullscreen viewer, with a same-screen "Your story" upload tile. Added in 1.9.0.

Layout flexibility: because each layout (Instagram, Flickr, Pinterest, Dribbble) is its own block, admins can mix layouts on different pages - e.g. a Pinterest feed on the home page and an Instagram feed on a member directory - instead of being locked to one site-wide layout setting.

MigrationPage admin restructure

The migration tool admin page is now a generic shell that hosts per-platform cards (rtMedia, MediaPress, BuddyBoss). Two pre-existing detection bugs were fixed in the same pass: the Imported count was always 0 regardless of actual progress, and the MediaPress dedup query was running against an undefined $wpdb. Migrations now report accurate counts and skip already-imported items correctly.

Pro Feature Categories

Category Page What It Adds
Layout Modes layout-modes.md Instagram, Pinterest, Flickr, and Dribbble feed layouts
Cloud Storage cloud-storage.md Amazon S3 and BunnyCDN storage drivers
Video Transcoding video-transcoding.md Async FFmpeg transcoding to 720p/480p/360p and HLS
Video Chapters video-chapters.md Chapter markers and resume playback
Auto-Captions auto-captions.md OpenAI Whisper transcription and WebVTT captions
Watermarking watermarking.md GD-based text and logo watermarks on media
Video Analytics video-analytics.md Play event tracking, heatmaps, and retention reports
AI Providers ai-providers.md Google Cloud Vision, AWS Rekognition, and Claude (Anthropic) support
Quotas quotas.md Per-user storage and count limits with membership integration
Advanced Privacy advanced-privacy.md Multi-level privacy, presets, album inheritance, and bulk updates
Connected Accounts connected-accounts.md Connect Flickr to import/export photos and auto-push new uploads
Stories stories.md 24-hour ephemeral stories with a tap-to-advance viewer and "seen by" receipts
Save to Collections collections.md A dedicated Save control for adding media to any number of named collections, separate from favoriting
Mobile App mobile-app.md White-label branding, feed layout, push notifications, and a leaderboard endpoint for the native app

User Reports (Pro Moderation)

Pro adds a Reports view that surfaces user-submitted abuse reports on media and members - the complaints your community files, as opposed to the free Moderation Queue, which is about AI/auto-flagged content awaiting an approve/reject decision.

  • Where: MediaVerse > Moderation, in the User Reports tab. (It is also reachable directly at admin.php?page=mvs-reports.)
  • Who: any user with the moderate_mvs_media capability.
  • What it lists: every report row - date, reporter, target type (media or user), the target, the reason code, and a details excerpt. A status filter switches between Pending, Resolved, and Dismissed, each with a live count.
  • Actions: on a pending report you can Resolve (mark handled) or Dismiss (no action needed). Both are nonce-protected and capability-checked.

Note for the parent agent: this section documents the Pro Reports admin page inline. If the docs site wants it discoverable on its own, it could later be split into a dedicated pro-features/reports.md page - the content above is self-contained and ready to lift out.

License Management

Setting Location Description
License Key Media > License Your product license key from wbcomdesigns.com
Activation Status Media > License Shows active, inactive, or expired
Deactivate License Media > License Release the activation to use on another site

A single license activates one site. Purchase additional activations from your account dashboard if you need to run Pro on multiple sites.

Layout Modes

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Transform your media community's look with one click - choose the visual style that fits your audience, from Instagram-style grids to Pinterest masonry boards.

What You Can Do

  • Switch between four distinct visual layouts without touching code
  • Match your community's purpose: photo sharing, discovery, portfolios, or design showcases
  • Drop a specific layout on any page with its dedicated block or shortcode (Instagram, Flickr, Pinterest, Dribbble feed)
  • Conditionally switch the active layout by page context using the mvs_active_layout filter

The Four Layouts at a Glance

Mode Best For
Instagram Photo-sharing communities, daily uploads, stories
Pinterest Inspiration boards, discovery-focused sites
Flickr Photography portfolios, camera clubs
Dribbble Design showcases, creative portfolios

How to Switch Layouts (for Site Owners)

  1. Go to Media > Settings > Display
  2. Find the Feed Layout option and select your preferred mode
  3. Click Save Settings - the explore page and all profile media tabs update immediately

The setting is stored in the mvs_pro_feed_layout option.

What Changes for Users When You Switch Layouts

  • The explore/browse page re-renders in the new layout style
  • All user profile media tabs switch to the new layout automatically
  • The lightbox updates: Flickr mode shows EXIF camera data in the sidebar; Instagram shows the swipe carousel
  • Stories appear above the grid in Instagram mode only
  • Search and tag filtering behave identically to the default layout, including the empty states (2.2.0): a zero-result search shows "No results for ..." with a Browse-all button and popular-tag chips, and an unknown tag shows "Tag not found" instead of an unfiltered feed

Layout mode selector in Settings showing all four options


Choosing a Layout Mode

The selected mode applies to the explore archive and the user profile media tab. For a specific layout on a specific page, use that layout's own block (e.g. Instagram Feed) or shortcode (e.g. [mvs_pro_instagram_feed]) instead of relying on the global setting.


Instagram Mode

Perfect for photo-sharing communities and daily uploads.

Instagram layout with recent uploaders bar and vertical card feed

The Instagram layout renders a vertical card feed. Each post shows the author avatar, full-width photo, reaction/comment/share buttons, like count, caption, and inline comment box - identical to the Instagram experience.

  • Recent uploaders appear as circular avatars above the feed (links to their profiles)
  • Each card has heart, comment, share, and bookmark buttons
  • Clicking "Expand" opens the lightbox with the full media detail view
  • Other users' posts show a "Following" button in the card header

Feed template: templates/layouts/instagram/feed.php Profile template: templates/layouts/instagram/profile.php


Pinterest Mode

Ideal for inspiration boards, discovery-focused sites, and mixed-format content.

Pinterest masonry layout with cards of varying heights

The Pinterest layout uses a masonry algorithm that preserves each image's original proportions. Cards include the media title and a truncated description below the image.

  • Column count is controlled by the Grid Columns display setting (2-4)
  • Hovering a card reveals a save-to-collection button
  • Infinite scroll loads the next page of results automatically

Feed template: templates/layouts/pinterest/feed.php Profile template: templates/layouts/pinterest/profile.php


Flickr Mode

Best for photography portfolios, camera clubs, and image-quality-focused communities.

Flickr justified gallery layout with full-width rows

The Flickr layout uses a justified gallery algorithm: images in each row are resized to fill the full container width while maintaining a consistent row height.

  • Clicking an image opens the lightbox with EXIF data displayed in the sidebar
  • Profile page shows a filmstrip-style contact sheet view

Feed template: templates/layouts/flickr/feed.php Profile template: templates/layouts/flickr/profile.php

Drop the Flickr layout on a specific page with the Flickr Feed block or the [mvs_pro_flickr_feed] shortcode.


Dribbble Mode

Great for design showcases, creative portfolios, and high-resolution work.

Dribbble layout showing large shot thumbnails in a 2-column grid

The Dribbble layout presents media as large portfolio shots in a 2-column grid. Each card shows the title, view count, and reaction count on hover. This layout is optimised for high-resolution PNG and GIF files.

  • Animated GIFs play on hover
  • Profile page shows featured work prominently at the top before the full grid

Feed template: templates/layouts/dribbble/feed.php Profile template: templates/layouts/dribbble/profile.php


Putting a Specific Layout on a Specific Page

Each layout ships as its own block and shortcode, so you can mix layouts across pages without changing the global setting:

Layout Block Shortcode
Instagram Instagram Feed [mvs_pro_instagram_feed]
Flickr Flickr Feed [mvs_pro_flickr_feed]
Pinterest Pinterest Feed [mvs_pro_pinterest_feed]
Dribbble Dribbble Feed [mvs_pro_dribbble_feed]

Overriding the Active Layout in Code

The global active-layout slug (read from the mvs_pro_feed_layout option) passes through the mvs_active_layout filter, so you can switch it by context:

add_filter( 'mvs_active_layout', function( $layout ) {
    // Force the Flickr layout on archive pages.
    if ( is_post_type_archive( 'mvs_media' ) ) {
        return 'flickr';
    }
    return $layout;
} );

Cloud Storage

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Stop storing media on your web server - offload every photo and video to Amazon S3 or BunnyCDN for faster delivery, lower server load, and global CDN performance.

Pluggable Storage Architecture

WPMediaVerse separates where media records live (always in your WordPress database) from where files are stored (local, S3, or BunnyCDN). Every upload goes through a StorageDriverInterface - a clean abstraction that means:

  • Switch drivers anytime - Change from local to S3 in one setting. Existing files continue to serve from their original location; new uploads go to the new driver.
  • Build your own driver - Developers can register custom drivers (Google Cloud Storage, DigitalOcean Spaces, Wasabi) by implementing the StorageDriverInterface. See Custom Storage Drivers.
  • Signed URLs for private media - Cloud drivers generate time-limited signed URLs so private and members-only media stays protected even when served from a public CDN.
  • No lock-in - Media metadata stays in your WordPress database regardless of storage driver. Switch providers without losing any data.

Why Use Cloud Storage

  • Your WordPress server no longer stores or serves media files - reducing disk usage and bandwidth costs
  • Files are served from edge locations closest to each visitor, so images load faster worldwide
  • S3 and BunnyCDN both scale to millions of files without any WordPress configuration changes
  • Private media gets signed URLs so only authorized users can access the actual file - even with cloud storage

How Media Serving Works in 1.4.0

Location-based serving

Each media item is served from where it is actually stored, based on its storage location and privacy setting at the time of the request. The active storage driver setting only controls where new uploads go - it does not affect files that were uploaded previously.

This means:

  • Switching the active driver (for example, from local to S3) does not break any existing media. Files already on S3 keep serving from S3. Files on local disk keep serving through the plugin's /serve route.
  • Enabling a cloud integration for the first time does not affect older uploads - they continue working as before.
  • Public media stored on cloud serves directly from the CDN (no WordPress request involved). Local media serves through the plugin's /serve proxy route.

Private media stays local

Only public media is eligible for cloud storage. Media with any other privacy setting (members-only, friends-only, private, or group) is always stored on the local server disk. This applies to the original file, all thumbnails, and all generated image variants (WebP, AVIF).

There is effectively one storage location per media item at any time: either cloud (for public media) or local (for everything else).

Per-request access check for private media

Every request to the /serve route for non-public media re-verifies the requesting user's view permission (can_view). A signed URL does not act as a transferable bearer token for private media. If the viewer no longer has access, the request returns 403 - even with a valid, unexpired signed URL. Public media uses bearer-style URLs (cacheable and shareable) because they carry no access restriction.

Cloudflare R2 requires a public domain

If you use Cloudflare R2 and have not configured a public domain (r2.dev subdomain or custom domain) on your bucket, WPMediaVerse will not emit the raw *.r2.cloudflarestorage.com API URL. That endpoint is never publicly readable. Instead, the plugin falls back to serving the file from the local copy via /serve.

To enable true CDN serving from R2, configure a public domain for your bucket in the Cloudflare R2 dashboard (either the r2.dev subdomain or your own custom domain), then enter that hostname in the CDN Domain field in Storage settings.

"Serve public cloud media directly" setting retired

The Serve public cloud media directly checkbox (mvs_cloud_direct_public_urls) has been removed from the settings UI in 1.4.0. Direct CDN serving for public cloud media is now automatic. You do not need to enable any toggle. The underlying option is retained in the database for back-compatibility, but it has no effect on serving behavior.

If you had this checkbox enabled before upgrading, no action is needed - behavior is the same or better.


Setting Up Amazon S3

  1. Log into your AWS Console and create an S3 bucket
  2. Create an IAM user with the required permissions (see IAM Policy below) and save the access key and secret key
  3. In WordPress, go to Media > Settings > Storage
  4. Set Storage Driver to Amazon S3
  5. Enter your bucket name, region, access key ID, and secret access key
  6. If you use CloudFront or a custom domain, enter the hostname in the CDN Domain field
  7. Click Test Connection - WPMediaVerse Pro uploads a small test file and reads it back to confirm everything works
  8. Click Save Settings - all new uploads now go directly to S3

S3 configuration fields in Storage settings

Setting Up BunnyCDN

  1. Log into your BunnyCDN dashboard and create a Storage Zone
  2. Note your storage zone name, API key, region, and pull zone hostname
  3. In WordPress, go to Media > Settings > Storage
  4. Set Storage Driver to BunnyCDN
  5. Enter your storage zone name, API key, region, and CDN hostname
  6. Click Test Connection to verify
  7. Click Save Settings - all new uploads now go to BunnyCDN

Cloud Storage settings panel showing driver selector

Choosing a Storage Driver

Go to Media > Settings > Storage and set the Storage Driver option. The value is stored in the mvs_storage_driver option.

Value Driver
local Default WordPress uploads directory (no Pro required)
s3 Amazon S3
bunnycdn BunnyCDN
r2 Cloudflare R2
dospaces DigitalOcean Spaces

Only one driver is active at a time. Switching drivers does not migrate existing files - previously uploaded files remain at their original URLs and continue to serve from their original location.


Amazon S3

S3 configuration fields in Storage settings

Settings

Option Option Key Description
S3 Bucket mvs_pro_s3_bucket The name of your S3 bucket
S3 Region mvs_pro_s3_region AWS region code, e.g. us-east-1
Access Key ID mvs_pro_s3_access_key Your AWS IAM access key ID
Secret Access Key mvs_pro_s3_secret_key Your AWS IAM secret access key
CDN Domain mvs_pro_s3_cdn_domain Optional CloudFront or custom domain for file URLs

Storing Credentials in wp-config.php

Instead of saving credentials to the database, define them as constants in wp-config.php:

define( 'MVS_PRO_AWS_ACCESS_KEY', 'AKIAIOSFODNN7EXAMPLE' );
define( 'MVS_PRO_AWS_SECRET_KEY', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' );

When these constants are defined, WPMediaVerse Pro uses them instead of the database values. The admin fields show a placeholder indicating constants are in use.

Required IAM Policy

Your IAM user needs at minimum:

{
  "Effect": "Allow",
  "Action": [
    "s3:PutObject",
    "s3:GetObject",
    "s3:DeleteObject",
    "s3:ListBucket"
  ],
  "Resource": [
    "arn:aws:s3:::your-bucket-name",
    "arn:aws:s3:::your-bucket-name/*"
  ]
}

CDN Domain

If you serve your bucket through CloudFront or a custom domain, enter the hostname (without trailing slash) in the CDN Domain field. WPMediaVerse Pro replaces the default S3 URL with this domain for all generated file URLs.


BunnyCDN

BunnyCDN configuration fields in Storage settings

Settings

Option Option Key Description
Storage Zone mvs_pro_bunny_zone Your BunnyCDN storage zone name
API Key mvs_pro_bunny_api_key Your BunnyCDN API key
Region mvs_pro_bunny_region Storage region: de, ny, la, sg, syd
CDN Hostname mvs_pro_bunny_cdn_hostname Your pull zone hostname, e.g. media.yoursite.b-cdn.net

Regions

Value Location
de Falkenstein, Germany (default)
ny New York, USA
la Los Angeles, USA
sg Singapore
syd Sydney, Australia

Testing the Connection

After saving settings, click Test Connection in the Storage settings panel. WPMediaVerse Pro uploads a small test file, reads it back, then deletes it. The result (success or error message) appears inline without a page reload.

Storage settings panel showing connection test result

If the test fails, verify your credentials, bucket name, and that the IAM or API key has sufficient permissions.


File Path Structure

Files are stored under the same path structure used for local uploads:

wpmediaverse/YYYY/MM/filename.ext

For S3 this becomes s3://your-bucket/wpmediaverse/YYYY/MM/filename.ext. For BunnyCDN it becomes a path within your storage zone.

Signed URLs with Cloud Storage

When media privacy is not public, WPMediaVerse Pro generates signed URLs through the /serve proxy route. The proxy re-verifies view permission on every request - signed URLs for non-public media do not grant transferable access. S3 presigned URLs and BunnyCDN token authentication are used for migration and admin operations, not for end-user delivery of private media.


Developer: Filtering Public Cloud URLs

The public-cloud serving behavior can be adjusted using three filters. Full parameter details are in the Developer Guide: Hooks and Filters.

Filter What it controls
mvs_serve_public_cloud_direct Return false to force all media back through the /serve proxy instead of emitting direct CDN URLs
mvs_public_cloud_thumbnail_url Rewrite or replace the direct CDN URL for a public cloud-hosted thumbnail
mvs_public_cloud_file_url Rewrite or replace the direct CDN URL for a public cloud-hosted original file

Video Transcoding

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Give your community professional-quality video playback with automatic quality selection - viewers on fast connections get 720p, mobile users on slower connections get 360p, automatically.

What Users See

When a user plays a video, they see a quality selector in the player corner showing available options: 720p, 480p, and 360p. The player automatically selects the best quality for their connection and lets them override it manually. Playback starts faster because the video streams progressively rather than requiring a full download.

While transcoding is in progress after upload, the video plays at its original uploaded resolution. Once transcoding completes (typically within a few minutes), the quality selector appears.

Single media page showing quality selector in the video player

What Admins Need

  • FFmpeg installed on the server and accessible from PHP. Verify this by running which ffmpeg via SSH.
  • Action Scheduler 3.0+ (bundled with WooCommerce, or install as a standalone plugin from wordpress.org)

Shared hosting plans often do not have FFmpeg. VPS and dedicated servers typically do. Contact your host if you are unsure.

Enabling Transcoding

  1. Confirm FFmpeg is installed on your server: run which ffmpeg via SSH and note the path
  2. Go to Media > Settings > Video
  3. Enable Transcode Uploaded Videos
  4. If your FFmpeg path differs from /usr/bin/ffmpeg, update the FFmpeg Path field
  5. Click Save Settings
  6. All new video uploads will now be queued for transcoding automatically

Video settings tab with transcode toggle and FFmpeg path

Requirements

  • FFmpeg installed on the server and accessible from PHP (verify with which ffmpeg via SSH)
  • Action Scheduler 3.0+ (bundled with WooCommerce or installable as a standalone plugin)

Enabling Transcoding

Go to Media > Settings > Video and enable Transcode Uploaded Videos. The setting is stored in the mvs_pro_transcode_enabled option.

Option Option Key Default Description
Transcode Uploaded Videos mvs_pro_transcode_enabled 0 Enable async FFmpeg transcoding on upload
FFmpeg Path mvs_pro_ffmpeg_path /usr/bin/ffmpeg Absolute path to the FFmpeg binary

Video settings tab with transcode toggle and FFmpeg path

Output Formats

Each uploaded video is transcoded to:

Resolution Bitrate Format
720p ~2500 kbps MP4 (H.264 + AAC)
480p ~1000 kbps MP4 (H.264 + AAC)
360p ~500 kbps MP4 (H.264 + AAC)

An HLS playlist (master.m3u8) is also generated, referencing all three resolutions. The player loads the HLS stream by default and falls back to direct MP4 at the highest available resolution for browsers without HLS support.

File Storage

Transcoded files are stored at:

/wp-content/uploads/mvs-transcodes/{media_id}/
  720p.mp4
  480p.mp4
  360p.mp4
  master.m3u8
  720p/
  480p/
  360p/

If cloud storage is configured, the transcoded files are uploaded to the same bucket or storage zone using the same path structure.

Post Meta

Meta Key Values Description
_mvs_transcode_status queued, processing, complete, failed Current transcoding job status
_mvs_transcodes serialized array URLs to each transcoded file and the HLS playlist

REST API

Base URL: /wp-json/mvs-pro/v1/

GET /media/{id}/transcodes

Get the transcoding status and file URLs for a media item.

Response:

{
  "status": "complete",
  "transcodes": {
    "720p": "https://example.com/wp-content/uploads/mvs-transcodes/123/720p.mp4",
    "480p": "https://example.com/wp-content/uploads/mvs-transcodes/123/480p.mp4",
    "360p": "https://example.com/wp-content/uploads/mvs-transcodes/123/360p.mp4",
    "hls": "https://example.com/wp-content/uploads/mvs-transcodes/123/master.m3u8"
  }
}

DELETE /media/{id}/transcodes

Delete all transcoded files for a media item. Requires ownership or admin (manage_mvs_settings).

Response: 200 OK.

POST /media/{id}/transcode

Trigger transcoding manually (note the singular transcode). Requires ownership or admin (manage_mvs_settings). Use this to re-transcode a file after correcting the FFmpeg path.

Response: 202 Accepted with the current status object.

There are also two admin-only diagnostic routes: GET /transcode/status (overview of pending/processing jobs) and GET /transcode/config (FFmpeg availability and configured presets). Both require manage_mvs_settings.

Transcoding Queue

Action Scheduler processes the transcoding queue in the background. You can monitor jobs at Tools > Scheduled Actions (if WooCommerce is active) or at the Action Scheduler standalone admin page. Filter by the mvs_pro_transcode_video hook to see only transcoding jobs.

Troubleshooting

If transcoding fails, check:

  1. FFmpeg is installed: run ffmpeg -version via SSH.
  2. The path in Media > Settings > Video > FFmpeg Path matches the which ffmpeg output.
  3. The PHP process has permission to invoke the binary (disable_functions in php.ini does not block it).
  4. The mvs-transcodes directory inside wp-content/uploads is writable by the web server user.

Failed jobs set _mvs_transcode_status to failed. Re-trigger via the REST API endpoint above or by editing the media item in WP Admin and clicking Re-Transcode.

Video Chapters

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro lets you add chapter markers to video files and tracks each viewer's resume position so they can pick up where they left off.

Video player showing chapter markers on the progress bar

How Chapters Work

Chapter markers appear on the video player's progress bar as clickable tick marks. A chapter list panel opens alongside the player, showing each chapter title and its timestamp. Clicking a chapter jumps the video to that position.

Chapter data is stored per media item via the REST API and rendered into the player on page load.


REST API

Base URL: /wp-json/mvs-pro/v1/

GET /media/{id}/chapters

Retrieve the chapter list for a media item.

Response:

{
  "chapters": [
    { "time": 0, "title": "Introduction" },
    { "time": 145, "title": "Main Topic" },
    { "time": 312, "title": "Conclusion" }
  ]
}

time is in seconds from the start of the video.

PUT /media/{id}/chapters

Replace the chapter list for a media item. Requires ownership or the edit_mvs_media capability.

Body:

{
  "chapters": [
    { "time": 0, "title": "Introduction" },
    { "time": 145, "title": "Main Topic" },
    { "time": 312, "title": "Conclusion" }
  ]
}

Sending an empty array ("chapters": []) removes all chapters from the media item.

Response: 200 OK with the updated chapter list.


Resume Playback

WPMediaVerse Pro tracks the furthest playback position reached by each authenticated user. When a user returns to a video they have partially watched, the player offers a Resume from X:XX prompt.

Resume positions are stored server-side, not in browser storage, so they persist across devices.

REST API

GET /media/{id}/resume

Get the resume position for the current authenticated user.

Response:

{
  "position": 187,
  "updated_at": "2025-03-28T09:14:00Z"
}

Returns 404 if no resume position exists for this user and media item.

POST /media/{id}/resume

Save or update the resume position. The player calls this endpoint as the video plays.

Body:

{ "position": 187 }

position is in seconds. The endpoint accepts updates only when the new position is greater than the stored one, preventing backwards seeks from overwriting progress.

Response: 200 OK.

DELETE /media/{id}/resume

Clear the resume position for the current user. This is called when the user watches to the end of the video or manually dismisses resume tracking.

Response: 204 No Content.


How Chapters Are Stored

Chapters are managed through the REST API (PUT /media/{id}/chapters) and stored per media item as JSON in the _mvs_chapters post meta. They are returned with the media REST response and rendered into the player on page load. Each entry is an object with a time (seconds from the start) and a title.

Auto-Captions

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro transcribes video and audio files using the OpenAI Whisper API and attaches the result as a WebVTT caption file. Captions are served with the media player and can be edited via the REST API.

Video player with captions visible and CC button

Requirements

  • An OpenAI API key with access to the Whisper transcription endpoint
  • The media file must be a supported audio or video format (MP3, MP4, M4A, WAV, WEBM, OGG)

Enabling Auto-Captions

Go to Media > Settings > Video and configure the following options.

Option Option Key Default Description
Auto-Generate Captions mvs_pro_captions_auto 0 Automatically transcribe each uploaded video or audio file
Caption Language mvs_pro_captions_language (auto) Optional source-language hint passed to the provider
Caption Provider mvs_pro_captions_provider whisper Transcription provider

The OpenAI API key is read from the free plugin's existing OpenAI API Key setting (the mvs_openai_api_key option, configured under Media > Settings > AI & Moderation). There is no separate Pro Whisper key field - captions reuse the same key as the rest of the AI features.

Auto-captions settings section with API key field

When Auto-Generate Captions is on, WPMediaVerse Pro queues a transcription job via Action Scheduler immediately after a file is stored. The caption file is saved once the Whisper API responds.

Caption File Storage

WebVTT files are stored at:

/wp-content/uploads/mvs-captions/{media_id}.vtt

If cloud storage is active, the VTT file is also uploaded alongside the media file.

REST API

Base URL: /wp-json/mvs-pro/v1/

GET /media/{id}/captions

Retrieve the caption file content and metadata for a media item.

Response:

{
  "status": "complete",
  "language": "en",
  "vtt_url": "https://example.com/wp-content/uploads/mvs-captions/123.vtt",
  "generated_at": "2025-03-28T10:00:00Z"
}

status can be none, pending, complete, or failed.

POST /media/{id}/captions/generate

Queue Whisper transcription for the media item. Requires ownership or admin (manage_mvs_settings). No body is required.

curl -X POST https://yoursite.com/wp-json/mvs-pro/v1/media/123/captions/generate \
  -H "X-WP-Nonce: NONCE"

Response: 202 Accepted with the current status object.

GET /media/{id}/captions/status

Poll the transcription job status for a media item.

Response: 200 OK with { "status": "pending" | "complete" | "failed" | "none" }.

PUT /media/{id}/captions

Replace the caption content with edited VTT text. Use this to correct transcription errors.

Body (JSON):

{
  "content": "WEBVTT\n\n00:00:00.000 --> 00:00:04.000\nHello and welcome.\n\n00:00:04.500 --> 00:00:09.000\nToday we are covering...",
  "language": "en"
}

Response: 200 OK with the updated caption metadata.

DELETE /media/{id}/captions

Remove the caption file and reset the caption status to none.

Response: 204 No Content.

WebVTT Format

WPMediaVerse Pro always stores captions in WebVTT format. Caption content you submit through the API must begin with the WEBVTT header - content that does not start with that header is rejected. There is no automatic SRT-to-VTT conversion; convert SRT to WebVTT before uploading.

Example VTT file:

WEBVTT

00:00:00.000 --> 00:00:04.000
Hello and welcome to this video.

00:00:04.500 --> 00:00:09.000
Today we are covering the main topic.

Editing Captions

To correct a transcription, replace the stored VTT through the REST API with PUT /media/{id}/captions, passing the edited WebVTT content in the content field (see the REST API section above). The content must begin with the WEBVTT header.

Watermarking

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro stamps a text or logo watermark onto uploaded images using PHP's GD library, so your brand or credit travels with every photo members share.

Watermark settings panel showing text and logo options

How it works (read this first)

The watermark is baked into the image at upload time - it is composited directly onto the stored file, so everyone who views the image sees it, on the page, in the feed, and in any download.

Because of that, three things are important to understand before you turn it on:

  • It is permanent. The watermark alters the stored image. There is no separate "clean" copy kept, and no un-watermark step. Turning the setting off later does not remove the watermark from images that were already uploaded.
  • It applies to new uploads only. Existing media is not re-stamped, and changing the text, logo, position, or opacity later only affects images uploaded after the change.
  • Images only. Video and audio are never watermarked.

If you need the original files kept pristine, do not enable watermarking - keep your own unwatermarked copies before uploading.

Requirements

  • PHP GD extension enabled (verify with phpinfo() - look for the gd section). If GD is unavailable the watermark is not applied and the failure is recorded on Media > Logs.
  • For logo watermarks: a PNG file with a transparent background is recommended.

Enabling watermarks

Go to Media > Settings > Display > Image Watermarking and configure the options below.

Option Option Key Default Description
Enable Watermark mvs_watermark_enabled 0 Stamp a watermark onto uploaded images
Apply to mvs_watermark_apply all all = every uploaded image; roles = only images uploaded by the roles selected below
Watermark uploads from mvs_watermark_roles (none) The uploader roles to watermark. Only used when Apply to is roles; with none selected, nothing is watermarked
Watermark Type mvs_watermark_type text text to overlay a text string, image to overlay a logo
Watermark Text mvs_watermark_text site title The text to overlay (used when type is text); defaults to your site name. Supports the {site} and {username} tokens
Watermark Image mvs_watermark_image_id 0 WordPress attachment ID of the logo image (used when type is image)
Position mvs_watermark_position bottom-right Where on the image to place the watermark
Opacity mvs_watermark_opacity 40 Watermark opacity, 0 (transparent) to 100 (opaque)
Text Size mvs_watermark_font_size 24 Base font size for text watermarks (px); scales with the image
Text Colour mvs_watermark_color #ffffff Hex colour for text watermarks

Watermark position selector showing position options

Who gets watermarked

Watermarking is controlled by Apply to, not by a photo's privacy level:

  • All uploads (all, the default) - every image any member uploads is watermarked.
  • Uploads from selected roles (roles) - only images uploaded by the roles you tick under Watermark uploads from are watermarked. If you choose this and tick no roles, nothing is watermarked (the settings screen warns you when that happens).

Watermark positions

Value Location
top-left Top-left corner with padding
top-right Top-right corner with padding
center Centred on the image
bottom-left Bottom-left corner with padding
bottom-right Bottom-right corner with padding (default)
tile Repeated across the whole image

Text watermarks

When Watermark Type is text, WPMediaVerse Pro renders the string in Watermark Text. Two tokens are supported:

  • {site} - your site name.
  • {username} - the uploader's public handle (their user_nicename, e.g. @jane). It never uses the WordPress login name.

The text size comes from Text Size and scales with the image so it stays proportional on any photo. When a TrueType font is available it is measured for accurate placement; otherwise the built-in GD bitmap font is used. Supply your own TTF with the mvs_watermark_font_path filter:

add_filter( 'mvs_watermark_font_path', function ( $path, $config ) {
    return '/path/to/your-font.ttf';
}, 10, 2 );

Logo watermarks

When Watermark Type is image, select an image from your WordPress Media Library using the Watermark Image field (its attachment ID is stored in mvs_watermark_image_id). WPMediaVerse Pro scales the logo to roughly 20% of the base image width before compositing. A PNG with a transparent background is recommended.

For developers

The stamp is applied by Pro's Watermarker through the mvs_watermark_stamp_file filter, fired by the free plugin's upload pipeline on each new image before its thumbnails and WebP/AVIF variants are generated - so every derived size inherits the mark from a single draw.

To disable watermarking programmatically (for example on a staging site) without changing the stored setting, filter mvs_watermark_enabled:

add_filter( 'mvs_watermark_enabled', '__return_false' );

Video Analytics

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro records play events for every video, builds per-video heatmaps, and provides a dashboard showing retention and engagement metrics.

Video analytics dashboard showing play counts and retention curve

How Event Tracking Works

The player fires events to the REST API as viewers interact with a video. Events are rate-limited to one event per second per session to prevent flooding. Anonymous viewers are tracked by session ID; authenticated users are tracked by user ID.

Tracked Events

Event Fired When
play Playback starts or resumes
pause Playback is paused
seek User jumps to a new position
complete Playback reaches the end of the video
buffer Player enters a buffering state

Events are written to the mvs_play_events database table.

Database Table: mvs_play_events

Column Type Description
id bigint Auto-increment primary key
media_id bigint The mvs_media post ID
user_id bigint WordPress user ID, or NULL for anonymous
session_id varchar(64) Unique session identifier
event_type varchar(20) Event type: play, pause, seek, complete, buffer
position_seconds float Playback position in seconds when the event fired
duration_seconds float Total video duration in seconds (nullable)
created_at datetime UTC timestamp

Data Retention

A daily WP-Cron job (mvs_pro_prune_play_events) prunes raw event rows; the default retention window is 90 days. There is no dedicated retention-days setting option.

REST API

Base URL: /wp-json/mvs-pro/v1/

POST /media/{id}/events

Record a single play event. Authentication is not required - anonymous events are accepted.

Body:

{
  "event_type": "play",
  "position": 0,
  "duration": 360,
  "session_id": "abc123xyz"
}

event_type must be one of play, pause, seek, complete, buffer. session_id should be a stable string generated client-side per browser session (e.g. a UUID stored in sessionStorage). Events are rate-limited per session inside the service.

Response: 204 No Content.

GET /media/{id}/analytics

Get the full analytics bundle for a single media item in one request: heatmap, retention curve, completion rate, average watch duration, engagement score, and drop-off points. Requires ownership or admin (manage_mvs_settings).

Response:

{
  "media_id": 123,
  "heatmap": [42, 41, 40, 39, 38, 12, 12, 11],
  "retention_curve": [100, 98, 97, 95, 60, 58],
  "completion_rate": 0.42,
  "avg_duration": 148,
  "engagement_score": 0.61,
  "drop_offs": [45, 120, 240]
}

avg_duration is in seconds. completion_rate is a decimal between 0 and 1. The retention_curve and heatmap are normalised arrays; pass an optional bucket_count query argument to control the heatmap resolution.

Admin-only aggregate routes

Two site-wide routes require the manage_mvs_settings capability:

  • GET /analytics/top - top media items by play activity.
  • GET /analytics/overview - aggregate analytics across all media.

Viewing Analytics in WP Admin

Pro adds a Video Analytics tab to the Media > Stats page (the AnalyticsDashboard, injected via the mvs_stats_tabs filter). It includes a per-media detail view with the full heatmap, retention curve, and drop-off table.

Video Analytics tab on the Stats page with retention curve

AI Providers

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro adds Google Cloud Vision, AWS Rekognition, and Claude (Anthropic) as AI analysis providers alongside the built-in OpenAI Vision option. All four providers support auto-tagging and content moderation.

AI and Moderation settings tab showing provider selector

Supported Providers

Provider Auto-Tag Content Moderation Configuration Location
OpenAI Vision (free + Pro) Yes Yes Media > Settings > AI & Moderation
Google Cloud Vision Yes Yes Media > Settings > AI & Moderation > Google Vision
AWS Rekognition Yes Yes Media > Settings > AI & Moderation > AWS Rekognition
Claude (Anthropic) Yes Yes Media > Settings > AI & Moderation > Claude

Only one provider is active at a time. Select it under Media > Settings > AI & Moderation > Provider.


Google Cloud Vision

Requirements

  • A Google Cloud project with the Cloud Vision API enabled
  • An API key or service account with roles/cloudvision.user

Settings

Option Option Key Description
Vision API Key mvs_pro_google_vision_key Google Cloud API key

Google Vision settings section with API key field

What Google Vision Returns

  • Labels: Object and scene descriptions, mapped to mvs_tag terms when Auto-Apply Tags is on
  • Safe Search: Likelihood scores for adult, violence, racy, medical, and spoof categories
  • Web Detection: Public web entities that match the image (used as supplemental tags)

AWS Rekognition

Requirements

  • AWS account with the Rekognition service enabled in your chosen region
  • IAM user or role with rekognition:DetectLabels and rekognition:DetectModerationLabels permissions

Settings

Option Option Key Description
Access Key ID mvs_pro_aws_access_key AWS IAM access key ID
Secret Access Key mvs_pro_aws_secret_key AWS IAM secret access key
Region mvs_pro_aws_region AWS region, e.g. us-east-1

AWS Rekognition settings section

What Rekognition Returns

  • Labels: Hierarchical object and scene labels with confidence scores, mapped to mvs_tag terms
  • Moderation Labels: Content categories (nudity, violence, drugs, etc.) with confidence scores used by the moderation threshold settings

Claude (Anthropic) (New in 1.8.0)

Requirements

  • An Anthropic account and API key with billing enabled

Settings

Option Option Key Description
Anthropic API Key mvs_pro_anthropic_key Your Anthropic API key
Claude Model mvs_pro_anthropic_model Vision-capable model used for analysis, tagging, and moderation. Choices: Claude Haiku 4.5 (fast & low cost, recommended, default), Claude Sonnet 4.6 (stronger judgment), Claude Opus 4.8 (most capable)

Claude settings section with API key and model fields

What Claude Returns

  • Description + tags: same natural-language description and comma-separated tag suggestions as the OpenAI path, mapped to mvs_tag terms when Auto-Apply Tags is on
  • Moderation: flags content against the site's configured AI Flag Criteria and Custom Flag Terms - the same category checkboxes and custom-terms field the free plugin's OpenAI path uses

Circuit Breaker

WPMediaVerse Pro implements a circuit breaker for all AI providers. If a provider records 5 consecutive failures (network timeout, invalid API key, rate limit), the circuit opens and no further API calls are made to that provider for a cooldown period of 1 hour. This prevents quota exhaustion during outages.

While the circuit is open, calls to that provider are skipped. The failure counter is stored in a transient that expires after the cooldown, and a successful call resets the counter immediately. There is no separate half-open/retry phase or manual reset command.


Auto-Tagging Behaviour

All providers map their returned labels to mvs_tag taxonomy terms. Terms are created if they do not exist. Auto-tagging and the confidence/threshold controls that govern it are configured through the free plugin's AI settings (Auto-Apply Tags and the moderation thresholds under Media > Settings > AI & Moderation) - the Pro providers feed into that same pipeline rather than adding their own tag-confidence option keys.


Content Moderation Thresholds

Each safety category has an independent threshold. When a category's confidence score meets or exceeds the threshold, the moderation action configured in When AI Flags Content is applied.

Configure thresholds at Media > Settings > AI & Moderation > Moderation Thresholds.

Moderation threshold sliders for content categories

Quotas

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Limit free users to 50 photos, give premium members unlimited uploads - quota packages let you build a tiered media community where upgrades have real value.

What Users See

When a user goes to upload media, a usage widget appears at the top of the upload page. It shows a progress bar for each limit in their active package: how many images they have used out of their allowed total, the same for video and audio, and overall storage used versus their storage cap. When a limit is reached, the upload form shows a friendly message explaining which limit was hit and how to upgrade.

Upload page with quota usage widget

Use Cases

  • Free tier: 50 photos, 5 videos, 100 MB storage. Upgrade to Pro for unlimited.
  • Club membership: 500 photos, 50 videos, 5 GB storage for paying members.
  • Content creator plan: Unlimited photos and videos, 50 GB storage.

Setting Up Quotas (for Site Owners)

  1. Go to Media > Quota Packages and click Add New Package
  2. Enter a name (e.g., "Free", "Pro Member", "Content Creator")
  3. Set the limits for image count, video count, audio count, and total storage. Use 0 for unlimited on any dimension
  4. Click Save
  5. Go to the Assignments tab to map the package to a membership level or user role
  6. Set a Default Package at Media > Quota Packages > Settings - this applies to users with no membership

Repeat to create as many packages as your membership tiers require.

Add New Package screen with count and storage limits

Membership Integrations

WPMediaVerse Pro detects active membership plugins and maps their levels to quota packages automatically.

Plugin How to Map
MemberPress Media > Quota Packages > Assignments > MemberPress tab
Paid Memberships Pro Media > Quota Packages > Assignments > PMPro tab
WooCommerce Memberships Media > Quota Packages > Assignments > WooCommerce tab

When a user holds multiple memberships, WPMediaVerse Pro applies the highest limits across each dimension independently.


How Quotas Work (Technical)

When a user uploads a file, WPMediaVerse Pro checks the user's active quota package before accepting the upload. If the upload would exceed any limit in the package, the upload is rejected.

Users with the manage_mvs_settings capability bypass quotas entirely. Quotas are a soft tool: a user with no assigned package is treated as unlimited.

Database Tables

mvs_quota_packages

Stores the quota package definitions. A limit of 0 means unlimited for that dimension.

Column Type Description
id bigint Primary key
name varchar(200) Human-readable package name
image_limit int unsigned Maximum image files allowed, 0 for unlimited
video_limit int unsigned Maximum video files allowed, 0 for unlimited
audio_limit int unsigned Maximum audio files allowed, 0 for unlimited
storage_bytes bigint unsigned Total storage cap across all file types, 0 for unlimited
is_default tinyint Whether this is the default package for users with no membership
sort_order int unsigned Display ordering
created_at datetime UTC creation timestamp

mvs_credit_log

Logs every quota credit transaction for auditing.

Column Type Description
id bigint Primary key
user_id bigint WordPress user ID
credit_type varchar(20) image, video, or audio
amount int Change applied (negative = deduct, positive = restore)
balance_after int Resulting balance for that credit type
source varchar(50) Origin of the change, e.g. upload
reference varchar(200) Optional reference string
note text Free-form note
created_at datetime UTC timestamp

REST API

Base URL: /wp-json/mvs-pro/v1/

GET /me/quota

Get the current quota usage and limits for the logged-in user. Requires the user to be logged in.

GET /users/{user_id}/quota

Get the quota usage and limits for a specific user. Requires admin (manage_mvs_settings).

Response:

{
  "user_id": 5,
  "package": "Pro Member",
  "usage": {
    "image_count": 48,
    "video_count": 3,
    "audio_count": 1,
    "storage_bytes": 524288000
  },
  "limits": {
    "image_limit": 500,
    "video_limit": 50,
    "audio_limit": 50,
    "storage_bytes": 5368709120
  }
}

Storage values are raw byte values. Divide by 1073741824 to display in GB.

GET /packages, POST /packages, PUT /packages/{id}, DELETE /packages/{id}

List, create, update, and delete quota packages. All require admin (manage_mvs_settings).

POST /users/{user_id}/package

Assign a quota package to a user. Requires admin (manage_mvs_settings).

POST /users/{user_id}/credits

Manually adjust a user's credits (for migrations or corrections). Requires admin (manage_mvs_settings).

GET /me/credits/history

Return the logged-in user's own credit transaction history.

Frontend Usage Widget

A usage summary widget appears on the upload page (via the mvs_before_upload_form action) and the user's media dashboard (via the mvs_dashboard_after_content action). It shows a progress bar for each limit in the user's active package.

Upload page with quota usage widget

The widget is rendered by the UsageWidget service. To remove it from the upload page, detach its action callback from mvs_before_upload_form in your own code.

Advanced Privacy

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro extends the free plugin's 6 privacy levels with album-level inheritance, per-user presets, and bulk privacy updates.

Privacy selector on the upload page showing all six levels

Privacy Levels

The six levels available in both free and Pro versions:

Level Value Who Can View
Public public Everyone including logged-out visitors
Members Only members Any logged-in WordPress user
Friends friends BuddyPress friends of the owner (requires BuddyPress)
Group group Members of a specific BuddyPress group (requires BuddyPress)
Private private Only the owner and users with moderate_mvs_media
Custom custom A defined list of user IDs via access grants

Pro adds multi-level inheritance, presets, and bulk management on top of these levels.


Album-Level Inheritance

Media added to a non-public album inherits the album's privacy. This is a Free feature as of 2.3.0 - see the Albums documentation.

The privacy of an item is clamped to the more restrictive of (item, album) at the moment it is added to the album. The clamp only tightens; it never loosens an item that is already more restrictive.

Site owners can opt out with the mvs_album_inherit_privacy filter:

add_filter( 'mvs_album_inherit_privacy', '__return_false' );
```json
{
  "privacy": "group",
  "group_id": 42
}

group_id is required when privacy is group.

Response: 200 OK with the updated privacy object.

POST /media/bulk-privacy

Update privacy for multiple media items in one request. Requires the user to be logged in; items the user does not own are skipped. Accepts up to 100 media IDs per request.

Body:

{
  "media_ids": [101, 102, 103],
  "privacy": "private"
}

Items the authenticated user does not own are skipped. The response lists which IDs were updated and which were skipped.

Response:

{
  "updated": [101, 102],
  "skipped": [103]
}

GET /privacy/presets

List the current user's saved privacy presets.

Response:

{
  "presets": [
    {
      "id": 1,
      "name": "Close friends only",
      "privacy": "friends",
      "is_default": true
    }
  ]
}

POST /privacy/presets

Save a new privacy preset for the current user.

Body:

{
  "name": "Close friends only",
  "privacy": "friends",
  "is_default": true
}

Setting is_default to true makes this preset the pre-selected option on the upload form. Only one preset can be the default; saving a new default clears the flag from the previous one.

Response: 201 Created with the new preset object.


Bulk Privacy Updates

Site administrators can update privacy in bulk from Media > All Media:

  1. Select media items using the checkboxes.
  2. Open the Bulk Actions dropdown.
  3. Select Change Privacy.
  4. Choose the target privacy level and click Apply.

Media list table with bulk actions dropdown

This uses the same POST /media/bulk-privacy endpoint internally and respects the same ownership rules.


User Privacy Presets

Users can save their preferred privacy level as a named preset from the upload page. The preset they mark as default is automatically selected each time they open the upload form.

Presets are stored as user meta. They are personal and not visible to other users or administrators.

Upload form showing preset selector dropdown


Developer Filter

Use mvs_privacy_can_view (available in the free plugin) to extend access logic. Pro privacy checks run through the same filter:

add_filter( 'mvs_privacy_can_view', function( $result, $media_id, $user_id, $privacy ) {
    // Grant access to users with a custom capability.
    if ( null === $result && user_can( $user_id, 'mvs_vip_access' ) ) {
        return true;
    }
    return $result;
}, 10, 4 );

Return null to let built-in logic run. Return true or false to override it.

Connected Accounts

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Connect an external photo platform to WPMediaVerse and move media in both directions - import existing photos and albums into your media library, and auto-push new uploads back out to that platform. In 1.5.0 the only built-in connector is Flickr, with full import, export, album browsing, and metadata sync support.

The connector framework is pluggable, so additional platforms can be added by developers in the future. This page covers the Flickr connector as it ships today.

What the Flickr Connector Does

Once a member connects their Flickr account, they can:

  • Import photos and videos from their Flickr photostream or from a specific album (set) into WPMediaVerse.
  • Export (auto-push) - automatically copy every new WPMediaVerse upload to their Flickr account.
  • Browse albums - pick a Flickr album to import from or push exports into.
  • Sync metadata - pull the latest title, description, tags, and privacy from Flickr back into the local media record (delta sync).

Privacy is mapped both ways. A Flickr "public/friends/family/private" visibility maps to the matching WPMediaVerse privacy level on import, and the member can choose how WPMediaVerse privacy maps back to Flickr on export.


Two Levels of Setup

There are two separate roles in this feature, and it helps to keep them apart:

  1. Site owner (admin) - enables the feature and optionally provides a plugin-level Flickr app key/secret so members can connect with one click. This is done once, in MediaVerse > Settings.
  2. Member (user) - connects their own Flickr account through an OAuth flow on the same settings page, then imports/exports their photos. Each user authorizes their own account; the connection (access token) is stored per user.

Step 1 - Enable Connected Accounts (Admin)

  1. In WordPress admin, go to MediaVerse > Settings.
  2. In the settings sidebar, open the Connected Accounts tab (under the Advanced group).
  3. Check Enable Platform Connectors feature.
  4. Click Save Changes.

When the feature is disabled, no connectors load and the connector REST endpoints are not registered.


Providing a built-in Flickr API key/secret lets your members connect with a single Connect with Flickr button - they never need their own developer key. This is the smoothest experience for most sites.

Create a Flickr app

  1. Sign in at Flickr and go to flickr.com/services/apps/create.
  2. Choose Apply for a non-commercial key (or commercial, depending on your use).
  3. Give the app a name and description, agree to the API terms, and submit.
  4. Flickr shows you a Key and a Secret - copy both.

Enter the credentials in WPMediaVerse

  1. Back in MediaVerse > Settings > Connected Accounts.
  2. Paste the key into Flickr Plugin API Key.
  3. Paste the secret into Flickr Plugin API Secret.
  4. Click Save Changes.

The secret field is masked. When you re-save the settings page without re-typing it, the stored secret is preserved - an empty submit will not wipe it.

If you leave these two fields empty, the one-click Connect with Flickr button is hidden and members must supply their own Flickr key instead (see "Members using their own key" below).


Step 3 - Connect a Flickr Account (Member)

The connect flow runs on the same Connected Accounts settings tab. Each registered platform shows as a card.

With the plugin-level key (one click)

  1. On the Flickr card, click Connect with Flickr.
  2. You are redirected to Flickr's authorization page. Sign in if needed and click OK, I'll authorize it.
  3. Flickr redirects back to WPMediaVerse. The card now shows Connected as @yourusername.

Members using their own key

If your site has no plugin-level key - or a member wants a dedicated rate limit - they can use their own Flickr app:

  1. On the Flickr card, click Use your own API key (recommended for heavy usage).
  2. Create a Flickr app at flickr.com/services/apps (same steps as above) and copy the Key and Secret.
  3. Enter the Flickr API Key and Flickr API Secret in the expanded fields.
  4. Click Connect with My Key and complete the Flickr authorization.

A member using their own key gets the note "Using: Your own key (dedicated rate limit)" on the connected card; a member on the plugin key sees "Using: Plugin key (shared)".

How the connection is stored

The OAuth authorization returns an access token that is encrypted before being saved to that user's profile. Flickr does not expose a token-revocation endpoint, so disconnecting clears the locally stored token and validation cache. The actual API key/secret never reaches the browser.


Step 4 - Import Photos and Albums

After connecting, the member can browse and import their Flickr media.

  1. On the connected Flickr card, open the import dialog.
  2. Choose to browse the whole photostream or a single album (set).
  3. Select the photos to import.
  4. Confirm the import.

For each photo, WPMediaVerse:

  • Downloads the best available image size from Flickr.
  • Creates a media item in your library through the standard upload pipeline (so quotas, watermarking, and AI features all apply).
  • Copies the title, description, tags, and Flickr privacy (mapped to WPMediaVerse privacy).
  • Records the Flickr photo ID so the same photo is never imported twice - re-importing an already-imported photo is reported as "skipped".

The import result reports per-item outcomes: imported, skipped (already present), or failed (with an error message).


Step 5 - Export and Auto-Push New Uploads

Exporting copies a WPMediaVerse media item up to the member's Flickr account.

Auto-push

On a connected Flickr card, the member can toggle Auto-push new uploads > Enable. With this on, every new WPMediaVerse upload by that member is automatically exported to Flickr. When Action Scheduler is available the export runs in the background; otherwise it runs inline at upload time.

Default privacy on Flickr

The card has a Default privacy on Flickr selector that controls how exported media is made visible on Flickr:

Option Effect on Flickr
Match WPMediaVerse Use the media item's own privacy, mapped to Flickr
Public Always public on Flickr
Friends Visible to Flickr friends
Friends + Family Visible to Flickr friends and family
Private Private on Flickr

If a member set a default album, exported photos are also added to that Flickr album. Like import, export deduplicates - a media item already exported to Flickr is reported as "skipped" rather than uploaded again.


Step 6 - Sync Metadata (Delta Sync)

The Flickr connector supports delta sync: pulling the latest title, description, tags, and privacy from Flickr back into the matching local media records. This is useful when a member edits photo metadata on Flickr and wants WPMediaVerse to reflect those edits. Sync only touches media that originated from (or was exported to) Flickr for that member, and records a "last synced" timestamp.


Test the Connection

A connected card has a Test Connection button. It runs a live check against Flickr's identity endpoint (flickr.test.login) and confirms the stored token still works. The result is cached for 15 minutes to avoid hammering the API. If the token has been revoked on Flickr's side, the test reports the failure and the member can reconnect.


Disconnecting

Click Disconnect on a connected card to remove the local connection. This clears the encrypted tokens, the cached account identity, the auto-push and default-privacy preferences, and the validation cache. Because Flickr has no remote revoke endpoint, you may also want to revoke WPMediaVerse from your Flickr account's connected apps page if you want Flickr's side cleared too.


Settings

These options live on MediaVerse > Settings > Connected Accounts and are stored in wp_options.

Setting Option key Default Description
Enable Platform Connectors feature mvs_connectors_enabled 0 (off) Master switch for the whole Connected Accounts feature. When off, no connectors load and the REST endpoints are not registered.
Flickr Plugin API Key mvs_pro_connector_flickr_app_key (empty) Site-wide Flickr app key for one-click member connection. Leave empty to require each member to supply their own.
Flickr Plugin API Secret mvs_pro_connector_flickr_app_secret (empty) Site-wide Flickr app secret. Masked field - an empty re-save keeps the existing value.

Per-member preferences (auto-push toggle, default privacy, the member's own key, and the OAuth tokens) are stored in user meta, not site options, so each member's Flickr connection is independent.


Troubleshooting

The "Connect with Flickr" button is missing. The plugin-level Flickr key/secret are empty. Either add them under Connected Accounts (Step 2) or have the member use Use your own API key instead.

No connector cards appear at all. The feature is off. Enable Platform Connectors feature and save (Step 1).

"Flickr OAuth token mismatch or session expired." The connect flow took longer than the 5-minute request-token window, or it was started in one browser tab and finished in another. Start the connect again from the Flickr card.

Imported photos look low-resolution. WPMediaVerse imports the best size Flickr exposes for that photo. Photos uploaded to Flickr at small sizes, or with download restrictions, may not offer a high-resolution original.

Test Connection still shows a stale result after reconnecting. The validation result is cached for 15 minutes. Disconnecting clears that cache; after a fresh connect the next Test Connection reflects the new token.

Auto-push isn't sending new uploads. Confirm the member has Auto-push new uploads enabled on the connected card, and that the media is owned by that connected member. Background exports rely on WordPress cron / Action Scheduler running on your site.


Developer Notes

The connector framework is pluggable. Each connector implements WPMediaVersePro\Connectors\ConnectorInterface and registers itself on the mvs_connectors filter, keyed by a slug. The REST surface lives under mvs-pro/v1/connectors:

Route Method Purpose
/connectors GET List connectors and per-user connection state
/connectors/{id}/connect POST Start the OAuth flow (returns a redirect URL)
/connectors/{id}/disconnect POST Remove the stored connection
/connectors/{id}/status GET Live connection validation
/connectors/{id}/photos GET Browse remote photos (paginated)
/connectors/{id}/albums GET Browse remote albums
/connectors/{id}/import POST Import remote photos by ID
/connectors/{id}/export POST Export local media by ID
/connectors/{id}/sync POST Delta-sync metadata for linked media

Read and initiate routes require the user to be logged in; browse, import, export, and sync routes require the user to be connected to that connector.

Stories

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro adds WhatsApp-style ephemeral stories: mark any upload as a story and it is visible to your followers for a limited time, then disappears automatically.

How Stories Work

  1. Enable the feature at MediaVerse > Settings > Gamification, "Stories" checkbox (sets mvs_stories_enabled, off by default). Once enabled, the free upload block shows an "Also share as a story" checkbox next to the tag input, and the mvs/pro-stories block renders its bar to visitors.
  2. On upload (or afterward via REST), a media item is marked as a story with an expiry - 24 hours by default, configurable from 1 to 168 hours.
  3. Viewers see active stories from people they follow (plus their own) in a horizontal, tap-to-advance carousel with a segmented progress bar per story.
  4. A view receipt is recorded the first time a viewer opens a story. The author can see who has viewed it ("seen by").
  5. Stories are pruned automatically by an hourly cron; the author can also end one early, and a site owner can force-expire any story from the admin.

Replying to a story reuses the existing free direct-message routes - there is no separate "reply to story" endpoint.

Gutenberg Block

mvs/pro-stories renders the stories bar and the fullscreen viewer, built entirely on the WordPress Interactivity API. Logged-in visitors also see a "Your story" add tile at the start of the bar - picking an image uploads it and posts it as a story in place, no separate upload flow needed.

Block Settings:

Attribute Default Description
count 30 Number of authors' stories to load in the bar
avatarSize 64 (px) Avatar circle diameter

The block renders nothing when mvs_stories_enabled is off, and shows only the "Your story" add tile (no bar) for a logged-in viewer whose network has no active stories yet. Anonymous visitors see nothing when there are no active stories.

Admin

MediaVerse > Stories lists every active story site-wide (author, media, expiry, view count) with a Force expire row action for moderation. This is the backend leg of the frontend (upload toggle + block) / REST feature - a site owner never needs direct DB access to pull down a story.

Where Story State Lives

Story flags are stored as free plugin media meta, not a separate Pro copy of the media:

Key Description
is_story 1 while the story is active
story_started_at UTC datetime the story was created
story_expires_at UTC datetime the story disappears

View receipts live in the Pro table mvs_pro_story_views, scoped to story_started_at so a re-shared story doesn't inherit an earlier run's viewers.

REST API

Base URL: /wp-json/mvs-pro/v1/

GET /stories

List active stories - defaults to the viewer's network (people they follow, plus themselves), or pass author_id to view one person's stories.

Auth: Public (an empty result for logged-out visitors, since the default scope needs a viewer)

Parameter Type Default Description
author_id int (viewer's network) Scope to one author
page int 1 Page number
per_page int 50 Items per page (max 100)

Response:

[
  {
    "media_id": 42,
    "media_type": "image",
    "thumbnail_url": "https://example.com/wp-content/uploads/wpmediaverse/2026/07/42-medium.jpg",
    "expires_at": "2026-07-10T09:00:00Z",
    "viewed": false,
    "author": {
      "id": 7,
      "name": "Jamie Rivera",
      "avatar": "https://example.com/avatar.jpg",
      "profile_url": "https://example.com/media/@jamie/"
    }
  }
]

POST /media/{id}/story

Mark a media item as a story.

Auth: Owner/Admin

Field Type Default Description
duration_hours int 24 Visibility window, 1-168 hours

Response: { "media_id": 42, "is_story": true, "expires_at": "2026-07-10T09:00:00Z" }

DELETE /media/{id}/story

End a story early. The media item itself is untouched - only the story designation is cleared.

Auth: Owner/Admin

Response: { "media_id": 42, "is_story": false }

POST /stories/{id}/view

Record a view receipt for the current user. The author's own views are never recorded - "seen by" counts the audience, not the owner.

Auth: User, subject to the media's normal privacy check

Response: { "recorded": true }

GET /stories/{id}/viewers

"Seen by" list for a story.

Auth: Owner/Admin

Parameter Type Default Description
page int 1 Page number
per_page int 50 Items per page (max 100)

Response: { "viewers": [ { "user_id": 3, "name": "Alex", "avatar": "...", "profile_url": "...", "viewed_at": "2026-07-09T18:02:00Z" } ], "total": 12 }

Hooks

Hook Type Description Parameters
mvs_story_created action Fires after a story is created $media_id, $user_id, $expires_at
mvs_story_expired action Fires when a story ends (cron cleanup or manual end) $media_id

Full reference: Hooks & Filters.

History

Stories originated as a free-plugin primitive but were never surfaced with a create-flow or viewer. In 1.9.0, StoryService was relocated to Pro and shipped as a complete feature: the upload toggle, viewer carousel, REST API, and view receipts. Free retains only the toggle UI, which stays hidden until Pro turns on mvs_stories_enabled.

Save to Collections (Pro Picker)

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro gives members a dedicated Save control for adding a media item to any number of their named collections, separate from the favorite (heart) button. Favoriting stays a one-tap like; saving to a collection is a deliberate, multi-select action with its own picker.

Media item with Save button and collection picker

Save vs. Favorite

  • Favorite (heart) - a one-tap like. No picker, no confirmation, and it is not a collection membership.
  • Save (bookmark) - opens the collection picker. It appears next to the heart wherever media actions render (single media page, lightbox, feed cards) once Pro is active, and only for logged-in users.

How It Works (for Users)

  1. Click Save (bookmark icon) on a media item.
  2. The picker lists your named collections with a checkbox per row - check to add the item, uncheck to remove it.
  3. Each toggle saves automatically the moment you click it; there is no separate confirm button.
  4. Every row shows its own state inline: Saving..., then Saved or Removed. If a save fails, the row shows an inline error with a retry option.
  5. Don't have the right collection yet? Type a name in + New collection and click Create - the name is validated (blank names are rejected).
  6. Click View your collections at the bottom of the picker to jump straight to the My Media dashboard Collections tab.

Named Collections Only

The picker only lists your manual collections - the ones where you hand-pick items. Smart collections are rule-based and resolve their contents dynamically, so they are not toggle targets in the picker; they still work exactly as documented in the free Collections page.

Accessibility

The picker is fully keyboard and screen-reader accessible:

  • The popover carries aria-labelledby pointing at its title.
  • Escape closes the picker and returns focus to the Save button that opened it - keyboard users are never stranded.
  • Per-row status text (Saving/Saved/Removed) is announced via aria-live="polite".
  • Loading, empty ("No collections yet, create one below"), and error states are all handled explicitly; errors carry role="alert".

Where "View Your Collections" Goes

By default, View your collections links to the My Media dashboard's Collections tab. If your site's dashboard lives at a different URL, filter mvs_pro_collections_manage_url - documented in the Free plugin's Hooks & Filters reference - to point it wherever your site keeps that page.

REST API

Base URL: /wp-json/mvs-pro/v1/

GET /media/{media_id}/collections

Return the current user's favorite flag plus their manual collections, each with a member boolean showing whether this media item is already in it.

Auth: User (logged in)

Response:

{
  "favorites": true,
  "collections": [
    { "id": 12, "title": "Travel Inspiration", "member": true },
    { "id": 18, "title": "Black and White", "member": false }
  ]
}

POST /media/{media_id}/collections

Add or remove the media item from one of the user's own manual collections.

Auth: User (logged in; the collection must belong to the requesting user)

Body:

Field Type Required Default Description
collection_id int Yes - The collection to toggle membership in
member bool No true true to add the item, false to remove it

Returns 404 (mvs_pro_invalid_collection) if the collection does not exist or is not owned by the requesting user.

Requirements

  • WPMediaVerse Pro 1.8.0 or higher.

Mobile App (White-Label & Push)

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Pro powers a native mobile app for your community. Pro supplies the app's branding, feature flags, feed layout, push notifications, and leaderboard through the REST API - the same data your site already manages, delivered in the shape a native client needs.

Mobile App Branding section on the Display settings tab

White-Label Branding

Brand the app for your own community: your logo, your accent color, your login screen. Every member who connects the app to your site sees your brand, not a generic MediaVerse skin.

Go to Media > Settings > Display > Mobile App Branding.

Option Option Key Default Description
Accent Color mvs_app_accent_color #7C3AED Primary brand color for buttons, links, and active states in the app
App Logo mvs_app_logo_id (none) Media Library attachment shown on the app login screen. PNG with transparency recommended. Falls back to the site name as text when empty
Login Background mvs_app_login_bg_id (none) Optional full-bleed image behind the app login screen. Plain background when empty
Default to Dark Mode mvs_app_dark_mode_default Off New installs open in dark mode; members can still switch

Feed Layout

The app mirrors the feed layout you picked for the web Explore page - grid, Instagram, Pinterest, Flickr, or Dribbble - so the app matches the browsing experience you configured. This is the same mvs_pro_feed_layout setting on Media > Settings > Display > Feed Layout described in Layout Modes; there is no separate app-only layout setting.

Feature Flags

The app also learns which competitive and video features are switched on for your site - battles, challenges, tournaments, boosts, streaks, transcoded video, and stories - so it only mounts the screens you've actually enabled. Each flag mirrors the matching admin toggle (e.g. mvs_challenges_enabled); there is nothing extra to configure for the app itself.

How the App Gets This (Developers)

A native or headless client calls GET /wp-json/mvs/v1/app/config once, before theming itself and deciding which screens to mount. This is a Free-plugin route (public, no authentication) that Pro contributes branding, layout, and feature flags into via filters. The filters themselves (mvs_app_config_branding, mvs_app_config_features, mvs_app_config_layout) are documented in the Free plugin's Hooks & Filters reference - this page covers the feature, not the filter contract.

Full request/response details for /app/config plus the Pro-only push and leaderboard routes below are in the Pro REST API Reference.

Push Notifications

Once a member is logged in, the app registers its Expo push token with your site so it can push notifications for events like new messages, challenge results, and battle/tournament outcomes.

  • POST /mvs-pro/v1/push/register-device - register (or refresh) a device token for the current user.
  • DELETE /mvs-pro/v1/push/register-device - remove a device token (e.g. on logout).

Both require a logged-in user. See the Pro REST API Reference for the full parameter list.

Leaderboard

GET /mvs-pro/v1/leaderboard powers the app's gamification screen. It's backed by the same LeaderboardService as the pro-leaderboard block, so the ranking the app shows is identical to the one on your site - ranked by reactions, upload count, or gamification XP, over all time, the last 30 days, or the last 7 days. The response includes the ranked page plus the current viewer's own rank in one round trip, so the app doesn't need a second request to show "your rank."

Requirements

  • WPMediaVerse Pro 1.9.0 or higher.
  • A native or headless client (e.g. the official MediaVerse app) that calls /app/config, the push routes, and the leaderboard route.

Gamification

Photo challenges, battles, tournaments, points, and streaks.

Gamification Overview

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

WPMediaVerse Gamification turns your media community into a competitive platform. Users earn XP, enter photo competitions, challenge each other to battles, and boost their media visibility - all integrated with the wb-gamification plugin's XP and reward engine.

Gamification requires WPMediaVerse Pro and the wb-gamification plugin (active and configured). All gamification features are disabled by default. Enable each feature individually at WPMediaVerse > Settings > Gamification.

Gamification overview in WPMediaVerse admin settings

Requirements

  • WPMediaVerse Pro 1.0.0+
  • wb-gamification plugin (active)
  • WordPress 6.5+
  • PHP 7.4+

Feature Types

Feature Description Setting Key
Photo Challenges Themed weekly competitions with voting mvs_challenges_enabled
Photo Battles 1v1 head-to-head media matchups mvs_battles_enabled
Tournaments Single-elimination bracket competitions mvs_tournaments_enabled
Media Boosts Spend points to increase media visibility mvs_boosts_enabled
Upload Streaks Track consecutive daily uploads mvs_streaks_enabled

Competition Types

All three competition types share a unified database schema. Each type follows its own lifecycle, but they share the same tables.

Challenges

Themed weekly competitions. Admin creates a challenge with a theme, entry window, and voting window. Users submit a photo, community votes, winners earn XP prizes.

Battles

1v1 matchups. A challenger picks a photo and invites an opponent. Both submit photos, the community votes, the system resolves the winner.

Tournaments

Single-elimination brackets for 4 to 64 participants. Users register, the system seeds the bracket, rounds proceed by community vote until a winner is determined.

Database Schema

Table Purpose
mvs_competitions Master record for all challenges, battles, and tournaments
mvs_competition_entries User photo submissions for any competition type
mvs_competition_matches Individual head-to-head pairings (battles and tournament rounds)
mvs_competition_votes Per-user votes on entries or matches
mvs_boosts Active and expired media boost records

The mvs_competitions.type column distinguishes between challenge, battle, and tournament records.

XP Integration

XP and points are not awarded by WPMediaVerse itself - the separate free wb-gamification plugin is the points engine. WPMediaVerse ships an integration manifest that registers 15 gamification actions (photo upload, album creation, likes/comments/follows/favorites received and given, bookmarks, battle win, challenge participation, challenge placing, tournament round win, tournament win, and streak milestones).

wb-gamification manifest showing WPMediaVerse actions and XP values

As members interact with the site and competitions, WPMediaVerse fires the matching action hooks - for example mvs_media_uploaded, mvs_battle_resolved, mvs_challenge_winner_named, mvs_tournament_finalized, and mvs_streak_milestone - and wb-gamification awards the points configured for each action. The per-competition XP prizes you set on a challenge or tournament are honored through the wb_gam_points_for_action filter, so the points a member earns match the prize shown in the UI.

Without the wb-gamification plugin active, competitions still run end to end - members enter, vote, and win - but no points are earned or spent. Install and activate wb-gamification to enable the XP and rewards layer.

Frontend Pages

URL Description
/media/challenges/ Browse and enter photo challenges
/media/battles/ View active and completed battles
/media/tournaments/ Browse tournaments and view brackets
/compete/ Unified competition hub showing all active competitions

The My Media dashboard adds Challenges, Battles, and Tournaments tabs showing the logged-in user's entries, results, and active matches.

My Media dashboard with competition tabs

Scheduled Actions

All competition lifecycle transitions run via Action Scheduler on an hourly recurrence. No competition state changes happen in real time - transitions occur at the next hourly tick after a deadline passes.

Scheduled Action Trigger Condition
mvs_activate_scheduled_challenges Challenge start date reached
mvs_close_challenge_entries Entry deadline reached
mvs_finalize_expired_challenges Voting deadline reached
mvs_resolve_expired_battles Battle vote deadline reached
mvs_start_registered_tournaments Tournament registration deadline reached
mvs_resolve_expired_matches Match vote deadline reached
mvs_expire_boosts Boost impression target or duration reached
mvs_daily_streak_check Daily at 2 AM - break streaks for missed uploads

Action Scheduler must be running for gamification to function. If you use a managed host that blocks WP-Cron, configure Action Scheduler with a server-level cron trigger.

Photo Challenges

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Run weekly themed photo competitions - your community submits their best shots, votes for their favorites, and the top three photographers win XP prizes.

What You Can Do (as a User)

  • Browse active challenges and see the current theme
  • Enter your best photo for any active challenge
  • Vote for your favorite entries during the voting window
  • Track your standing and see the final rankings with winner badges
  • Earn XP for participating, even if you do not place in the top three

How It Works (for Users)

  1. Go to Media > Challenges on your site
  2. Click the Active tab to see the current challenge theme (e.g., "Golden Hour Photography")
  3. Click Enter Challenge - a media picker opens showing your uploaded photos
  4. Select one of your photos or upload a new one, add an optional caption, and click Submit
  5. Your entry appears in the challenge gallery alongside other participants
  6. When the entry window closes, the challenge moves to the Voting tab
  7. Vote for your single favorite entry - you can only vote once per challenge
  8. When voting closes, open the Finalized tab to see the ranked results
  9. Winner badges (1st, 2nd, 3rd) appear on the top entries and XP is awarded automatically to your account

Photo Challenges frontend page showing active challenge

For Site Owners

  1. Go to WPMediaVerse > Settings > Gamification and enable Photo Challenges
  2. Go to Competitions > Challenge Manager and click Add Challenge
  3. Set a theme title, entry start date, entry end date, and voting end date
  4. Set XP prizes for 1st, 2nd, 3rd place and a participation XP amount for all entrants
  5. Click Save - the challenge appears on the frontend when the start date arrives
  6. To run challenges on autopilot without manual creation, enable Autopilot in the settings (see below)

Challenge Manager create form

Lifecycle

A challenge moves through four stages. All transitions are handled by Action Scheduler on an hourly schedule.

Stage Description
Scheduled Challenge is published but the start date has not arrived
Active Entry window is open - users can submit photos
Voting Entry window closed - community can vote on submissions
Finalized Voting closed - winners determined, XP awarded

Creating a Challenge

Go to Competitions > Challenge Manager and click Add Challenge.

Challenge Manager create form

Field Description
Theme Title or topic for the challenge (e.g., "Golden Hour Photography")
Theme Library Pick from 25+ pre-built themes instead of writing one manually
Entry Start Date and time when photo submissions open
Entry End Date and time when submissions close
Voting End Date and time when voting closes
Max Entries Per User How many photos one user may submit
XP - 1st Place XP awarded to the top vote-getter
XP - 2nd Place XP awarded to the second-highest vote-getter
XP - 3rd Place XP awarded to the third-highest vote-getter
XP - Participation XP awarded to all entrants when the challenge finalizes

Autopilot

Autopilot creates and schedules challenges automatically so you do not need to create them manually each week.

Setting Key Description
Enable Autopilot mvs_autopilot_enabled Automatically create a new challenge when the current one enters Voting stage
Autopilot Day mvs_autopilot_day Day of the week to start each new challenge - a lowercase day name (monday through sunday)
Autopilot Hour mvs_autopilot_hour Hour of day (0-23, site timezone) to open entries

When autopilot runs, it picks the next unused theme from the Theme Library. Once all themes are used, it cycles back to the first theme.

Theme Library

The Theme Library ships with 25+ pre-built challenge themes. Go to Competitions > Theme Library to browse, add, edit, or disable themes.

Theme Library grid showing theme cards with categories

Themes are categorized (Nature, Urban, Portrait, Abstract, etc.). You can add custom themes and assign them to any category.

Settings Reference

Setting Key Default
Enable Photo Challenges mvs_challenges_enabled Off
Enable Autopilot mvs_autopilot_enabled Off
Autopilot Day mvs_autopilot_day monday
Autopilot Hour mvs_autopilot_hour 9

REST API

Base URL: /wp-json/mvs-pro/v1/challenges

Method Endpoint Description
POST /challenges Create a new challenge. Requires the manage_mvs_settings capability.
GET /challenges/{id} Get challenge details including stage, entry count, and dates
POST /challenges/{id}/enter Submit a photo entry. Requires authentication.
GET /challenges/{id}/entries List submitted entries with vote counts
POST /challenges/{id}/vote Cast a vote for an entry. One vote per user.

POST /challenges/{id}/enter

{
  "media_id": 456,
  "caption": "Optional entry caption"
}

Returns 409 Conflict if the user has already reached max_entries_per_user for this challenge.

POST /challenges/{id}/vote

{
  "entry_id": 789
}

Returns 403 Forbidden if the challenge is not in Voting stage. Returns 409 Conflict if the user already voted.

Frontend Behavior

The /media/challenges/ page displays challenges in three tabs: Active, Voting, and Finalized.

Challenges page with tab navigation and challenge cards

  • Active tab - Shows the entry submission form when the user is logged in and has not yet reached the entry limit
  • Voting tab - Shows all entries as a grid with vote buttons; the user's vote is highlighted after casting
  • Finalized tab - Shows entries ranked by votes with winner badges for positions 1, 2, and 3

The media picker in the entry form lets users select from their existing uploaded media or upload a new photo directly.

Challenge entry submission form

Who Can See Entries (Logged-Out Experience)

  • Entering makes the photo public. The moment a member submits an entry, that photo is visible in the challenge gallery to everyone - including logged-out visitors - even if the original upload is private. This is by design: challenges are a public competition, so an entered photo can't stay hidden while still being voted on. The entry form tells the member this before they submit.
  • Challenges show when voting opened, not just when it ends. Each challenge displays both a Voting opened and a Voting ends timestamp, so visitors can see the full voting window instead of only a deadline with no visible start.
  • Logged-out visitors get a call to action, not a blank area. On the Active tab, a visitor who is not logged in sees Create an account to enter challenges (linking to registration) in place of the entry form. On the Voting tab, each entry shows Log in to vote (linking to login) instead of a vote button. Neither area is left empty.

Scheduled Actions

Action Hook Condition
mvs_activate_scheduled_challenges Runs hourly - sets Scheduled challenges to Active when start date is past
mvs_close_challenge_entries Runs hourly - sets Active challenges to Voting when entry deadline is past
mvs_finalize_expired_challenges Runs hourly - sets Voting challenges to Finalized, tallies votes, awards XP

Photo Battles

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Challenge any photographer on the site to a head-to-head photo duel - your best shot vs. theirs, side by side, with the community deciding the winner.

What You Can Do (as a User)

  • Challenge any member to a 1v1 photo battle directly from a media item page
  • Pick your strongest photo as your battle entry
  • Accept or decline incoming battle challenges from your notifications
  • Watch the live vote count during the voting period
  • See your win/loss record on your media dashboard

How It Works (for Users)

Challenging Someone

  1. Open any of your uploaded photos on the site
  2. Click Challenge to Battle
  3. Search for a member by username to challenge them
  4. Your photo is set as your battle entry and the invite is sent
  5. Your opponent receives a notification to accept or decline

After the Challenge is Accepted

  1. Both you and your opponent have 48 hours to confirm or change your battle photo
  2. Choose one photo from your uploads - click Submit My Photo
  3. Once both photos are submitted, the battle opens for community voting
  4. The VS layout shows both photos side by side with a vote button under each
  5. Any logged-in member (except participants) can cast one vote
  6. When the voting period ends, the winner is announced automatically and XP is awarded

Photo Battles frontend page showing VS layout

Viewing Your Battles

Go to My Media > Battles to see all your battles with win/loss/pending status. Click any battle to see the full vote breakdown.

For Site Owners

  1. Go to WPMediaVerse > Settings > Gamification and enable Photo Battles
  2. Battles are self-service - users challenge each other directly, no admin involvement required
  3. Submit and vote windows are currently 48 hours each
  4. Monitor all active battles from Competitions

Lifecycle

Stage Description
Pending Challenger sent the invite - waiting for opponent response
Accepted Opponent accepted - both users can now submit their battle photo
Submitting Both users have 48 hours (default) to submit photos
Voting Both photos submitted - community votes for 48 hours (default)
Resolved Vote deadline passed - winner determined, XP awarded
Declined Opponent declined the challenge - battle closed
Expired Submit or vote deadline passed with incomplete participation

Starting a Battle

From any media item page, click Challenge to Battle. You can also start a battle from your My Media dashboard.

Media item page with Challenge to Battle button

  1. Select the photo you want to use as your battle entry
  2. Search for and select your opponent by username
  3. Submit the challenge invite

The opponent receives a WPMediaVerse notification and, if BuddyPress notifications are active, a BuddyPress notification.

Submitting a Photo

After the opponent accepts, both users have 48 hours to submit their battle photo. Each participant selects one photo from their media library or uploads a new one.

If either participant does not submit within the deadline, the battle expires and no XP is awarded.

Voting

Once both photos are submitted, the battle moves to Voting. Any logged-in user (not just participants) can vote for one photo. Participants cannot vote in their own battle.

Votes are recorded in mvs_competition_votes. Each user can cast one vote per battle.

Settings Reference

Setting Key Default
Enable Photo Battles mvs_battles_enabled Off

Submit and vote deadlines are currently fixed at 48 hours each. Configurable deadline settings are planned for a future release.

REST API

Base URL: /wp-json/mvs-pro/v1/battles

Method Endpoint Description
POST /battles Create a new battle challenge
GET /battles/{id} Get battle details, stage, and vote counts
POST /battles/{id}/accept Accept a battle invite. Opponent only.
POST /battles/{id}/decline Decline a battle invite. Opponent only.
POST /battles/{id}/submit Submit your battle photo
POST /battles/{id}/vote Cast a vote for a photo in this battle

POST /battles

{
  "challenger_media_id": 456,
  "opponent_user_id": 99
}

Returns 403 Forbidden if battles are disabled. Returns 400 Bad Request if the challenger does not own the specified media.

POST /battles/{id}/submit

{
  "media_id": 501
}

Returns 403 Forbidden if the authenticated user is not a participant in this battle. Returns 409 Conflict if this participant already submitted.

POST /battles/{id}/vote

{
  "entry_id": 789
}

Returns 403 Forbidden if the battle is not in Voting stage, or if the voter is a participant.

Frontend Behavior

The /media/battles/ page shows all battles visible to the logged-in user.

Battles browse page showing battle cards with VS layout

  • Active battles - Shows the VS card layout with both photos and vote buttons if the battle is in Voting stage
  • Pending battles - Shows a card with the invite status and accept/decline buttons for the opponent
  • Resolved battles - Shows final vote counts with a winner badge

The My Media > Battles tab shows only the user's own battles with win/loss/pending status.

My Media Battles tab showing battle history

Scheduled Actions

Action Hook Condition
mvs_resolve_expired_battles Runs hourly - resolves battles where the vote deadline has passed; expires battles where the submit deadline passed without both submissions

Tournaments

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Enter a single-elimination bracket competition - submit your best photo, survive each round of community voting, and claim the championship title.

What You Can Do (as a User)

  • Register for an open tournament with one of your photos
  • Watch the bracket fill up as other photographers register
  • Vote in active round matches - any member can vote (except in their own match)
  • Track your bracket position round by round
  • Earn XP for every match you win and a large XP prize if you win the tournament

How It Works (for Users)

Registering

  1. Go to Media > Tournaments on your site
  2. Find a tournament in the Registration stage and click Register
  3. Pick one photo from your uploads to represent you in the tournament
  4. Click Submit Registration - your photo appears in the participant list
  5. Once registration closes, the bracket is generated automatically

Following the Bracket

  1. Open the tournament detail page to see the full bracket visualization
  2. Completed rounds are shown in grey; the active round's matches are highlighted
  3. Each matchup shows two photos side by side with the current vote count
  4. Click Vote on the photo you think should advance - you get one vote per match
  5. You cannot vote in a match where you are a participant
  6. When all matches in a round are resolved, the next round opens automatically

What Happens If You Win or Lose

  • Win a match: You advance to the next round and earn Round Win XP
  • Lose a match: You are eliminated but keep any XP already earned
  • Win the tournament: You earn the Winner XP prize and a tournament champion badge
  • Reach the final but lose: You earn the Runner-Up XP prize

Tournament bracket visualization showing round progression

For Site Owners

  1. Go to WPMediaVerse > Settings > Gamification and enable Tournaments
  2. Go to Competitions > Tournament Manager and click Add Tournament
  3. Set the title, bracket size (4, 8, 16, 32, or 64 participants), registration window, and match vote duration
  4. Set XP prizes for the winner, runner-up, and each round win
  5. Click Save - the tournament appears on the frontend when registration opens
  6. The bracket generates automatically when registration closes

Tournament Manager create form

Bracket Sizes

Supported sizes: 4, 8, 16, 32, 64 participants.

If registrations do not fill the bracket exactly, the system assigns byes to the highest-seeded empty slots. Bye recipients automatically advance to the next round without a match.

Seeding is random at bracket generation time.

Lifecycle

Stage Description
Registration Tournament is open - users can register with a photo
In Progress Registration closed - bracket generated, rounds underway
Finals Only two participants remain
Complete Winner determined, XP awarded to all placers

The system generates the bracket when the registration deadline passes (hourly Action Scheduler check). Each round's matches open for voting simultaneously. A new round begins after all matches in the current round are resolved.

Creating a Tournament

Go to Competitions > Tournament Manager and click Add Tournament.

Tournament Manager create form

Field Description
Title Tournament name displayed to users
Bracket Size Maximum participants: 4, 8, 16, 32, or 64
Registration Opens Date and time users can start registering
Registration Closes Deadline for registrations - bracket generates after this
Match Vote Duration How long each round's matches are open for votes (in hours)
XP - Winner XP awarded to the tournament winner
XP - Runner-Up XP awarded to the finalist who loses
XP - Round Win XP awarded each time a participant wins a match

Settings Reference

Setting Key Default
Enable Tournaments mvs_tournaments_enabled Off

REST API

Base URL: /wp-json/mvs-pro/v1/tournaments

Method Endpoint Description
POST /tournaments Create a tournament. Requires the manage_mvs_settings capability.
GET /tournaments/{id} Get tournament details, stage, and participant count
POST /tournaments/{id}/register Register for a tournament with a photo
GET /tournaments/{id}/bracket Get the full bracket structure with all matches and results
POST /tournaments/{id}/vote Cast a vote in an active tournament match

POST /tournaments/{id}/register

{
  "media_id": 456
}

Returns 409 Conflict if the user is already registered. Returns 400 Bad Request if registration is closed or the bracket is full.

GET /tournaments/{id}/bracket

Returns the full bracket as a nested structure by round.

{
  "tournament_id": 12,
  "size": 16,
  "current_round": 2,
  "rounds": [
    {
      "round": 1,
      "matches": [
        {
          "match_id": 101,
          "entry_a": { "entry_id": 5, "media_id": 456, "user_id": 11, "votes": 14 },
          "entry_b": { "entry_id": 9, "media_id": 502, "user_id": 22, "votes": 8 },
          "winner_entry_id": 5,
          "status": "resolved"
        }
      ]
    }
  ]
}

POST /tournaments/{id}/vote

{
  "match_id": 201,
  "entry_id": 789
}

Returns 403 Forbidden if the match is not in the current active round. Returns 409 Conflict if the user already voted in this match. Participants cannot vote in matches they are part of.

Frontend Behavior

The /media/tournaments/ page lists all tournaments with their current stage and participant count.

Tournaments browse page showing tournament cards

Clicking a tournament opens the detail page with the bracket visualization. Active matches show vote buttons directly inside the bracket.

Tournament detail page with bracket

  • Registration stage - Shows a Register button and the current participant count relative to bracket size
  • In Progress - Shows the bracket with completed rounds greyed out and active round matches highlighted
  • Complete - Shows the full bracket with the winner highlighted at the top

The My Media > Tournaments tab shows tournaments the user is registered in, with their current bracket position.

Bye Handling

When registrations do not fill the bracket exactly, byes are assigned before round 1 begins. A bye appears in the bracket as an automatic win - the real participant advances and their slot shows "Bye" for the opposing side. Bye participants do not earn a Round Win XP award.

Scheduled Actions

Action Hook Condition
mvs_start_registered_tournaments Runs hourly - generates brackets when registration deadline passes
mvs_resolve_expired_matches Runs hourly - resolves matches where the vote deadline has passed; advances winners to next round; detects when all matches in a round are resolved and opens the next round

Media Boosts

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Spend your earned points to push a photo to the top of the Explore feed - get more eyes on your best work right when you want it seen.

What You Can Do (as a User)

  • Boost any of your photos to increase its visibility in the Explore feed
  • Choose how many impressions you want to buy (e.g., 500 views)
  • See a live point cost preview before committing
  • Track how many impressions your boost has delivered so far
  • Run boosts on multiple photos simultaneously (a boost runs until its impression target or 30-day expiry; it cannot be cancelled early)

How It Works (for Users)

  1. Open one of your uploaded photos
  2. Click Boost - a panel shows your current point balance and the cost per 100 impressions
  3. Enter your target impression count (e.g., 500 impressions costs 250 points at the default rate)
  4. Click Confirm Boost - points are deducted immediately from your balance
  5. Your photo is now marked as boosted and appears at elevated positions in the Explore feed
  6. Return to the photo page anytime to see a progress bar showing impressions delivered vs. target
  7. When the impression target is reached (or after 30 days), the boost expires automatically and your photo returns to its normal organic rank

Media item page showing Boost button

Where Boosted Content Appears

Boosted photos appear throughout the Explore feed at regular intervals mixed in with organic content - they do not all cluster at the top. This means boosted photos reach a wider audience as visitors scroll, rather than just people who see the top of the page.

For Site Owners

  1. Go to WPMediaVerse > Settings > Gamification and enable Media Boosts
  2. Set the Max Impressions Per Boost (default: 5,000) to control how large a boost can be
  3. Set the Cost Per 100 Impressions (default: 50 points) to match your community's point economy
  4. Users must earn points through other gamification activities (uploads, challenges, streaks) before they can boost

How Boosts Work (Technical)

  1. The user clicks Boost on any media item they own
  2. They set an impression target (up to the site maximum)
  3. The system deducts points from their wb-gamification balance
  4. The media item is flagged as boosted in mvs_boosts
  5. The Explore feed query injects boosted items at a higher rank
  6. When the impression counter reaches the target, or the boost duration expires, the boost auto-expires and the media returns to organic ranking

A user can have multiple media items boosted simultaneously. One media item can only have one active boost at a time.

Boost Cost Calculation

Cost is based on the impression target:

cost = ceil( impression_target / 100 ) × mvs_boost_cost_per_100

Example: boosting for 500 impressions at the default cost of 50 points per 100 costs 250 points.

The point deduction fires via wb-gamification's point spend API at the moment the boost is created. If the user does not have enough points, the API returns an error and the boost is not created.

Settings Reference

Setting Key Default Description
Enable Media Boosts mvs_boosts_enabled Off Master toggle for the boosts feature
Max Impressions Per Boost mvs_boost_max_impressions 5000 The highest impression target a user can set
Cost Per 100 Impressions mvs_boost_cost_per_100 50 Points deducted per 100 impressions purchased

Database Table: mvs_boosts

Column Type Description
id bigint Primary key
media_id bigint The boosted media item
user_id bigint User who created the boost
target_impressions int Impression target set at boost creation
current_impressions int Running impression count
points_spent int Points deducted from user balance
status varchar active or expired
created_at datetime When the boost was created
expires_at datetime Hard expiry datetime (independent of impressions)

REST API

Base URL: /wp-json/mvs-pro/v1

All boost endpoints require an authenticated user. Boosts are created and listed against the current user - there is no per-media boost route and no cancel endpoint; a boost runs until its impression target is met or its 30-day expiry passes.

Method Endpoint Description
GET /boosts List the current user's boosts (optional status, page, per_page)
POST /boosts Create a boost for one of your media items
GET /boosts/balance Get the current user's gamification point balance

POST /boosts

{
  "media_id": 456,
  "impressions_target": 500
}

Returns 400 Bad Request if impressions_target is below 100 or exceeds mvs_boost_max_impressions, if the media is not yours, or if you do not have enough points. Returns 409 Conflict if the media item already has an active boost. Returns 503 Service Unavailable if the wb-gamification points backend is not active.

GET /boosts/balance

{
  "balance": 1240
}

Returns { "balance": 0 } when the wb-gamification backend is not active.

Explore Feed Injection

Boosted media is injected into the Explore feed by a filter on the WPMediaVerse explore query. The injected items appear at regular intervals (every N organic items) rather than clustering at the top. The injection interval is not currently user-configurable.

Impression counts increment server-side when a boosted item is rendered in the feed. Impressions are tracked per page load, not per unique user.

Expiry

A boost expires when either condition is met:

  • current_impressions reaches target_impressions
  • The current time passes expires_at

The hard expiry datetime is set to 30 days after boost creation, regardless of the impression target.

Scheduled Actions

Action Hook Condition
mvs_expire_boosts Runs hourly - sets status = expired on boosts where the impression target is reached or expires_at has passed

Upload Streaks

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

Upload one photo a day and watch your streak grow - hit milestones to earn XP rewards, and use freeze tokens to protect your streak on days you miss.

What You Can Do (as a User)

  • Build a streak simply by uploading at least one photo or video each day
  • See your current streak count and your all-time best streak on your dashboard
  • Earn XP automatically when you hit streak milestones: 7 days, 30 days, 100 days, 365 days
  • Use freeze tokens to skip one missed day without losing your streak
  • Show off your streak badge next to your username across the site

How It Works (for Users)

Building Your Streak

  1. Upload any photo or video on your site - that counts as your streak day
  2. Your streak counter increments once per calendar day, no matter how many files you upload
  3. Come back the next day and upload again to keep your streak alive
  4. Your streak count and a flame badge appear on your profile and next to your username in comments

Streak Milestones

When your streak reaches a milestone, XP is awarded automatically to your account:

Milestone XP Awarded
7 days 50 XP
30 days 250 XP
100 days 1,000 XP
365 days 5,000 XP

Each milestone is awarded only once. If your streak breaks and you rebuild to 7 days again, no additional XP is awarded for that milestone.

User dashboard showing streak badge with flame icon

Using Freeze Tokens

If you miss a day, a freeze token automatically protects your streak. One token covers one missed day. If you have no tokens and miss a day, your streak resets to zero (your all-time best is never reset).

When the site owner enables Allow Streak Freezes (and the wb-gamification points backend is active), you can buy freeze tokens with your points directly from the streak widget. Admins can also grant tokens manually. Freezes are off by default - if the owner has not enabled them, a missed day always resets your streak.

Where Your Streak Badge Appears

Once your streak reaches 1 day, a badge showing your streak count appears:

  • Next to your username on every media card you post
  • Next to your name in comments
  • On your profile page
  • In the member directory

For Site Owners

  1. Go to WPMediaVerse > Settings > Gamification and enable Upload Streaks
  2. Streaks run automatically - no manual management needed
  3. Grant freeze tokens to specific users from Users > Edit User > Streak Tokens in wp-admin
  4. The daily streak check runs at 2 AM site timezone via Action Scheduler - confirm Action Scheduler is processing jobs

How Streaks Work (Technical)

A streak increments by 1 when a user uploads at least one media item on a calendar day (site timezone). If the user uploads multiple times on the same day, the streak still only increments once.

If a user misses a day with no upload, the mvs_daily_streak_check cron job (runs daily at 2 AM, site timezone) resets their current streak to 0. The longest streak value is never reset.

A streak freeze token skips one missed day. If the user has a freeze token and misses a day, the cron job consumes one token and does not reset the streak.

User Meta Keys

Meta Key Type Description
_mvs_current_streak int Number of consecutive days with at least one upload
_mvs_longest_streak int The user's all-time highest streak
_mvs_last_upload_date string YYYY-MM-DD of the user's most recent upload (site timezone)
_mvs_streak_freezes int Number of unused freeze tokens

Milestone XP Rewards

XP is awarded once per milestone. If a user reaches day 30, they receive the 7-day and 30-day rewards. If their streak later breaks and they rebuild to day 7, no XP is awarded again for that milestone.

Milestone awards are tracked via a separate user meta key to prevent duplicate payouts.

Milestone XP Awarded
7 days 50 XP
30 days 250 XP
100 days 1,000 XP
365 days 5,000 XP

Streak milestone XP award notification

Streak Freeze Tokens

When the Allow Streak Freezes setting is enabled (and the wb-gamification points backend is active), members can buy freeze tokens with their points from the streak widget - the cost is set by Freeze Cost (Points) (mvs_pro_streak_freeze_cost, default 100). Admins can also grant tokens manually. The whole freeze feature is off by default; with it disabled, a missed day always resets the streak.

When the mvs_daily_streak_check cron runs and finds a user missed yesterday:

  1. Check _mvs_streak_freezes
  2. If count is greater than 0 - decrement by 1, leave streak intact
  3. If count is 0 - reset _mvs_current_streak to 0

A freeze only protects against a single missed day. Two consecutive missed days break the streak even with a freeze token.

Streak Badge

A streak badge displays next to the username in media cards, comments, and the member directory when the user has an active streak of 1 day or more. The badge shows the current streak count.

Media card showing username with streak badge

The badge is suppressed if the user's streak is 0 or if the streaks feature is disabled.

Settings Reference

Setting Key Default
Enable Upload Streaks mvs_streaks_enabled Off

Scheduled Actions

Action Hook Schedule Description
mvs_daily_streak_check Daily at 2 AM (site timezone) Compares _mvs_last_upload_date to yesterday for every user with a streak greater than 0. Applies freezes or resets streaks. Awards XP for newly reached milestones.

The daily streak check runs via Action Scheduler, not WP-Cron. If Action Scheduler is not processing jobs, streaks will not break or award XP on time.

Gamification Admin Dashboard

Requires WPMediaVerse Pro - This feature is available exclusively in the Pro version.

The Gamification admin area is accessible at Competitions. It provides a unified view of all active and pending competitions, plus dedicated managers for challenges, tournaments, and battles.

Gamification admin screens only appear when at least one gamification feature is enabled in Settings.

Competitions admin page showing the main table

Competitions Overview Table

The main Competitions page lists every challenge, battle, and tournament in a single table.

Column Description
Title Competition name - click to open the detail view
Type Challenge, Battle, or Tournament
Status Current lifecycle stage with a color-coded badge
Entries Number of participants or entries submitted
Created Date the competition was created
Start / End Active date range
Actions Quick action buttons for the row

Quick Actions:

  • View - Opens the frontend competition page in a new tab
  • Edit - Opens the admin edit form for this competition
  • Finalize - Manually force a competition to the Finalized/Resolved stage. Use this when the scheduled action has not run yet or to end a competition early.

You can filter the table by Type and Status using the dropdowns above the table.

Competitions table filter controls

Challenge Manager

Go to Competitions > Challenge Manager to create and edit photo challenges.

Challenge Manager list view

The Challenge Manager list shows all challenges sorted by start date descending. Click Add Challenge to open the create form, or click any challenge title to edit it.

From the edit form you can:

  • Change the theme, dates, and XP prize amounts for a challenge that has not yet started
  • View the current entry count for a challenge that is Active or in Voting
  • Manually trigger finalization before the voting deadline

You cannot edit entry windows or XP prizes after a challenge reaches Active stage. Changing dates for an Active challenge requires manually editing the database.

Theme Library

Click Competitions > Theme Library to manage the pool of challenge themes used by Autopilot.

Theme Library admin page with category filter

Column Description
Theme Name The challenge title displayed to users
Category Organizational category (Nature, Urban, Portrait, etc.)
Used Count How many times Autopilot has used this theme
Status Active (available to Autopilot) or Disabled

Add a custom theme by clicking Add Theme. Set the name, category, and optional description. Mark a theme as Disabled to exclude it from Autopilot selection without deleting it.

Tournament Manager

Go to Competitions > Tournament Manager to create and manage tournaments.

Tournament Manager showing tournament list

From the Tournament Manager you can:

  • Create a new tournament with bracket size, dates, and XP prizes
  • View the generated bracket for any in-progress tournament
  • Manually advance a round if the scheduled action has not yet run
  • View per-match vote counts and participant photos

Clicking a tournament title opens the admin bracket view, which mirrors the frontend bracket but adds vote count detail and a manual resolve button per match.

Admin bracket view for a tournament

Battle Monitor

Go to Competitions > Battle Monitor to oversee all active battles.

Battle Monitor table showing active battles

The Battle Monitor table includes every battle that is not in a terminal state (Declined, Expired, Resolved). Columns show:

Column Description
Challenger Username and photo thumbnail
Opponent Username and photo thumbnail
Stage Current stage (Pending, Accepted, Submitting, Voting)
Votes Vote count for each side (visible once in Voting stage)
Submit Deadline When the submit window closes
Vote Deadline When the vote window closes
Actions Resolve (force resolution) or Delete

Resolve disputes - If a battle is stuck due to a bug or edge case, use the Resolve button to manually set a winner or mark the battle as expired.

Gamification Settings Page

Go to WPMediaVerse > Settings > Gamification to configure all gamification features.

Gamification settings page showing feature toggles

Feature Toggles

Toggle Setting Key What It Controls
Enable Photo Challenges mvs_challenges_enabled Shows the challenges frontend page and enables the REST API routes
Enable Photo Battles mvs_battles_enabled Shows the battles frontend page and enables the REST API routes
Enable Tournaments mvs_tournaments_enabled Shows the tournaments frontend page and enables the REST API routes
Enable Media Boosts mvs_boosts_enabled Shows Boost buttons on media items and enables the REST API routes
Enable Upload Streaks mvs_streaks_enabled Activates streak tracking on media upload and the daily cron check

Autopilot Configuration

Setting Key Default
Enable Autopilot mvs_autopilot_enabled Off
Autopilot Day mvs_autopilot_day monday
Autopilot Hour mvs_autopilot_hour 9

XP Reward Amounts

These fields set the default XP prizes applied when creating new challenges and tournaments. Existing competitions are not affected when you change these values.

Field Description
Challenge 1st Place XP Default XP for challenge winner
Challenge 2nd Place XP Default XP for challenge runner-up
Challenge 3rd Place XP Default XP for third place
Challenge Participation XP Default XP for all entrants
Tournament Winner XP Default XP for tournament champion
Tournament Runner-Up XP Default XP for tournament finalist
Tournament Round Win XP Default XP per round win

Boost Configuration

Setting Key Default
Max Impressions Per Boost mvs_boost_max_impressions 5000
Cost Per 100 Impressions mvs_boost_cost_per_100 50

Saving the Settings page does not restart any scheduled actions. If you enable a feature that was previously disabled, existing scheduled actions will pick up the new state on the next hourly run.

Developer Guide

REST API, hooks, templates, storage drivers, WP-CLI, and migrations.

REST API Reference

Endpoints and hooks marked (Pro) require WPMediaVerse Pro. Everything documented below ships in the free plugin.

Base URL: /wp-json/mvs/v1/

All routes below use the mvs/v1 namespace (the messaging routes share the same namespace).

Authentication. Reads of public data are open. Every write - and every /me/* route - requires an authenticated user. Pass the X-WP-Nonce header with a nonce generated via wp_create_nonce( 'wp_rest' ) and send cookies with credentials: 'same-origin', or use a WordPress Application Password for non-browser clients.

Private-community gate (2.2.0). When the host community is private, the entire mvs/v1 surface - including public reads - requires authentication, and with Pro active the mvs-pro/v1 namespace is covered by the same gate. Unauthenticated requests get 401 with code mvs_community_private. The gate is off by default and controlled by three filters: mvs_rest_require_auth (return true to arm the gate; BuddyNext arms it automatically when its private-community mode is on), mvs_rest_can_access (defaults to is_user_logged_in(); override to allow specific unauthenticated callers, e.g. a trusted server-to-server integration), and mvs_rest_gated_route_prefixes (the covered route prefixes; Pro appends its own namespace here, and sites can add more).

Authorization model. Three levels are used throughout:

  • Public - no auth; privacy is enforced inside the query so private rows never leak.
  • Authenticated - any logged-in user (is_user_logged_in()).
  • Capability - a specific capability such as upload_mvs_media, moderate_mvs_media, or manage_mvs_access.

Update methods. Every route documented below with PUT also accepts PATCH and POST. WordPress registers these three together as its "editable" method group, so all three reach the same handler with the same arguments and the same response. PUT is used throughout this page as the canonical form; pick whichever your HTTP client handles most comfortably.

Rate limiting. Many routes are throttled per user/IP (the limit is noted where it is unusually tight). Exceeding a limit returns 429 Too Many Requests.


Media

GET /media

Auth: Public (privacy enforced in query). Rate-limited to 120/min.

List media items. Returns only rows the caller is allowed to see.

Parameters:

Parameter Type Default Description
page int 1 Page number
per_page int 20 Items per page (max: 100, filterable via mvs_rest_pagination_max)
media_type string (all) Filter by type: image, video, audio, document
author int (all) Filter by user ID
slug string (none) Fetch a single item by post slug
tag string (all) Filter by mvs_tag slug
category string (all) Filter by mvs_category slug
orderby string date Sort: date, trending, popular (filterable via mvs_feed_sort_options)
scope string public public or all (owner/privileged callers)
s string (none) Full-text search term
group_covers bool false Collapse gallery groups to a single cover item

Response:

{
  "items": [
    {
      "id": 123,
      "title": "Sunset Photo",
      "description": "",
      "media_type": "image",
      "file_url": "https://example.com/wp-content/uploads/wpmediaverse/2025/03/photo.jpg",
      "privacy": "public",
      "author_id": 1,
      "album_id": null,
      "views": 42,
      "reactions_count": 5,
      "comments_count": 2,
      "created_at": "2025-03-27T12:00:00Z",
      "can_edit": false,
      "is_favorited": false,
      "viewer_reaction": null
    }
  ],
  "total": 50,
  "pages": 5
}

Viewer-aware fields. Every media item carries three fields resolved against the requesting user, not cached statically: can_edit (bool - true when the viewer is the author or has manage_options), is_favorited (bool), and viewer_reaction (string reaction slug, or null if the viewer hasn't reacted). All three are false/null for anonymous requests. List endpoints batch-load this state per page (MediaController::prime_viewer_state()) rather than querying per row.

POST /media

Auth: Capability - upload_mvs_media (or manage_options).

Upload a new media file.

Body (multipart/form-data):

Field Required Description
file Yes The file to upload
title No Media title (defaults to filename)
description No Text description
privacy No Privacy level (default: site setting)
group_id No BuddyPress group ID (required when privacy=group)
album_id No Add to this album after upload
is_story No true to mark as a story

Response: 201 Created with the new media object.

GET /media/{id}

Auth: Public (privacy check in permission callback). Returns 403 if the caller cannot view it, 404 if it does not exist.

Get a single media item.

PUT /media/{id}

Auth: Capability - owner with edit_mvs_medias, or edit_others_mvs_medias.

Update a media item.

Body (JSON):

{
  "title": "Updated Title",
  "description": "Updated description",
  "privacy": "private"
}

DELETE /media/{id}

Auth: Capability - owner with delete_mvs_medias, or delete_others_mvs_medias.

Delete a media item and its stored file.

POST /media/{id}/replace

Auth: Capability - same as PUT /media/{id} (edit permission).

Replace the underlying file of an existing media item while keeping its ID, comments, reactions, and stats. Send the new file as multipart/form-data with a file field.

POST /media/{id}/view

Auth: Public.

Record a view for the item. Increments the view counter and writes a row to mvs_media_views.

POST /media/{id}/download

Auth: Public. Rate-limited to 30/min.

Record a download event and increment mvs_media_stats.downloads. Refused with 403 when the global Allow Downloads toggle is off OR the per-media allow_download meta is '0'.

POST /media/{id}/share

Auth: Public. Rate-limited to 60/min.

Record a share event and increment mvs_media_stats.shares. Called by the lightbox Share button after a successful navigator.share / clipboard copy.

GET /media/{id}/access

Auth: Public.

Report whether the current user can view the item (resolves privacy rules and any access grants). Returns an access decision, not the file.

Response:

{
  "media_id": 123,
  "can_view": true,
  "privacy": "members",
  "is_owner": false
}

privacy and is_owner are only populated when the caller can view the item or owns it; a caller with neither returns 403 before these fields are built.

GET /media/{id}/group

Auth: Public.

Return every item that belongs to the same gallery/upload group as {id} (used to build multi-item lightboxes).

GET /media/{id}/signed-url

Auth: Authenticated with view access to the media.

Generate a time-limited signed URL for a private file. The signed URL points at /serve and carries an HMAC-SHA256 signature binding the request to a user, media ID, and expiry.

Parameter Type Default Description
download bool false Issue a download (attachment) URL
ttl int (setting) Override the signed-URL lifetime, in seconds

GET /serve

Auth: Public - the HMAC signature on the URL is the credential (analogue of an S3 pre-signed URL). For non-public media the handler also re-checks can_view per request.

Stream the underlying file (full file or a thumbnail variant) for a validated signed URL. Drains output buffers and disables zlib.output_compression before streaming so byte counts match Content-Length. Honours Range: headers for video/audio.

Param Required Description
mvs_id yes Media ID
mvs_uid yes User ID the URL was signed for (0 for anonymous public media)
mvs_exp yes Unix expiration timestamp
mvs_sig yes HMAC-SHA256 signature
mvs_size no large / medium / thumbnail / watermark to serve a variant
mvs_dl no When 1, sets Content-Disposition: attachment and increments the download counter

GET /me/media

Auth: Authenticated.

List the current user's own media, including private and pending items. Accepts the same parameters as GET /media.


Albums

GET /albums

Auth: Public (privacy enforced in query).

List albums. Supports page, per_page, author, orderby, order.

POST /albums

Auth: Authenticated with album-create permission.

Create an album.

{
  "title": "My Album",
  "description": "Optional",
  "privacy": "public"
}

GET /albums/{id}

Auth: Public (private albums 404 for non-owners).

Get an album with its media list.

PUT /albums/{id}

Auth: Authenticated - owner / edit permission.

Update an album.

DELETE /albums/{id}

Auth: Authenticated - owner / delete permission.

Delete an album (does not delete the media items it contains).

PUT /albums/{id}/reorder

Auth: Authenticated - owner / edit permission.

Reorder the items inside an album.

{ "order": [103, 101, 102] }

POST /albums/{id}/items

Auth: Authenticated - owner / edit permission.

Add media to an album.

{ "media_ids": [101, 102, 103] }

DELETE /albums/{id}/items/{media_id}

Auth: Authenticated - owner / edit permission.

Remove a single media item from an album.

PUT /albums/{id}/cover

Auth: Authenticated - owner / edit permission.

Set the album cover.

{ "media_id": 101 }

Collections

GET /collections

Auth: Authenticated. Returns the current user's collections.

POST /collections

Auth: Authenticated.

Create a collection (manual or smart).

{
  "title": "Nature",
  "type": "smart",
  "rules": { "tags": ["nature"], "media_type": "image" },
  "privacy": "public"
}

GET /collections/{id}

Auth: Public (privacy check in permission callback).

Get a collection with its resolved item list.

PUT /collections/{id}

Auth: Authenticated - owner only.

Update a collection.

DELETE /collections/{id}

Auth: Authenticated - owner only.

Delete a collection.

PUT /collections/{id}/rules

Auth: Authenticated - owner only.

Set the smart-collection rules used to resolve its items.

{ "rules": [ { "field": "tag", "value": "nature" } ] }

Reactions

All reaction operations live on a single route that varies by HTTP method.

GET /media/{id}/reactions

Auth: Public.

Get reaction counts grouped by type. When a logged-in user calls it, the response also indicates that user's own reaction.

POST /media/{id}/reactions

Auth: Authenticated.

Add or change your reaction. Note the field name is reaction_type.

{ "reaction_type": "love" }

DELETE /media/{id}/reactions

Auth: Authenticated.

Remove your reaction.


Comments

GET /media/{id}/comments

Auth: Public (visibility follows the parent media's privacy).

List comments. Supports page, per_page (max 100).

POST /media/{id}/comments

Auth: Authenticated. Use @username syntax for mentions.

{
  "content": "Great photo @jane!",
  "parent": 0,
  "from_activity": 0
}
Field Required Description
content Yes Comment text
parent No Parent comment ID for threaded replies (default 0)
from_activity No Source BuddyPress activity ID, when posted from the activity stream

PUT /media/{id}/comments/{comment_id}

Auth: Authenticated - owner (within the edit window) or moderate_mvs_media.

Edit a comment.

DELETE /media/{id}/comments/{comment_id}

Auth: Authenticated - owner or moderate_mvs_media.

Delete a comment.


Favorites

GET /media/{id}/favorite

Auth: Authenticated.

Return whether the current user has favorited the item.

POST /media/{id}/favorite

Auth: Authenticated.

Add the item to favorites (toggles on). Optional collection_id saves it into a specific collection.

DELETE /media/{id}/favorite

Auth: Authenticated.

Remove the item from favorites.

GET /me/favorites

Auth: Authenticated.

List the current user's favorites. Supports collection_id, page, per_page.


Access Control & Grants

These routes manage per-media access rules and direct user grants. All require the media owner or the manage_mvs_access capability.

GET /media/{media_id}/rules

Auth: Owner or manage_mvs_access.

List the access rules attached to a media item.

POST /media/{media_id}/rules

Auth: Owner or manage_mvs_access. Rate-limited to 30/min.

Replace the full rule set for a media item.

{
  "rules": [
    { "rule_type": "follower", "rule_value": "1" },
    { "rule_type": "purchase", "rule_value": "1", "price": 4.99, "currency": "USD" }
  ]
}

Each rule's rule_type must be one of AccessRulesService::RULE_TYPES.

DELETE /media/{media_id}/rules/{rule_id}

Auth: Owner or manage_mvs_access.

Delete a single access rule.

POST /media/{media_id}/grant

Auth: Owner or manage_mvs_access.

Grant a specific user access to the media.

{
  "user_id": 55,
  "source": "manual",
  "expires_at": "2026-01-01T00:00:00Z"
}

source defaults to manual and must be one of AccessRulesService::GRANT_SOURCES.

DELETE /media/{media_id}/grant/{user_id}

Auth: Owner or manage_mvs_access.

Revoke a user's grant.

GET /me/grants

Auth: Authenticated.

List the media the current user has been granted access to.

Parameter Type Default Description
per_page int 20 Items per page (max: 100)
page int 1 Page number
active_only bool true Exclude expired grants

Follows

POST /users/{id}/follow

Auth: Authenticated. Rate-limited to 30/min.

Follow a user.

DELETE /users/{id}/follow

Auth: Authenticated.

Unfollow a user.

GET /users/{id}/followers

Auth: Public.

List a user's followers (display name + avatar).

GET /users/{id}/following

Auth: Public.

List who a user follows.

GET /me/following

Auth: Authenticated.

List who the current user follows.

GET /me/followers

Auth: Authenticated.

List the current user's followers.


User Profile

GET /me/profile

Auth: Authenticated.

Get the current user's profile.

PUT /me/profile

Auth: Authenticated.

Update profile fields.

{
  "first_name": "Jane",
  "last_name": "Smith",
  "display_name": "jsmith",
  "description": "Photographer"
}

POST /me/avatar

Auth: Authenticated.

Upload a new profile avatar (multipart/form-data, file field).

DELETE /me/avatar

Auth: Authenticated.

Remove the custom avatar and revert to Gravatar.


Users

GET /users/{id}

Auth: Public. Rate-limited to 60/min.

Get a user's public profile: bio, avatar URL, follower/following counts, and public media count. user_login / user_registered are returned only to the user themselves or to admins (enumeration hardening).

GET /users/{id}/media

Auth: Public (privacy enforced in query).

List a user's visible media. Supports page, per_page.

GET /users/search

Auth: Public.

Search for users by display name or username.

Parameter Type Default Description
q string (required) Search term
per_page int 10 Results per page (max: 50)

Reports & Blocking

POST /media/{id}/report

Auth: Authenticated. Rate-limited to 10/min.

Submit a content report against a media item.

{
  "reason": "inappropriate",
  "details": "Optional explanation"
}

reason must be one of ReportService::REASONS.

POST /users/{id}/report

Auth: Authenticated.

Report a user. Same reason / details body as media reports.

POST /users/{id}/block

Auth: Authenticated.

Block a user.

DELETE /users/{id}/block

Auth: Authenticated.

Unblock a user.

GET /me/blocked

Auth: Authenticated.

List the users the current user has blocked.


Moderation

All moderation routes require the moderate_mvs_media capability.

GET /moderation

List flagged / pending media items. Supports collection params (page, per_page).

The route is /moderation, not /moderation/queue. Earlier revisions of this page showed /moderation/queue, which returns 404.

GET /moderation/counts

Return queue counts (pending, flagged, etc.) for building badges and tabs.

POST /moderation/{id}/approve

Approve a media item.

POST /moderation/{id}/reject

Reject a media item. Optional reason string is recorded.

POST /moderation/{id}/analyze

Trigger AI analysis (description / tagging / safety) on a media item.

GET /ai/usage

Return AI usage / budget figures for the moderation dashboard.


Bulk Operations

POST /media/bulk

Auth: Authenticated with the relevant per-action capability. Rate-limited to 10/min, max 100 IDs per call.

Perform a bulk action on multiple media items.

{
  "action": "delete",
  "media_ids": [101, 102, 103]
}
Field Required Description
action Yes One of delete, move_to_album, change_privacy
media_ids Yes Array of media IDs (max 100)
album_id When action=move_to_album Destination album ID
privacy When action=change_privacy New privacy value

Stats

GET /media/{id}/stats

Auth: Public for public media; 403 for media the caller cannot view.

Per-item statistics (views, reactions, comments, downloads).

GET /me/stats

Auth: Authenticated.

Aggregate statistics across the current user's own media.


Tags

GET /tags

Auth: Public.

List / autocomplete mvs_tag terms.

Parameter Type Default Description
search string (none) Filter by name
per_page int 20 Results per page (max: 100)
orderby string name name or count

POST /tags

Auth: Capability - create_tag_permissions_check (any user who can upload media).

Create a new tag. Body: name (required), optional slug.

GET /tags/cloud

Auth: Public.

Return top tags with usage counts for a tag cloud. Optional limit (default 50, max 200).

POST /tags/merge

Auth: Capability - admin (moderate_mvs_media).

Merge one tag into another. All media on source_id are re-tagged with target_id and source_id is deleted.

{ "source_id": 12, "target_id": 7 }

PUT /tags/{id}

Auth: Capability - admin.

Rename a tag.

{ "name": "New Tag Name" }

DELETE /tags/{id}

Auth: Capability - admin.

Delete a tag.


Notifications

GET /me/notifications

Auth: Authenticated.

List the current user's notifications.

Parameter Type Default Description
per_page int 20 Items per page (max: 100)
page int 1 Page number
filter string all Filter set (e.g. all, unread)

The total count is returned in the X-WP-Total header.

GET /me/notifications/count

Auth: Authenticated.

Return the current user's unread notification count.

POST /me/notifications/read

Auth: Authenticated.

Mark notifications as read. Pass an ids array to mark specific notifications, or omit it to mark all as read.

{ "ids": [12, 13, 14] }

Native App & Interests

Added in 1.9.0 to support a native mobile/headless client: a public pre-login config call, an interest-picker onboarding flow, and "people you may know" suggestions. See mvs_app_config_features and related filters for how Free/Pro contribute to /app/config.

GET /app/config

Auth: Public.

Single call a client makes before theming itself and deciding which feature surfaces to mount. Returns only what the core /wp-json/ index cannot express (branding + feature flags) - site name, description, icon, and auth come from the core index, never restated here.

Response:

{
  "accent_color": null,
  "logo_url": null,
  "login_bg_url": null,
  "dark_mode_default": false,
  "layout": "grid",
  "pro_active": true,
  "features": {
    "messaging": true,
    "reactions": true,
    "comments": true,
    "favorites": true,
    "albums": true,
    "collections": true,
    "follows": true,
    "notifications": true,
    "activity": true
  }
}

features.messaging is false when mvs_dm_access is nobody/disabled/none. Pro extends features with its own toggles (battles, challenges, tournaments, boosts, streaks, video, stories, …) and can populate accent_color / logo_url / login_bg_url / dark_mode_default / layout from its Mobile App Branding settings.

GET /app/interests

Auth: Public.

Available interest chips for the onboarding picker - the top 40 mvs_category terms by usage count, each with a representative public cover thumbnail. Cached (transient, default 1 hour, filterable via mvs_app_interests_cache_ttl).

Response:

[
  { "id": 12, "name": "Nature", "slug": "nature", "count": 84, "cover_url": "https://example.com/.../serve?..." }
]

GET /me/interests

Auth: Authenticated.

The current user's saved interest picks.

Response:

{ "interest_ids": [12, 19] }

POST /me/interests

Auth: Authenticated.

Save the current user's interest picks. Only valid mvs_category term IDs are kept; unknown IDs are silently dropped. Saving also marks the viewer as onboarded (mvs_onboarded user meta), so a client doesn't have to separately call /me/onboarding/complete.

{ "interest_ids": [12, 19, 4] }
Field Required Description
interest_ids Yes Array of mvs_category term IDs

Response: { "interest_ids": [12, 19, 4] } (filtered to valid term IDs).

GET /users/suggested

Auth: Authenticated.

"People you may know" - ranked creators (popularity + interest overlap with the viewer's picks), excluding the viewer, users they already follow, and blocked users. Each result carries up to 3 sample public-media thumbnails for the suggestion card.

Parameter Type Default Description
limit int (service default) Max results, clamped to 1-50

Response:

[
  {
    "id": 55,
    "name": "Jane Smith",
    "avatar": "https://example.com/avatar.jpg",
    "profile_url": "https://example.com/media/@jane/",
    "follower_count": 120,
    "is_following": false,
    "sample_media": ["https://example.com/.../thumb1.jpg"]
  }
]

The candidate pool is cached (default 1 hour, filterable via mvs_suggestions_cache_ttl).

POST /me/onboarding/complete

Auth: Authenticated.

Explicitly flag the current user as onboarded (mvs_onboarded user meta), for clients whose first-session flow doesn't end with saving interests. Idempotent.

Response: { "onboarded": true }


Admin

POST /admin/welcome/dismiss

Auth: Authenticated (per-user state).

Dismiss the admin welcome banner for the current user.


Activity Feed

GET /feed

Auth: Public (private events never appear; following scope is empty for anonymous callers).

Return the activity feed.

Parameter Type Default Description
scope string public public (all public media) or following (followed users)
per_page int 20 Items per page (max: 100)
page int 1 Page number

GET /users/{id}/activity

Auth: Public.

Return a user's public activity (uploads, album creations, reactions). Supports page, per_page.


Messaging

Direct-messaging routes share the mvs/v1 namespace. All require authentication. Conversations started by users you do not follow land in the Requests tab until accepted or declined.

GET /me/conversations

List the current user's conversations.

Parameter Type Default Description
tab string all all, unread, or requests
per_page int 20 Conversations per page (max: 50)
page int 1 Page number

POST /conversations

Start a new conversation.

{ "recipient_id": 42, "as_request": false }
Field Required Default Description
recipient_id Yes - User ID to start (or resume) a conversation with
as_request No false When true, force the conversation to open as a pending message request (lands in the recipient's Requests tab and must be accepted/declined) instead of an active thread - even if the sender/recipient relationship would otherwise allow a direct thread. Lets a native app open a "message request" flow explicitly through mvs/v1 alone (1.8.0).

Response: 201 Created with the new conversation object.

GET /conversations/{id}

Get a single conversation's metadata and participants.

PATCH /conversations/{id}

Update the current user's per-conversation preferences.

{
  "is_muted": true,
  "is_pinned": false,
  "is_archived": false
}

DELETE /conversations/{id}

Leave (soft-delete) the conversation for the current user.

GET /conversations/{id}/messages

List messages in a conversation (newest-first).

Parameter Type Default Description
per_page int 30 Messages per page (max: 100)
before int 0 Return messages with ID less than this (cursor pagination)

POST /conversations/{id}/messages

Send a message.

{
  "content": "Hey, love the photo!",
  "message_type": "text",
  "media_id": null,
  "attachment_id": null,
  "parent_id": null,
  "metadata": {}
}
Field Required Description
content Yes (unless an attachment/media is sent) Message text
message_type No text (default) or media
media_id No Attach an existing media post
attachment_id No Attach a file uploaded via POST /messages/upload
parent_id No Reply to this message ID
metadata No Arbitrary structured metadata

POST /conversations/{id}/read

Mark all messages in the conversation as read for the current user.

POST /conversations/{id}/typing

Send a typing-indicator event (no persistent storage; fires a real-time event only).

POST /conversations/{id}/accept

Accept a message request - moves the conversation from Requests to All.

POST /conversations/{id}/decline

Decline a message request - removes the conversation from the inbox.

DELETE /messages/{id}

Soft-delete a message for the current user (content hidden, record retained).

DELETE /messages/{id}/unsend

Hard-delete (unsend) a message. Only available within the edit window and only for the message owner.

POST /messages/{id}/reactions

Add an emoji reaction to a message.

{ "emoji": "heart" }

DELETE /messages/{id}/reactions

Remove your emoji reaction from a message.

POST /messages/upload

Upload an attachment for use in a DM. Returns a reference ID to pass as attachment_id (or media_id) when sending the message. Body: multipart/form-data with a single file field.

{ "media_id": 204, "url": "https://example.com/..." }

GET /me/messages/unread-count

Return the total unread message count for the current user.

{ "count": 3 }

GET /messages/poll

Long-poll for new messages. The server holds the connection open and responds as soon as a new message arrives or the timeout is reached.

Parameter Type Default Description
since int (required) Return messages with ID greater than this value
conversation_id int 0 Scope the poll to a single conversation

Error Responses

All errors follow the WP REST API error format:

{
  "code": "mvs_invalid_type",
  "message": "This file type is not allowed.",
  "data": { "status": 400 }
}

Common error codes:

Code Status Meaning
mvs_invalid_type 400 MIME type not in allowed list
mvs_file_too_large 400 File exceeds max upload size
mvs_blocked_extension 400 Dangerous file extension
mvs_no_ids 400 Bulk request had no media IDs
mvs_duplicate 409 Duplicate file (when duplicate_action=skip)
mvs_not_found 404 Resource not found
mvs_user_not_found 404 User not found
mvs_not_logged_in / mvs_unauthorized 401 Authentication required
mvs_forbidden / rest_forbidden 403 Access denied by privacy/capability rules
mvs_storage_failed 500 Storage driver error
(rate limit) 429 Too many requests

Authentication

Routes for obtaining the credentials the rest of the API expects. The app-auth filters that govern this flow (mvs_app_password_login_enabled, mvs_app_connect_schemes, mvs_app_scheme, mvs_app_credential_issued) are documented in Hooks & Filters.

GET /auth/nonce

Return a fresh wp_rest nonce for the current session.

Auth: Authenticated (cookie).

{ "nonce": "a1b2c3d4e5" }

Browser clients that keep a page open longer than the nonce lifetime call this to refresh rather than reloading the page.

POST /auth/app-password

Exchange a WordPress username and password for an Application Password - the credential a mobile or desktop client actually stores.

Auth: Public by necessity. This is how a member obtains their first credential, so there is nothing to authenticate with yet.

Parameter Type Required Description
username string yes Email address or username.
password string yes Account password.
app_name string no Name shown beside this credential in the member's profile.
app_id string no Stable per-install id, so a repeat sign-in replaces the existing row instead of adding another.

Because it is unauthenticated, this route is guarded harder than any other: a site-owner switch (mvs_app_password_login_enabled), a TLS requirement, uniform failure responses that do not reveal whether a username exists, the suspension gate, and rate limiting applied before any credential is read. Accounts with two-factor authentication receive 409 rather than a silent 2FA bypass.

Always send this over HTTPS. Store the returned credential in the platform keychain, never in plain preferences.


Account

DELETE /me

Schedule deletion of the authenticated member's own account.

Auth: Authenticated.

Parameter Type Required Description
confirm string yes Must be the literal string DELETE.
password string conditional The account password, re-entered. Required unless mvs_account_deletion_password_required is filtered to false.

Deletion is not immediate. The request starts a grace period (default 30 days, filterable via mvs_account_deletion_grace_days; return 0 to delete on request). During the grace period the member can still cancel.

GET /me/deletion

Return the authenticated member's pending deletion request, if any - including when it is scheduled to execute.

Auth: Authenticated.

DELETE /me/deletion

Cancel a pending deletion request and restore the account to normal standing.

Auth: Authenticated.

These three routes are the member-facing half of GDPR erasure. The admin-facing export and erasure tools are covered in GDPR & Privacy.

GET /me/transactions

The authenticated member's own usage ledger - upload credits consumed and granted.

Auth: Authenticated.

Parameter Type Default Description
per_page integer 20 Rows per page.
page integer 1 Page number.

Pairs with the [mvs_usage_history] shortcode, which renders the same data.


Access control

GET /access/options

Return the building blocks for the access-rule builder: the site's roles, and the rule types available to members.

Auth: Authenticated.

Drives the frontend edit-modal access panel, the admin sub-page, and the mobile app - all three read this one endpoint rather than hardcoding a rule-type list. Pro extends the returned rule types through the mvs_access_rule_types_ui filter, so a client that renders whatever this endpoint returns picks up Pro's monetization and code-grant rule types with no client change.


These sit alongside the messaging routes documented above.

GET /conversations/{id}/media

Every media attachment shared in one conversation, newest first. This is what backs the "shared media" panel in a chat.

Auth: Authenticated, and the caller must be a participant.

Parameter Type Default Range Description
per_page integer 60 1-200 Attachments per page.

GET /conversations/{id}/messages/search

Full-text search within a single conversation.

Auth: Authenticated, and the caller must be a participant.

Parameter Type Required Default Range Description
q string yes - - Search term.
per_page integer no 50 1-100 Results per page.

Search is scoped to the one conversation in the path. There is no cross-conversation search endpoint.

Pro REST API Reference

Endpoints marked (Pro) require WPMediaVerse Pro. Sections tagged with a version (e.g. "New in 1.9.0") were added in that release or later - check your installed Pro version if a route 404s.

Base URL: /wp-json/mvs-pro/v1/

WPMediaVerse Pro registers its own REST namespace, mvs-pro/v1, alongside the free mvs/v1 namespace. Pro must be active for any of these routes to be registered.

Authentication uses the same mechanism as the free API: pass an X-WP-Nonce header with a nonce from wp_create_nonce( 'wp_rest' ) and include credentials: 'same-origin' so the request is tied to the logged-in user. Application Passwords (WP 5.6+) work for mobile/headless clients.

Note (2.2.0): the free plugin's private-community gate (mvs_rest_require_auth) covers mvs-pro/v1 too - Pro appends its namespace via the mvs_rest_gated_route_prefixes filter, so on a private community even Pro's public reads (e.g. tournament brackets) require login. Each route additionally enforces its own permission callback as listed below.

Update methods. Every route documented below with PUT also accepts PATCH and POST - WordPress registers those three together as its "editable" method group, so all three reach the same handler with the same arguments and the same response. PUT is used as the canonical form throughout this page.

Feature toggles. A Pro feature that is switched off never registers its routes, so its endpoints return 404, not 403. If a documented route 404s on a Pro site, check the matching toggle in Settings Reference before assuming a version mismatch. This affects the Stories and Connectors routes in particular, since both ship disabled by default.

Permission conventions used below:

  • Public - no authentication required (__return_true or open read). Some are rate-limited inside the service.
  • User - any logged-in user (is_user_logged_in).
  • Owner/Admin - the media owner or a user who can edit others' media.
  • Admin - requires manage_options or manage_mvs_settings (noted per route).
  • HMAC - no WordPress auth; request body is verified against an HMAC-SHA256 signature header.

Some feature areas only register their routes when the matching admin toggle is enabled (mvs_battles_enabled, mvs_challenges_enabled, mvs_tournaments_enabled, mvs_boosts_enabled, mvs_streaks_enabled, mvs_connectors_enabled). When a feature is disabled its routes are not registered.


Quota & Credits

User-facing quota summaries plus admin package/credit management and the signed external top-up webhook.

GET /me/quota

Return the current user's quota summary (per-type usage and limits for image, video, audio).

Auth: User


GET /me/quota/check

Lightweight pre-upload check: can the current user upload a given media type (and optional file size) right now?

Auth: User

Parameters:

Parameter Type Required Default Description
media_type string Yes - One of image, video, audio
file_size int No 0 Size in bytes, for storage-limit checks

Response:

{ "can_upload": true, "reason": "" }

GET /me/credits/history

Return the current user's credit transaction history.

Auth: User


GET /users/{user_id}/quota

Get a specific user's quota summary.

Auth: Admin


POST /users/{user_id}/package

Assign a quota package to a user.

Auth: Admin

Body:

Field Type Required Description
package_id int Yes Package to assign

POST /users/{user_id}/credits

Grant extra upload credits to a user for a specific media type.

Auth: Admin

Body:

Field Type Required Default Description
media_type string Yes - One of image, video, audio
amount int Yes - Credits to add (min 1)
note string No "" Optional ledger note

GET /packages

List all quota packages.

Auth: Admin


POST /packages

Create a quota package.

Auth: Admin

Body:

Field Type Required Default Description
name string Yes - Package name
image_limit int No 0 Image upload limit (0 = none)
video_limit int No 0 Video upload limit
audio_limit int No 0 Audio upload limit
storage_bytes int No 0 Storage cap in bytes
is_default bool No false Whether this is the default package

PUT /packages/{id}

Update a quota package.

Auth: Admin


DELETE /packages/{id}

Delete a quota package.

Auth: Admin


POST /credits/webhook

External credit top-up endpoint. Used by integrations that grant credits from an outside system.

Auth: HMAC - the request is not cookie/capability authenticated. The handler verifies the X-MVS-Signature header against hash_hmac( 'sha256', $body, $secret ) using hash_equals(). Requests with an invalid or missing signature are rejected.


Privacy

Advanced per-item and bulk privacy controls plus reusable per-user privacy presets.

PUT /media/{id}/privacy

Update the privacy level of a single media item. Pro-only because it supports the advanced levels (e.g. specific-people, album-inherit).

Auth: Owner/Admin

Body:

Field Type Required Default Description
privacy string Yes - Privacy level (validated against the configured set)
custom_users int[] No [] User IDs allowed to view, when privacy is "Specific People"
inherit_album bool No false When true, the item follows its album's privacy

POST /media/bulk-privacy

Apply the same privacy level to many media items at once.

Auth: User (per-item ownership is enforced inside the service)

Body:

Field Type Required Description
media_ids int[] Yes 1-100 media IDs to update
privacy string Yes Privacy level to apply to all selected items

GET /privacy/presets

List the current user's saved privacy presets.

Auth: User


POST /privacy/presets

Save a new privacy preset for reuse.

Auth: User

Body:

Field Type Required Default Description
name string Yes - Preset name (≤100 chars)
privacy string Yes - Privacy level to store
custom_users int[] No [] User IDs to include when privacy is "Specific People"

Video - Transcoding

FFmpeg-backed transcoding actions and admin queue/server inspection.

GET /media/{id}/transcodes

Retrieve stored transcode renditions and current job status for a video.

Auth: Owner/Admin

status values: queued, processing, failed, complete.


POST /media/{id}/transcode

Trigger a transcode job for a video. Defaults to the configured presets when none are supplied.

Auth: Owner/Admin

Body:

Field Type Required Description
presets string[] No Specific presets to run (each must be a known preset key)

DELETE /media/{id}/transcodes

Delete all transcoded files for a video.

Auth: Owner/Admin


GET /transcode/status

Admin overview of the transcode job queue.

Auth: Admin

Parameters:

Parameter Type Default Description
status string all One of queued, processing, failed, complete, all
per_page int 20 Jobs per page (1-100)
page int 1 Page number

GET /transcode/config

Report FFmpeg availability and the configured transcode presets.

Auth: Admin


Video - Chapters & Resume

GET /media/{id}/chapters

List chapter markers for a video.

Auth: User (must be able to read the media)


PUT /media/{id}/chapters

Replace all chapter markers for a video.

Auth: Owner/Admin (chapter-edit permission)

Body:

{
  "chapters": [
    { "time_seconds": 0,   "title": "Introduction" },
    { "time_seconds": 120, "title": "Main Feature", "thumbnail_url": "https://…/chap.jpg" }
  ]
}

Each chapter requires time_seconds (int ≥ 0) and title (1-200 chars); thumbnail_url is optional.


GET /media/{id}/resume

Get the current user's saved resume position for a video.

Auth: User


POST /media/{id}/resume

Save the current playback position. Called periodically by the Pro player.

Auth: User

Body:

Field Type Required Description
position number Yes Seconds into the video (≥ 0)

DELETE /media/{id}/resume

Clear the current user's resume position for a video.

Auth: User


Captions

WebVTT caption retrieval, manual upload, Whisper generation, deletion, and job status.

GET /media/{id}/captions

Return caption metadata (VTT URL, language, provider, word count, duration, generated-at). Returns 404 when no captions exist.

Auth: User (must be able to read the media)


PUT /media/{id}/captions

Upload/replace a caption track by sending raw WebVTT content.

Auth: Owner/Admin

Body:

Field Type Required Description
vtt_content string Yes The full WebVTT document

POST /media/{id}/captions/generate

Queue an OpenAI Whisper transcription job to auto-generate captions.

Auth: Owner/Admin


DELETE /media/{id}/captions

Remove the caption track from a video/audio item.

Auth: Owner/Admin


GET /media/{id}/captions/status

Poll the status of a caption generation job.

Auth: User (must be able to read the media)


Analytics

Public play-event ingestion plus owner/admin analytics surfaces.

POST /media/{id}/events

Record a video play/heatmap event. Public and rate-limited inside the service; mirrors the free /media/{id}/view route.

Auth: Public (rate-limited)

Body:

Field Type Required Default Description
event_type string Yes - One of the allowed event types
position number Yes - Playhead position in seconds (≥ 0)
duration number No 0 Segment duration in seconds (≥ 0)
session_id string Yes - Player session identifier (1-64 chars)

GET /media/{id}/analytics

Full analytics for a single media item, including the heatmap buckets.

Auth: Owner/Admin

Parameters:

Parameter Type Default Description
bucket_count int 100 Heatmap resolution (10-500 buckets)

GET /analytics/top

Top media by engagement.

Auth: Admin

Parameters:

Parameter Type Default Description
period string 30d One of today, 7d, 30d, all
limit int 10 Number of items (1-100)
sort string engagement One of engagement, plays, completion

GET /analytics/overview

Site-wide analytics summary.

Auth: Admin


Boosts

Spend gamification points to promote a media item's visibility.

GET /boosts

List the current user's boosts.

Auth: User

Parameters:

Parameter Type Default Description
status string "" Filter by boost status
per_page int 20 Items per page
page int 1 Page number

X-WP-Total is returned in the response header.


POST /boosts

Create a boost for a media item. Returns 201 with the new boost_id.

Auth: User

Body:

Field Type Required Default Description
media_id int Yes - Media item to boost
impressions_target int No 500 Target number of impressions

GET /boosts/balance

Return the current user's point balance and the boost cost/limit settings.

Auth: User

Response:

{ "balance": 1200, "cost_per_100": 50, "max_impressions": 5000 }

Streaks

Requires mvs_streaks_enabled.

POST /streaks/buy-freeze

Purchase a streak-freeze token with gamification points. Fails with a 400 if the gamification plugin is inactive or the user lacks enough points.

Auth: User

Response:

{ "freezes": 3, "balance": 1100 }

Challenges

Requires mvs_challenges_enabled. Themed photo challenges with entries, voting, and finalized results.

GET /challenges

List challenges.

Auth: Public

Parameters:

Parameter Type Default Description
status string "" Filter by status
per_page int 20 Items per page (max via mvs_rest_pagination_max, default 100)
page int 1 Page number

POST /challenges

Create a challenge.

Auth: Admin

Body (key fields): title (required), description, theme, cover_media_id, start_date (required), end_date (required), voting_end_date (required), max_entries_per_user (default 1), and XP awards xp_1st / xp_2nd / xp_3rd / xp_participation.


GET /challenges/{id}

Get a single challenge.

Auth: Public


PUT /challenges/{id}

Update a challenge.

Auth: Admin


POST /challenges/{id}/cancel

Cancel a challenge.

Auth: Admin


GET /challenges/{id}/entries

List entries for a challenge.

Auth: Public

Parameters: per_page (default 20), page (default 1), orderby (default votes).


POST /challenges/{id}/entries

Submit an entry to a challenge.

Auth: User

Body:

Field Type Required Description
media_id int Yes Media item to enter

POST /challenges/{id}/entries/{entry_id}/vote

Vote for a challenge entry.

Auth: User


DELETE /challenges/{id}/entries/{entry_id}/vote

Remove the current user's vote from an entry.

Auth: User


GET /challenges/{id}/results

Get finalized results for a challenge.

Auth: Public


Battles

Requires mvs_battles_enabled. 1v1 photo battles.

GET /battles

List battles.

Auth: Public

Parameters: user_id (default 0), status (default ""), per_page (default 20), page (default 1).


POST /battles

Create (challenge a user to) a battle.

Auth: User

Body:

Field Type Required Default Description
opponent_id int Yes - The user being challenged
theme string No "" Optional battle theme

GET /battles/{id}

Get a single battle.

Auth: Public


POST /battles/{id}/accept

Accept a battle challenge.

Auth: User


POST /battles/{id}/decline

Decline a battle challenge.

Auth: User


POST /battles/{id}/submit

Submit a media entry for a battle.

Auth: User

Body:

Field Type Required Description
media_id int Yes Media item to submit

POST /battles/{id}/vote

Cast a vote in a battle.

Auth: User

Body:

Field Type Required Description
voted_for_id int Yes The user (battle side) being voted for

Tournaments

Requires mvs_tournaments_enabled. Single-elimination bracket tournaments.

GET /tournaments

List tournaments.

Auth: Public

Parameters: status (default ""), per_page (default 20), page (default 1).


POST /tournaments

Create a tournament.

Auth: Admin

Body (key fields): title (required), description, theme, cover_media_id, bracket_size (default 8), registration_start (required), registration_end (required), round_duration_hours (default 48), xp_round_win (default 150), xp_tournament_win (default 500).


GET /tournaments/{id}

Get tournament detail.

Auth: Public


POST /tournaments/{id}/register

Register the current user for a tournament.

Auth: User


DELETE /tournaments/{id}/register

Unregister the current user from a tournament.

Auth: User


GET /tournaments/{id}/bracket

Get the tournament bracket.

Auth: Public


GET /tournaments/{id}/participants

Get the tournament participants (display name + avatar only).

Auth: Public


POST /tournaments/{id}/matches/{match_id}/submit

Submit a media entry for a bracket match.

Auth: User

Body:

Field Type Required Description
media_id int Yes Media item to submit for the match

POST /tournaments/{id}/matches/{match_id}/vote

Vote in a bracket match.

Auth: User

Body:

Field Type Required Description
voted_for_id int Yes The participant being voted for

Competitions (read-only roll-up)

GET /competitions/active-summary

A single discovery roll-up combining the active challenge, open tournaments, and the most recent battles. Intentionally public so it can drive landing-page/widget discovery. When the requester is logged in, the response also includes their my_activity.

Auth: Public

Response:

{
  "active_challenge": { "id": 5, "title": "Black and White Week" },
  "open_tournaments": [],
  "recent_battles": []
}

There is no generic /competitions CRUD. Challenges, battles, and tournaments are managed through their own route groups above.


Connectors

Requires mvs_connectors_enabled. OAuth-based import/export connectors for external platforms (Flickr, etc.). Connector IDs are slugs (e.g. flickr).

GET /connectors

List all registered connectors with each one's connection state.

Auth: User


POST /connectors/{id}/connect

Initiate the OAuth flow for a connector.

Auth: User

Body:

Field Type Default Description
use_custom bool false Use user-supplied API credentials instead of plugin defaults
api_key string "" Custom API key (when use_custom is true)
api_secret string "" Custom API secret (when use_custom is true)
return_url string "" Where to redirect after the OAuth callback (defaults to the connectors admin page)

POST /connectors/{id}/disconnect

Revoke and remove the connection.

Auth: User (must be connected to this connector)


GET /connectors/{id}/status

Live validation of the connection.

Auth: User (must be connected)


GET /connectors/{id}/photos

Browse the remote platform's photos.

Auth: User (must be connected)

Parameters:

Parameter Type Default Description
page int 1 Page number
per_page int 20 Photos per page (1-30)
album_id string "" Restrict to a specific remote album/set

GET /connectors/{id}/albums

Browse the remote platform's albums.

Auth: User (must be connected)


POST /connectors/{id}/import

Import selected remote photos into the media library.

Auth: User (must be connected)

Body:

Field Type Required Description
photo_ids string[] Yes Remote photo IDs to import

POST /connectors/{id}/export

Export local media to the remote platform.

Auth: User (must be connected)

Body:

Field Type Required Description
media_ids int[] Yes Local attachment IDs to export

POST /connectors/{id}/sync

Run an incremental delta sync of recently changed items.

Auth: User (must be connected)


Stories (New in 1.9.0)

WhatsApp-style ephemeral stories. Story state is stored as free media meta (is_story / story_expires_at); per-viewer "seen by" receipts live in the Pro table mvs_pro_story_views. Replying to a story reuses the existing free DM routes - there is no separate reply endpoint here. Requires mvs_stories_enabled.

GET /stories

List active stories. Defaults to the viewer's network (people they follow, plus themselves); pass author_id to scope to one profile.

Auth: Public (returns an empty set for logged-out visitors since the default scope needs a viewer)

Parameters:

Parameter Type Default Description
author_id int (viewer's network) Scope to one author's stories
page int 1 Page number
per_page int 50 Items per page (max 100)

Response: array of { media_id, media_type, thumbnail_url, expires_at, viewed, author: { id, name, avatar, profile_url } }. X-WP-Total / X-WP-TotalPages headers carry pagination.


POST /media/{id}/story

Mark a media item as a story.

Auth: Owner/Admin

Body:

Field Type Default Description
duration_hours int 24 Visibility window, 1-168 hours

Response: { media_id, is_story: true, expires_at }


DELETE /media/{id}/story

End a story early. The media itself is untouched - only the story designation is cleared.

Auth: Owner/Admin

Response: { media_id, is_story: false }


POST /stories/{id}/view

Record a view receipt for the current user. The author's own views are never recorded - "seen by" counts the audience, not the owner.

Auth: User (subject to the media's normal privacy check)

Response: { recorded: true }


GET /stories/{id}/viewers

"Seen by" list for a story.

Auth: Owner/Admin

Parameters:

Parameter Type Default Description
page int 1 Page number
per_page int 50 Items per page (max 100)

Response: { viewers: [ { user_id, name, avatar, profile_url, viewed_at } ], total }. X-WP-Total / X-WP-TotalPages headers carry pagination.


App (Mobile App) (New in 1.9.0)

Routes that support the native mobile app: branding/feature-flag delivery, push-device registration, and the app leaderboard. See Mobile App for the feature-level explanation of white-label branding and feed layout.

GET /wp-json/mvs/v1/app/config

The single call a native/headless client makes before theming itself and deciding which feature surfaces to mount. This route ships in the Free plugin (namespace mvs/v1, not mvs-pro/v1); Pro contributes to its response via filters rather than registering its own route.

Auth: Public (__return_true)

Response:

{
  "accent_color": "#7C3AED",
  "logo_url": "https://example.com/wp-content/uploads/2026/07/logo.png",
  "login_bg_url": null,
  "dark_mode_default": false,
  "layout": "instagram",
  "pro_active": true,
  "features": {
    "messaging": true,
    "reactions": true,
    "comments": true,
    "favorites": true,
    "albums": true,
    "collections": true,
    "follows": true,
    "notifications": true,
    "activity": true,
    "battles": false,
    "challenges": true,
    "tournaments": false,
    "boosts": false,
    "streaks": true,
    "video": true,
    "stories": true
  }
}

Site name, description, icon, and auth discovery come from the core WordPress /wp-json/ index, not this route - /app/config only carries what the core index cannot express: branding and feature flags. accent_color, logo_url, login_bg_url, and dark_mode_default are null/false unless Pro's white-label branding settings are configured (#mobile-app-mvs)). layout mirrors the site owner's mvs_pro_feed_layout choice (grid when Pro is inactive). The features map is Free's always-on capabilities plus Pro's toggle-driven flags (battles, challenges, tournaments, boosts, streaks, video, stories) - each Pro flag is only true when its matching admin toggle is on.


POST /mvs-pro/v1/push/register-device

Register (or refresh) the current user's device push token so the app can deliver push notifications.

Auth: User

Body:

Field Type Required Default Description
expo_push_token string Yes - The device's Expo push token
platform string No "" One of "", ios, android, web
device_name string No "" Human-readable device label

Response:

{ "registered": true }

DELETE /mvs-pro/v1/push/register-device

Remove a device push token for the current user (e.g. on logout or app uninstall).

Auth: User

Body:

Field Type Required Description
expo_push_token string Yes The device's Expo push token to remove

Response:

{ "removed": true }

GET /mvs-pro/v1/leaderboard

Paginated leaderboard for the app's gamification screen. Backed by the same LeaderboardService as the pro-leaderboard block, so the app and web show identical rankings. Returns the ranked page plus the current viewer's own rank in a single round trip.

Auth: Public. Public rows are cached (5 minutes by default via mvs_pro_leaderboard_cache_ttl); the viewer's own rank is cached per-user and only computed when logged in.

Parameters:

Parameter Type Default Description
source string reactions One of reactions, media_count, gamification_xp
period string all One of all, 30d, 7d
page int 1 Page number
per_page int 10 Rows per page (1-100)

Response:

{
  "rows": [
    { "rank": 1, "user_id": 5, "display_name": "Jane", "avatar_url": "https://…", "profile_url": "https://…", "score": 342, "metric_label": "reactions" }
  ],
  "total": 128,
  "viewer_rank": 14,
  "viewer_score": 27
}

X-WP-Total and X-WP-TotalPages headers carry pagination. viewer_rank is null when the current user is not logged in or has no score yet.


Admin

POST /admin/gamification-welcome/dismiss

Dismiss the gamification first-run welcome banner (site-wide option).

Auth: Admin (manage_mvs_settings)


Error Responses

Pro endpoints use the same error envelope as the free API:

{
  "code": "mvs_pro_rest_forbidden",
  "message": "You do not have permission to manage WPMediaVerse Pro settings.",
  "data": { "status": 403 }
}

Common Pro error codes:

Code Status Meaning
mvs_pro_rest_forbidden 403 Caller lacks the required Pro capability
mvs_pro_no_captions 404 No captions exist for the requested media
mvs_pro_privacy_update_failed 400 Privacy could not be updated (bad level or write failure)
mvs_gamification_unavailable 400 Gamification plugin not active (streak freeze)
mvs_insufficient_points 400 Not enough gamification points for the requested action

Group conversations

Group messaging routes. The one-to-one conversation routes live in the Free REST API reference; these add the multi-participant layer on top of the same conversation store.

Two permission levels apply:

  • Group admin - the creator, plus anyone promoted to admin. Required to rename a group or change its membership.
  • Participant - any member of the group. Required to leave it.

POST /groups

Create a group conversation.

Auth: Authenticated.

Parameter Type Required Description
participant_ids array of integer yes User IDs to add. The creator is added automatically as group admin.
title string no Group name. Clients usually fall back to a list of participant names when this is empty.

Fires mvs_group_conversation_created.

PUT /groups/{id}

Update a group - in practice, rename it.

Auth: Group admin.

Also accepts PATCH and POST.

POST /groups/{id}/participants

Add a participant.

Auth: Group admin.

Parameter Type Required Description
user_id integer yes The member to add.
role string no admin or member. Defaults to member.

Fires mvs_participant_added.

DELETE /groups/{id}/participants/{user_id}

Remove a participant.

Auth: Group admin.

Fires mvs_participant_removed.

PUT /groups/{id}/participants/{user_id}/role

Promote or demote a participant.

Auth: Group admin.

Parameter Type Required Description
role string yes One of admin, member.

Also accepts PATCH and POST.

POST /groups/{id}/leave

Leave a group you are a participant of.

Auth: Participant.

This is deliberately separate from DELETE /groups/{id}/participants/{user_id}: leaving is something any participant may do to themselves, while removing someone else requires group admin. Do not implement "leave" by having the client call the removal route with its own user ID - that path checks for admin and will return 403 for ordinary members.


Media collections

GET /media/{media_id}/collections

Return which of the caller's collections this media item belongs to.

Auth: Authenticated.

Used to render the checked state of a "save to collection" menu without a separate lookup per collection.

POST /media/{media_id}/collections

Add or remove a media item from one collection.

Auth: Authenticated.

Parameter Type Required Default Description
collection_id integer yes - Target collection.
member boolean no true true adds the item, false removes it.

A single toggle endpoint rather than separate add and delete routes: send member: false to remove.

Hooks & Filters Reference

All WPMediaVerse hooks use the mvs_ prefix. Pro-only hooks require WPMediaVerse Pro to be active and are labeled (Pro). Hooks introduced in version 1.1 are labeled (New in 1.1).


Quick Reference

Hook Name Type Free/Pro Since
mvs_loaded action Free 1.0
mvs_pro_loaded action Pro 1.0
mvs_ai_providers action Free 1.0
mvs_theme_json filter Free 1.0
mvs_before_media_insert action Free 1.0
mvs_media_uploaded action Free 1.0 (signature extended in 1.2.3)
mvs_before_upload_form action Free 1.0
mvs_before_thumbnail_generation action Free 1.1
mvs_after_thumbnail_generation action Free 1.1
mvs_upload_args filter Free 1.0
mvs_allowed_file_types filter Free 1.1
mvs_max_upload_size filter Free 1.1
mvs_upload_directory filter Free 1.1
mvs_media_metadata filter Free 1.0
mvs_before_content action Free 1.0
mvs_after_content action Free 1.0
mvs_before_template_render action Free 1.1
mvs_after_template_render action Free 1.1
mvs_dashboard_before_content action Free 1.0
mvs_dashboard_tabs action Free 1.0
mvs_dashboard_panels action Free 1.0
mvs_dashboard_after_content action Free 1.0
mvs_locate_template filter Free 1.0
mvs_template_variables filter Free 1.1
mvs_body_classes filter Free 1.1
mvs_reserved_media_paths filter Free 1.0
mvs_before_explore_grid action Free 1.0
mvs_after_explore_grid action Free 1.1
mvs_feed_query_args filter Free 1.1
mvs_feed_sort_options filter Free 1.1
mvs_media_response filter Free 1.0
mvs_album_response filter Free 1.1
mvs_collection_response filter Free 1.1
mvs_rest_pagination_max filter Free 1.1
mvs_explore_query_args filter Free 1.0
mvs_parent_route filter Free 1.0
mvs_reaction_added action Free 1.0
mvs_reaction_removed action Free 1.0
mvs_favorite_added action Free 1.0
mvs_share_recorded action Free 1.0
mvs_media_group_assigned action Free 1.0
mvs_album_cover_set action Free 1.0
mvs_album_items_added action Free 1.0
mvs_tag_term_count filter Free 1.0
mvs_user_badge_html filter Free 1.0
mvs_reaction_toggled action Free 1.0
mvs_favorite_toggled action Free 1.0
mvs_comment_created action Free 1.0
mvs_mentions_created action Free 1.0
mvs_user_followed action Free 1.0
mvs_user_unfollowed action Free 1.0
mvs_media_shared action Free 1.0
mvs_report_submitted action Free 1.0
mvs_user_blocked action Free 1.0
mvs_tags_merged action Free 1.0
mvs_activity_types filter Free 1.0
mvs_activity_max_media filter Free 1.0
mvs_notification_created action Free 1.1
mvs_should_send_notification filter Free 1.1
mvs_notification_data filter Free 1.1
mvs_notification_types filter Free 1.0
mvs_notification_message filter Free 1.0
mvs_conversation_created action Free 1.0
mvs_message_sent action Free 1.0
mvs_message_request_accepted action Free 1.0
mvs_message_deleted action Free 1.0
mvs_message_reaction_added action Free 1.0
mvs_voice_message_sent action Free 1.0
mvs_conversation_read action Free 1.0
mvs_can_send_message filter Free 1.0
mvs_dm_access_level filter Free 1.0
mvs_dm_message_rate_limit filter Free 1.0
mvs_dm_convo_rate_limit filter Free 1.0
mvs_message_max_length filter Free 1.0
mvs_message_types filter Free 1.0
mvs_dm_allowed_file_types filter Free 1.0
mvs_messaging_poll_intervals filter Free 1.0
mvs_messaging_transport filter Free 1.0
mvs_show_online_status filter Free 1.0
mvs_dm_max_upload_size filter Free 1.0
mvs_settings_sidebar_after action Free 1.0
mvs_settings_before_save action Free 1.1
mvs_settings_render_{renderer} action Free 1.0
mvs_dashboard_widgets action Free 1.1
mvs_settings_sections filter Free 1.0
mvs_settings_group_labels filter Free 1.0
mvs_hide_submenu_slugs filter Free 1.0
mvs_moderation_tabs filter Free 1.0
mvs_stats_tabs filter Free 1.0
mvs_comment_edit_window filter Free 1.1
mvs_should_render_chat_panel filter Free 1.2
mvs_page_id_{slot} filter Free 1.2
mvs_user_data_purged action Free 1.2
mvs_media_flagged action Free 1.0
mvs_moderation_changed action Free 1.0
mvs_should_ai_analyze filter Free 1.1
mvs_ai_result filter Free 1.1
mvs_ai_moderation_result filter Free 1.1
mvs_openai_api_key filter Free 1.0
mvs_media_deleted action Free 1.0
mvs_storage_driver filter Free 1.0
mvs_watermark_enabled filter Free 1.0
mvs_watermark_stamp_file filter Free 1.0
mvs_watermark_font_path filter Pro 1.0
mvs_cloud_thumbnail_url filter Free 1.3.0
mvs_cloudops_allow_non_public_to_cloud filter Free 1.3.0
mvs_filename_strategy filter Free 1.3.0
mvs_thumbnail_sizes filter Free 1.3.0
mvs_thumbnail_size_resolved filter Free 1.3.0
mvs_can_repair_thumb filter Free 1.2.3
mvs_repair_media_thumb filter Free 1.2.3
mvs_watermark_font_path filter Pro 1.0
mvs_webhook_sslverify filter Free 1.3.0
mvs_profile_updated action Free 1.0
mvs_avatar_uploaded action Free 1.0
mvs_avatar_deleted action Free 1.0
mvs_user_display_name filter Free 1.0
mvs_user_profile_url filter Free 1.0
mvs_profile_data filter Free 1.0
mvs_profile_update_fields filter Free 1.0
mvs_avatar_allowed_types filter Free 1.0
mvs_avatar_max_size filter Free 1.0
mvs_access_rule_created action Free 1.0
mvs_access_rule_deleted action Free 1.0
mvs_access_granted action Free 1.0
mvs_access_revoked action Free 1.0
mvs_story_created action Pro 1.0 (moved from Free in 1.9.0)
mvs_story_expired action Pro 1.0 (moved from Free in 1.9.0)
mvs_privacy_can_view filter Free 1.0
mvs_buddynext_active filter Free 1.0
mvs_pro_transcode_complete action Pro 1.0
mvs_pro_captions_generated action Pro 1.0
mvs_pro_transcode_presets filter Pro 1.1
mvs_pro_poster_frame filter Pro 1.1
mvs_pro_analytics_recorded action Pro 1.1
mvs_pro_analytics_event_data filter Pro 1.1
mvs_pro_analytics_summary filter Pro 1.1
mvs_layout_assets action Pro 1.0
mvs_before_layout_render action Pro 1.1
mvs_active_layout filter Pro 1.0
mvs_layout_modes filter Pro 1.0
mvs_layout_template_map filter Pro 1.0
mvs_layout_config filter Pro 1.1
mvs_pro_credits_added action Pro 1.0
mvs_pro_woo_package_assigned action Pro 1.0
mvs_pro_woo_package_reverted action Pro 1.0
mvs_pro_memberpress_package_assigned action Pro 1.0
mvs_pro_memberpress_package_reverted action Pro 1.0
mvs_pro_pmpro_package_assigned action Pro 1.0
mvs_pro_pmpro_package_reverted action Pro 1.0
mvs_quota_render_mapping_fields action Pro 1.0
mvs_quota_save_mapping action Pro 1.0
mvs_pro_before_quota_check filter Pro 1.1
mvs_pro_quota_source filter Pro 1.1
mvs_challenge_created action Pro 1.0
mvs_challenge_entry_submitted action Pro 1.0
mvs_challenge_finalized action Pro 1.0
mvs_battle_created action Pro 1.0
mvs_battle_accepted action Pro 1.0
mvs_battle_resolved action Pro 1.0
mvs_tournament_created action Pro 1.0
mvs_tournament_started action Pro 1.0
mvs_tournament_match_resolved action Pro 1.0
mvs_tournament_finalized action Pro 1.0
mvs_autopilot_challenge_created action Pro 1.0
mvs_autopilot_create_failed action Pro 1.0
mvs_autopilot_no_theme_available action Pro 1.0
mvs_autopilot_pool_reset action Pro 1.0
mvs_streak_milestone action Pro 1.0
mvs_challenge_winner_named action Pro 1.2.3
mvs_challenge_activated action Pro 1.5.0
mvs_challenge_voting_started action Pro 1.5.0
mvs_battle_cancelled action Pro 1.5.0
mvs_tournament_cancelled action Pro 1.5.0
mvs_tournament_updated action Pro 1.5.0
mvs_competition_status_changed action Pro 1.5.0
mvs_competitions_tick_ran action Pro 1.5.0
mvs_activate_scheduled_challenges action Pro 1.5.0
mvs_close_challenge_entries action Pro 1.5.0
mvs_finalize_expired_challenges action Pro 1.5.0
mvs_start_registered_tournaments action Pro 1.5.0
mvs_resolve_expired_matches action Pro 1.5.0
mvs_pro_leaderboard_xp_rows filter Pro 1.2.0
mvs_challenge_email_created_subject filter Pro 1.5.0
mvs_challenge_email_created_body filter Pro 1.5.0
mvs_challenge_email_entry_subject filter Pro 1.5.0
mvs_challenge_email_entry_body filter Pro 1.5.0
mvs_challenge_email_winner_subject filter Pro 1.5.0
mvs_challenge_email_winner_body filter Pro 1.5.0
mvs_challenge_email_participant_subject filter Pro 1.5.0
mvs_challenge_email_participant_body filter Pro 1.5.0
mvs_connectors filter Pro 1.5.0
mvs_media_imported action Pro 1.5.0
mvs_media_exported action Pro 1.5.0
mvs_optimize_image filter Free 1.3.0
mvs_optimize_jpeg_quality filter Free 1.3.0
mvs_webp_quality filter Free 1.3.0
mvs_avif_quality filter Free 1.3.0
mvs_ffmpeg_binary filter Free 1.3.0
mvs_default_video_poster_url filter Free 1.3.0
mvs_media_privacy_changed action Free 1.3.0
mvs_serve_public_cloud_direct filter Free 1.4.0
mvs_public_cloud_thumbnail_url filter Free 1.4.0
mvs_public_cloud_file_url filter Free 1.4.0
mvs_broadcast_thumbnail_ttl filter Free 1.5.0
mvs_stable_public_urls filter Free 1.7.0
mvs_public_media_max_age filter Free 1.7.0
mvs_public_local_thumbnail_url filter Free 1.7.0
mvs_public_local_file_url filter Free 1.7.0
mvs_suppress_bp_comment_notification filter Free 2.0.0
mvs_ai_moderation_terms filter Free 1.8.0
mvs_default_thumbnail_style filter Free 1.8.0
mvs_grid_thumb_size_key filter Free 1.8.0
mvs_storage_repair_enabled filter Free 1.8.0
mvs_strip_dead_bp_links filter Free 1.7.1
mvs_dead_bp_link_patterns filter Free 1.7.1
mvs_dm_denial_message filter Free 1.8.0
mvs_dm_denial_reason filter Free 1.8.0
mvs_collections_enabled filter Free 1.8.0
mvs_app_config_features filter Free 1.9.0
mvs_app_config_branding filter Free 1.9.0
mvs_app_config_layout filter Free 1.9.0
mvs_app_interests_cache_ttl filter Free 1.9.0
mvs_suggestions_cache_ttl filter Free 1.9.0
mvs_pro_collections_manage_url filter Pro 1.8.0
mvs_pro_compete_points_url filter Pro 1.8.0

Table of Contents

  1. Plugin Lifecycle
  2. Upload Pipeline
  3. Template System
  4. Explore & Feed
  5. REST API
  6. Social & Engagement
  7. Notifications
  8. Direct Messages
  9. Admin & Settings
  10. AI & Moderation
  11. Storage & Files
  12. User Profiles
  13. Access & Privacy
  14. BuddyPress Integration
  15. Video Processing (Pro)
  16. Analytics (Pro)
  17. Layout System (Pro)
  18. Quota System (Pro)
  19. Competitions (Pro)
  20. Connectors (Pro)
  21. Common Recipes

1. Plugin Lifecycle

mvs_loaded

Fires after the free plugin is fully initialized and the DI container is ready. Use this instead of plugins_loaded when you need access to WPMediaVerse services.

Parameters: none

/**
 * Bootstrap a third-party integration after MVS is ready.
 *
 * @since 1.0
 */
add_action( 'mvs_loaded', function() {
    // Safe to resolve services from the container here.
} );

mvs_pro_loaded (Pro)

Fires after WPMediaVerse Pro is fully initialized.

Parameters: none

/**
 * Run Pro-specific setup after the Pro plugin is ready.
 *
 * @since 1.0
 */
add_action( 'mvs_pro_loaded', function() {
    // Pro services are available here.
} );

mvs_ai_providers

Fires during plugin init to allow registering custom AI providers with the AI service.

Parameters:

Parameter Type Description
$ai_service AIService The AI service instance
/**
 * Register a custom AI provider.
 *
 * @since 1.0
 *
 * @param AIService $ai_service The AI service instance.
 */
add_action( 'mvs_ai_providers', function( $ai_service ) {
    $ai_service->register_provider( new MyCustomAIProvider() );
} );

Additional Lifecycle Filters

Filter Description Parameters Returns
mvs_theme_json Filter theme.json data passed to the frontend JS bundle $data (array) array

2. Upload Pipeline

mvs_media_uploaded

Fires after a new media post is created, stored, and indexed. This is the primary hook for post-upload processing - use it for gamification, activity feeds, external pipelines, and analytics.

Parameters:

Parameter Type Description
$media_id int The new mvs_media post ID
$file_data array File data. Keys: mime, file_path, file_url, file_size, file_type, file_hash, media_type, privacy, user_id, is_first (1.2.3+)
$user_id int Uploader user ID (1.2.3+)
$media_type string Resolved type: photo, video, audio, document (1.2.3+)

Backward compatibility: Listeners registered with accepted_args=1 or =2 continue to work unchanged - the new positional args are appended.

/**
 * Run post-upload side effects, e.g. push to an external pipeline or analytics.
 *
 * @since 1.2.3
 *
 * @param int    $media_id   The new media post ID.
 * @param array  $file_data  File data (now includes user_id and is_first).
 * @param int    $user_id    Uploader user ID.
 * @param string $media_type 'photo' | 'video' | 'audio' | 'document'.
 */
add_action( 'mvs_media_uploaded', function( int $media_id, array $file_data, int $user_id, string $media_type ) {
    my_external_pipeline_notify( $media_id, $user_id );

    if ( ! empty( $file_data['is_first'] ) ) {
        my_flag_first_upload( $user_id );
    }
}, 10, 4 );

Gamification note: XP for uploads is not awarded by calling a function inside this hook. The separate WB Gamification plugin already consumes mvs_media_uploaded (and other mvs_* actions) through its WPMediaVerse integration, and the per-action point value is resolved through the wb_gam_points_for_action filter. To change upload XP, filter wb_gam_points_for_action rather than calling an award function here.


mvs_upload_args

Filters the upload arguments before file processing. Return a WP_Error to reject the upload.

Parameters:

Parameter Type Description
$args array Keys: mime, media_type, file_size, file_name
$user_id int Uploading user ID

Returns: array|WP_Error

/**
 * Reject uploads over 10 MB for subscribers.
 *
 * @since 1.0
 *
 * @param array $args    Upload arguments.
 * @param int   $user_id Uploading user ID.
 * @return array|WP_Error
 */
add_filter( 'mvs_upload_args', function( array $args, int $user_id ) {
    if ( user_can( $user_id, 'subscriber' ) && $args['file_size'] > 10 * MB_IN_BYTES ) {
        return new WP_Error( 'quota_exceeded', __( 'Upload limit exceeded for your plan.', 'wpmediaverse' ) );
    }
    return $args;
}, 10, 2 );

mvs_before_thumbnail_generation (New in 1.1)

Fires before WordPress multi_resize runs for a newly uploaded image. Use this to add or modify the sizes array.

Parameters:

Parameter Type Description
$media_id int Media post ID
$file_path string Absolute path to the uploaded file
$sizes array Size definitions passed to multi_resize
/**
 * Add a custom 800×600 thumbnail size before generation.
 *
 * @since 1.1
 *
 * @param int    $media_id  Media post ID.
 * @param string $file_path Absolute file path.
 * @param array  $sizes     Size definitions.
 */
add_action( 'mvs_before_thumbnail_generation', function( int $media_id, string $file_path, array $sizes ) {
    // Log or modify $sizes via reference if the hook passes by reference,
    // otherwise use mvs_after_thumbnail_generation to inspect results.
}, 10, 3 );

mvs_after_thumbnail_generation (New in 1.1)

Fires after all thumbnails are generated and stored in media meta.

Parameters:

Parameter Type Description
$media_id int Media post ID
$generated array Map of size slug to generated file data
$file_path string Absolute path to the source file
/**
 * Push newly generated thumbnails to an external CDN.
 *
 * @since 1.1
 *
 * @param int    $media_id  Media post ID.
 * @param array  $generated Generated thumbnail map.
 * @param string $file_path Source file path.
 */
add_action( 'mvs_after_thumbnail_generation', function( int $media_id, array $generated, string $file_path ) {
    foreach ( $generated as $size => $data ) {
        my_cdn_push( $data['path'] );
    }
}, 10, 3 );

Additional Upload Filters

Filter Description Parameters Since
mvs_before_media_insert Fires before the mvs_media post is created none 1.0
mvs_before_upload_form Fires before the upload form HTML renders none 1.0
mvs_allowed_file_types Filter allowed MIME types array $types (array) 1.1
mvs_max_upload_size Filter max upload size in bytes $max_size (int), $user_id (int) 1.1
mvs_upload_directory Filter upload subdirectory path $subdir (string), $user_id (int), $media_type (string) 1.1
mvs_media_metadata Filter extracted metadata before storage $metadata (array), $file_path (string), $media_id (int) 1.0

3. Template System

mvs_locate_template

Filters the resolved template file path. Use this to serve templates from a custom theme directory.

Parameters:

Parameter Type Description
$template string Resolved absolute file path
$template_name string Template filename (e.g., media-single.php)
$template_path string Subdirectory within the template directory

Returns: string

/**
 * Load MVS templates from the active theme's /mvs/ subfolder.
 *
 * @since 1.0
 *
 * @param string $template      Resolved template path.
 * @param string $template_name Template filename.
 * @return string
 */
add_filter( 'mvs_locate_template', function( string $template, string $template_name ) {
    $custom = get_stylesheet_directory() . '/mvs/' . $template_name;
    return file_exists( $custom ) ? $custom : $template;
}, 10, 2 );

mvs_dashboard_tabs

Fires inside the dashboard shortcode to allow registering custom tabs.

Parameters: none

/**
 * Register a custom "Collections" tab on the user dashboard.
 *
 * @since 1.0
 */
add_action( 'mvs_dashboard_tabs', function() {
    echo '<button class="mvs-tab" data-panel="collections">' . esc_html__( 'Collections', 'my-plugin' ) . '</button>';
} );

mvs_dashboard_panels

Fires inside the dashboard shortcode to allow registering custom panel content.

Parameters: none

/**
 * Render content for the custom "Collections" panel.
 *
 * @since 1.0
 */
add_action( 'mvs_dashboard_panels', function() {
    echo '<div id="mvs-panel-collections" class="mvs-panel">';
    // Panel content here.
    echo '</div>';
} );

Additional Template Hooks

Hook Type Description Parameters Since
mvs_before_content action Before main template content in full-page templates none 1.0
mvs_after_content action After main template content none 1.0
mvs_before_template_render action Before a template part renders $template_name, $args 1.1
mvs_after_template_render action After a template part renders $template_name, $args 1.1
mvs_dashboard_before_content action Before dashboard shortcode content none 1.0
mvs_dashboard_after_content action After dashboard shortcode content none 1.0
mvs_template_variables filter Filter template variables before render $args (array), $template_name (string) 1.1
mvs_body_classes filter Filter MVS body CSS classes $classes (array) 1.1
mvs_reserved_media_paths filter Filter reserved URL paths under /media/ $paths (array) 1.0

4. Explore & Feed

mvs_feed_query_args (New in 1.1)

Filters the query arguments used to fetch the media feed. Use this to add custom ordering, meta queries, or taxonomy filters.

Parameters:

Parameter Type Description
$query_args array WP_Query arguments for the feed
$request WP_REST_Request The incoming REST request

Returns: array

/**
 * Add a custom meta query to the media feed.
 *
 * @since 1.1
 *
 * @param array           $query_args Feed query arguments.
 * @param WP_REST_Request $request    Incoming REST request.
 * @return array
 */
add_filter( 'mvs_feed_query_args', function( array $query_args, $request ) {
    $query_args['meta_query'][] = [
        'key'     => '_featured',
        'value'   => '1',
        'compare' => '=',
    ];
    return $query_args;
}, 10, 2 );

mvs_feed_sort_options (New in 1.1)

Filters the available sort options shown in the explore feed UI.

Parameters:

Parameter Type Description
$options array Associative array of slug => label

Returns: array

/**
 * Add a "Most Commented" sort option to the explore feed.
 *
 * @since 1.1
 *
 * @param array $options Existing sort options.
 * @return array
 */
add_filter( 'mvs_feed_sort_options', function( array $options ) {
    $options['most_commented'] = __( 'Most Commented', 'my-plugin' );
    return $options;
} );

Additional Explore Hooks

Hook Type Description Parameters Since
mvs_before_explore_grid action Before the explore grid renders none 1.0
mvs_after_explore_grid action After the explore grid renders none 1.1

5. REST API

mvs_media_response

Filters the REST API response array for a single media item. Use this to add or remove fields before the response is sent.

Parameters:

Parameter Type Description
$data array REST response data array
$media_id int Media post ID

Returns: array

/**
 * Append custom post meta to the media REST response.
 *
 * @since 1.0
 *
 * @param array $data     REST response data.
 * @param int   $media_id Media post ID.
 * @return array
 */
add_filter( 'mvs_media_response', function( array $data, int $media_id ) {
    $data['location'] = get_post_meta( $media_id, '_location', true );
    return $data;
}, 10, 2 );

Additional REST Filters

Filter Description Parameters Default Since
mvs_album_response Filter album REST response $data (array), $album_id (int) Raw album data 1.1
mvs_collection_response Filter collection REST response $data (array), $collection_id (int) Raw collection data 1.1
mvs_rest_pagination_max Max per_page for media feed endpoint $maximum (int) 100 1.1
mvs_explore_query_args Filter the WP_Query args used by the /media/ explore template $query_args (array), $profile (WP_User|null) Template query 1.0
mvs_parent_route Filter the resolved parent route slug for a template context $parent (string), $context (string), $args (array) Resolved slug 1.0
mvs_rest_require_auth Community privacy gate: return true to require authentication for the gated REST namespaces (BuddyNext turns this on when the host community is private) $require (bool), $request (WP_REST_Request) false 2.2
mvs_rest_can_access When mvs_rest_require_auth is on, decides whether the current request may pass; blocked requests get 401 mvs_community_private $allowed (bool), $request (WP_REST_Request) is_user_logged_in() 2.2
mvs_rest_gated_route_prefixes Route prefixes the community privacy gate covers. Pro appends /mvs-pro/v1/ so its public reads (e.g. tournament brackets) are gated too; sites can add further prefixes $prefixes (string[]), $request (WP_REST_Request) array( '/mvs/v1/' ) 2.2

Native App Config (1.9.0)

These filters back the GET /app/config response consumed by the native mobile app (see REST API Reference). Free seeds sane defaults; Pro (or a white-label add-on) supplies its own values.

mvs_app_config_features

Filters the features boolean map returned by GET /app/config. Free seeds its always-on capabilities plus the messaging gate (derived from mvs_dm_access); Pro filters in its own toggles (battles, challenges, tournaments, boosts, streaks, video, stories, …).

Parameters:

Parameter Type Description
$features array<string,bool> Default: messaging, reactions, comments, favorites, albums, collections, follows, notifications, activity (all true except messaging, which follows mvs_dm_access)

Returns: array<string,bool>

add_filter( 'mvs_app_config_features', function( array $features ) : array {
    $features['my_custom_feature'] = true;
    return $features;
} );

mvs_app_config_branding

Filters the white-label branding array returned by GET /app/config. No Free setting drives this - Pro's Mobile App Branding settings (accent color, logo, login background, dark-mode default) populate it. Do not add site name/description/icon here; those come from the core /wp-json/ index.

Parameters:

Parameter Type Description
$branding array Default: accent_color, logo_url, login_bg_url all null; dark_mode_default false

Returns: array

add_filter( 'mvs_app_config_branding', function( array $branding ) : array {
    $branding['accent_color'] = '#1a73e8';
    return $branding;
} );

mvs_app_config_layout

Filters the feed layout slug (grid|instagram|pinterest|flickr|dribbble) reported to the app so it presents media the way the site owner configured Explore. Pro supplies it from its mvs_pro_feed_layout setting; Free always defaults to grid.

Parameters:

Parameter Type Description
$layout string Default 'grid'

Returns: string

add_filter( 'mvs_app_config_layout', static fn() => 'grid' );

mvs_app_interests_cache_ttl

Filters the cache TTL (seconds) for the interest-list transient behind GET /app/interests. 0 disables caching.

Parameters:

Parameter Type Description
$ttl int Default HOUR_IN_SECONDS

Returns: int

add_filter( 'mvs_app_interests_cache_ttl', static fn() => 6 * HOUR_IN_SECONDS );

mvs_suggestions_cache_ttl

Filters the cache TTL (seconds) for the suggested-creators candidate pool behind GET /users/suggested. 0 disables caching.

Parameters:

Parameter Type Description
$ttl int Default HOUR_IN_SECONDS

Returns: int

add_filter( 'mvs_suggestions_cache_ttl', static fn() => 15 * MINUTE_IN_SECONDS );

mvs_pro_collections_manage_url (Pro)

Filters the URL the frontend "Save to collection" picker links to for "View your collections" (defaults to the My Media dashboard's Collections tab). Lets a site that places the dashboard elsewhere repoint the link.

Parameters:

Parameter Type Description
$url string Default: permalink of the page at slug my-media + #collections, or '' if that page doesn't exist

Returns: string

add_filter( 'mvs_pro_collections_manage_url', function( string $url ) : string {
    return home_url( '/my-photos/#collections' );
} );

mvs_pro_compete_points_url (Pro)

Filters the URL the Compete hub's points-balance chip links to (normally the WB Gamification rewards hub page). Returning an empty string degrades the chip to a non-linked span.

Parameters:

Parameter Type Description
$url string Default: permalink of the wb_gam_hub_page_id option's page, or '' if not configured

Returns: string

add_filter( 'mvs_pro_compete_points_url', function( string $url ) : string {
    return home_url( '/rewards/' );
} );

6. Social & Engagement

mvs_comment_created

Fires when a comment is posted on a media item.

Parameters:

Parameter Type Description
$media_id int Media post ID
$user_id int Commenting user ID
$comment_id int New comment ID
$content string Comment text
$source string Source context: web, bp_activity, etc.
/**
 * Send a Slack alert when a comment is posted.
 *
 * @since 1.0
 *
 * @param int    $media_id   Media post ID.
 * @param int    $user_id    Commenter user ID.
 * @param int    $comment_id New comment ID.
 * @param string $content    Comment text.
 * @param string $source     Comment source.
 */
add_action( 'mvs_comment_created', function( int $media_id, int $user_id, int $comment_id, string $content, string $source ) {
    my_slack_notify( "New comment on media #{$media_id}" );
}, 10, 5 );

mvs_reaction_toggled

Fires when a user adds, changes, or removes a reaction on a media item.

Parameters:

Parameter Type Description
$media_id int Media post ID
$user_id int User who changed the reaction
$reaction_type string|null Reaction slug (e.g., love) or null if removed
$action string added, changed, or removed
/**
 * Award points when a reaction is added.
 *
 * @since 1.0
 *
 * @param int         $media_id      Media post ID.
 * @param int         $user_id       User ID.
 * @param string|null $reaction_type Reaction slug or null.
 * @param string      $action        add, change, or remove.
 */
add_action( 'mvs_reaction_toggled', function( int $media_id, int $user_id, $reaction_type, string $action ) {
    if ( 'added' === $action ) {
        my_award_points( get_post_field( 'post_author', $media_id ), 1 );
    }
}, 10, 4 );

mvs_user_followed

Fires when a follow relationship is created.

Parameters:

Parameter Type Description
$follower_id int User who followed
$following_id int User who was followed
/**
 * Send a "new follower" notification.
 *
 * @since 1.0
 *
 * @param int $follower_id  User who followed.
 * @param int $following_id User who was followed.
 */
add_action( 'mvs_user_followed', function( int $follower_id, int $following_id ) {
    my_send_follower_notification( $following_id, $follower_id );
}, 10, 2 );

mvs_collections_enabled (New in 1.8.0)

Gates whether the lightbox and single-media page render a separate "Save to collection" control next to the favorite heart. Favoriting (a one-tap like) and saving to a named collection are deliberately separate actions in the UI; there is no bundled Free collections-management backend, so this defaults to false and stays hidden until a collections backend (e.g. WPMediaVerse Pro) enables it.

Parameters:

Parameter Type Description
$enabled bool Whether to render the Save control. Default false

Returns: bool

/**
 * Enable the "Save to collection" control from a custom collections backend.
 *
 * @since 1.8.0
 *
 * @param bool $enabled Whether the Save control renders. Default false.
 * @return bool
 */
add_filter( 'mvs_collections_enabled', '__return_true' );

Frontend companion. In the shared-ui lightbox (Interactivity API store), the Save button calls the actions.lightboxOpenCollections action, which dispatches a mvs-collections-click CustomEvent (bubbling) carrying detail: { mediaId }. A collections backend listens for this event on the document to open its own picker UI - the lightbox itself has no picker.

document.addEventListener( 'mvs-collections-click', ( event ) => {
	myCollectionsPicker.open( event.detail.mediaId );
} );

Additional Social Hooks

Hook Type Description Parameters Since
mvs_reaction_added action Reaction added to media $media_id, $user_id, $reaction_type 1.0
mvs_reaction_removed action Reaction removed from media $media_id, $user_id 1.0
mvs_favorite_added action Favorite added (fires in addition to mvs_favorite_toggled) $media_id, $user_id 1.0
mvs_favorite_toggled action Favorite added or removed $media_id, $user_id, $action (added/removed) 1.0
mvs_share_recorded action Share recorded against a media item $media_id, $user_id 1.0
mvs_media_group_assigned action Media assigned to a BuddyPress group after upload $media_id, $group_id 1.0
mvs_album_cover_set action Album cover image set $album_id, $media_id 1.0
mvs_tag_term_count filter Filter the displayed media count for a tag term $count (int), $term_taxonomy_id (int) 1.0
mvs_user_badge_html filter Inject HTML for a user badge next to an author name (Pro streak/verified badges) $html (string), $user_id (int) 1.0
mvs_mentions_created action @mentions parsed from a comment $media_id, $mentioned_ids, $context, $comment_id 1.0
mvs_user_unfollowed action Follow relationship removed $follower_id, $following_id 1.0
mvs_media_shared action Media shared to external platform $media_id, $user_id, $platform 1.0
mvs_report_submitted action Content report filed $report_id, $reporter_id, $target_type, $target_id, $reason 1.0
mvs_user_blocked action User blocked another user $blocker_id, $blocked_id 1.0
mvs_tags_merged action Two tags merged $source_id, $target_id, $posts 1.0
mvs_activity_types filter Register activity feed types $types (array) 1.0
mvs_activity_max_media filter Max media items rendered into one activity post. Applies to both routes into the feed: the BuddyPress composer, and carousel/gallery uploads (2.3.0), which pass the group key as a second argument $count (int), default 6; $media_group (string, carousel path only) 1.0

7. Notifications

mvs_notification_created (New in 1.1)

Fires after a notification is stored in the database. Use this to push notifications to a custom channel.

Parameters:

Parameter Type Description
$notification_id int New notification record ID
$user_id int Recipient user ID
$type string Notification type slug (e.g., comment, follow)
$actor_id int User who triggered the notification
$media_id int Related media post ID (0 if not media-related)
/**
 * Send an email for comment notifications.
 *
 * @since 1.1
 *
 * @param int    $notification_id Notification record ID.
 * @param int    $user_id         Recipient user ID.
 * @param string $type            Notification type.
 * @param int    $actor_id        Triggering user ID.
 * @param int    $media_id        Related media ID.
 */
add_action( 'mvs_notification_created', function( int $notification_id, int $user_id, string $type, int $actor_id, int $media_id ) {
    if ( 'comment' !== $type ) {
        return;
    }
    $user  = get_userdata( $user_id );
    $actor = get_userdata( $actor_id );
    wp_mail(
        $user->user_email,
        __( 'New comment on your media', 'my-plugin' ),
        sprintf( __( '%s commented on your photo.', 'my-plugin' ), $actor->display_name )
    );
}, 10, 5 );

mvs_should_send_notification (New in 1.1)

Filters whether a notification should be stored and dispatched. Return false to suppress.

Parameters:

Parameter Type Description
$should_send bool Whether to send (default true)
$user_id int Recipient user ID
$type string Notification type slug
$actor_id int Triggering user ID
$media_id int Related media ID

Returns: bool

/**
 * Suppress notifications during a scheduled import.
 *
 * @since 1.1
 *
 * @param bool   $should_send Whether to send the notification.
 * @param int    $user_id     Recipient user ID.
 * @param string $type        Notification type.
 * @param int    $actor_id    Triggering user ID.
 * @param int    $media_id    Related media ID.
 * @return bool
 */
add_filter( 'mvs_should_send_notification', function( bool $should_send, int $user_id, string $type, int $actor_id, int $media_id ) {
    if ( get_transient( 'my_plugin_import_running' ) ) {
        return false;
    }
    return $should_send;
}, 10, 5 );

Additional Notification Filters

Filter Description Parameters Since
mvs_notification_data Filter notification data array before insert $data (array), $type (string) 1.1
mvs_notification_types Filter the list of allowed notification type slugs $types (array) 1.0
mvs_notification_message Override the rendered message label for a notification type. Return a non-null string to replace the default $label (string|null), $type (string), $actor_name (string), $media_title (string) 1.0

8. Direct Messages

mvs_can_send_message

Filters whether a user is allowed to send a DM to a recipient.

Parameters:

Parameter Type Description
$can bool Current permission result
$sender_id int Sending user ID
$recipient_id int Receiving user ID

Returns: bool

/**
 * Block DMs from unverified accounts.
 *
 * @since 1.0
 *
 * @param bool $can          Current permission.
 * @param int  $sender_id    Sender user ID.
 * @param int  $recipient_id Recipient user ID.
 * @return bool
 */
add_filter( 'mvs_can_send_message', function( bool $can, int $sender_id, int $recipient_id ) {
    if ( ! my_is_verified( $sender_id ) ) {
        return false;
    }
    return $can;
}, 10, 3 );

mvs_message_sent

Fires after a DM is stored in the database.

Parameters:

Parameter Type Description
$message_id int New message ID
$conversation_id int Conversation the message belongs to
$sender_id int Sending user ID
$recipient_ids int[] Array of recipient user IDs
/**
 * Push a DM via WebSocket after storage.
 *
 * @since 1.0
 *
 * @param int   $message_id      New message ID.
 * @param int   $conversation_id Conversation ID.
 * @param int   $sender_id       Sender user ID.
 * @param int[] $recipient_ids   Recipient user IDs.
 */
add_action( 'mvs_message_sent', function( int $message_id, int $conversation_id, int $sender_id, array $recipient_ids ) {
    my_websocket_push( $conversation_id, $message_id );
}, 10, 4 );

mvs_dm_denial_reason (New in 1.8.0)

Filters the reason code returned when MessagingService blocks a conversation because the sender is blocked by the recipient. Return one of the codes denial_message() understands (blocked, dms_disabled, mutual_follow_required, account_too_new, rate_limited, not_participant, content_too_long, …) so the human-readable message stays consistent.

Parameters:

Parameter Type Description
$reason string Default 'blocked'
$sender_id int Sending user ID
$recipient_id int Recipient user ID

Returns: string

/**
 * Report a custom denial reason for a bespoke blocking rule.
 *
 * @since 1.8.0
 *
 * @param string $reason       Default 'blocked'.
 * @param int    $sender_id    Sender user ID.
 * @param int    $recipient_id Recipient user ID.
 * @return string
 */
add_filter( 'mvs_dm_denial_reason', function( string $reason, int $sender_id, int $recipient_id ) : string {
    return my_plugin_is_shadow_blocked( $sender_id, $recipient_id ) ? 'blocked' : $reason;
}, 10, 3 );

mvs_dm_denial_message (New in 1.8.0)

Filters the human-readable message shown for a DM denial reason code. Apps consuming mvs/v1 directly may localize from the reason code instead of this string; use this filter for per-site or per-locale message overrides.

Parameters:

Parameter Type Description
$message string Default message for the reason code
$reason string Reason code (see mvs_dm_denial_reason)

Returns: string

/**
 * Customize the "blocked" denial message shown to senders.
 *
 * @since 1.8.0
 *
 * @param string $message Default message.
 * @param string $reason  Reason code.
 * @return string
 */
add_filter( 'mvs_dm_denial_message', function( string $message, string $reason ) : string {
    return 'blocked' === $reason ? __( 'This member is not accepting messages from you.', 'my-plugin' ) : $message;
}, 10, 2 );

Additional DM Hooks

Hook Type Description Parameters Since
mvs_conversation_created action New DM conversation created $conv_id, $user_a, $participants 1.0
mvs_message_request_accepted action Message request accepted $conversation_id, $user_id 1.0
mvs_message_deleted action Message deleted $message_id, $user_id, $is_unsend 1.0
mvs_message_reaction_added action Emoji reaction added to a message $message_id, $user_id, $emoji 1.0
mvs_voice_message_sent action Voice message sent $message_id, $conversation_id, $duration 1.0
mvs_conversation_read action Conversation marked as read $conversation_id, $user_id 1.0
mvs_dm_access_level filter Override DM access check result $access, $sender_id, $recipient_id 1.0
mvs_dm_message_rate_limit filter Max messages/minute per user $limit (int), default 20 1.0
mvs_dm_convo_rate_limit filter Max new conversations/hour $limit (int), default 10 1.0
mvs_message_max_length filter Max message character length $length (int), default 2000 1.0
mvs_message_types filter Allowed message type slugs $types (array), default text, media_share, image, video, audio, voice, file, system 1.0
mvs_dm_allowed_file_types filter Allowed MIME types for DM attachments. Since 2.2.0 the default is media only (image/video/audio) - application/pdf was removed; add it back here per-site to re-allow documents $types (array), default image + video + audio MIME list 1.0
mvs_messaging_poll_intervals filter Polling intervals (ms) for the chat client $intervals (array), keys active/list/background 1.0
mvs_messaging_transport filter Swap the messaging transport object (e.g. WebSocket instead of REST polling) $transport (TransportInterface) 1.0
mvs_show_online_status filter Filter online status visibility $show (bool), $viewer_id, $user_id 1.0
mvs_dm_max_upload_size filter Max DM attachment size in bytes $bytes (int), default 10 * MB_IN_BYTES 1.0

9. Admin & Settings

mvs_settings_before_save (New in 1.1)

Fires before settings are saved to the database. Use this to validate or transform settings values.

Parameters:

Parameter Type Description
$option_page string Settings page slug being saved
/**
 * Log settings saves for auditing.
 *
 * @since 1.1
 *
 * @param string $option_page Settings page slug.
 */
add_action( 'mvs_settings_before_save', function( string $option_page ) {
    error_log( "MVS settings page '{$option_page}' saved by user " . get_current_user_id() );
} );

mvs_dashboard_widgets (New in 1.1)

Fires after the built-in overview page widgets render. Use this to add custom stat cards to the admin overview.

Parameters: none

/**
 * Add a custom stat card to the MVS admin overview.
 *
 * @since 1.1
 */
add_action( 'mvs_dashboard_widgets', function() {
    $count = my_plugin_get_exports_count();
    echo '<div class="mvs-stat-card"><span class="mvs-stat-number">' . esc_html( $count ) . '</span><span class="mvs-stat-label">' . esc_html__( 'Exports', 'my-plugin' ) . '</span></div>';
} );

Additional Admin Hooks

Hook Type Description Parameters Since
mvs_settings_sidebar_after action After settings sidebar renders none 1.0
mvs_settings_render_{renderer} action Renders a custom settings section. The dynamic part is the section's renderer key (e.g. mvs_settings_render_pages) $section (array) 1.0
mvs_settings_sections filter Register settings sidebar sections $sections (array) 1.0
mvs_settings_group_labels filter Override settings group labels $labels (array) 1.0
mvs_hide_submenu_slugs filter Hide admin submenu slugs under the MVS menu $slugs (array) 1.0
mvs_moderation_tabs filter Filter the tabs shown on the moderation queue page $tabs (array) 1.0
mvs_stats_tabs filter Filter the tabs shown on the stats page $tabs (array) 1.0
mvs_comment_edit_window filter Seconds a user can still edit a comment after posting $seconds (int), default 15 * MINUTE_IN_SECONDS 1.1
mvs_should_render_chat_panel filter Whether the floating chat panel renders on the current request $render (bool), $visibility (string, one of everywhere/logged_in/bp_pages) 1.2
mvs_page_id_{slot} filter Override the resolved page ID for a plugin page slot (e.g. mvs_page_id_explore). The dynamic part is the slot slug $page_id (int), $slot (string) 1.2
mvs_user_data_purged action Fires after a user's MVS data is erased (GDPR / account deletion) $user_id (int) 1.2

10. AI & Moderation

mvs_moderation_changed

Fires when a media item's moderation status changes via the REST API or admin UI.

Parameters:

Parameter Type Description
$media_id int Media post ID
$status string New status: approved, rejected, flagged
$old_status string Previous moderation status
$user_id int Moderator user ID (0 if system-triggered)
/**
 * Notify the media owner of a moderation decision.
 *
 * @since 1.0
 *
 * @param int    $media_id   Media post ID.
 * @param string $status     New moderation status.
 * @param string $old_status Previous moderation status.
 * @param int    $user_id    Moderator user ID.
 */
add_action( 'mvs_moderation_changed', function( int $media_id, string $status, string $old_status, int $user_id ) {
    if ( 'rejected' === $status ) {
        $author_id = (int) get_post_field( 'post_author', $media_id );
        my_send_rejection_email( $author_id, $media_id );
    }
}, 10, 4 );

mvs_should_ai_analyze (New in 1.1)

Filters whether the AI pipeline should analyze a given media item. Return false to skip.

Parameters:

Parameter Type Description
$should_analyze bool Whether to analyze (default true)
$media_id int Media post ID

Returns: bool

/**
 * Skip AI analysis for video media.
 *
 * @since 1.1
 *
 * @param bool $should_analyze Whether to run AI analysis.
 * @param int  $media_id       Media post ID.
 * @return bool
 */
add_filter( 'mvs_should_ai_analyze', function( bool $should_analyze, int $media_id ) {
    if ( 'video' === get_post_meta( $media_id, '_mvs_media_type', true ) ) {
        return false;
    }
    return $should_analyze;
}, 10, 2 );

mvs_openai_api_key

Filters the OpenAI API key used for AI moderation and tagging. Use this to supply the key from a secrets manager.

Parameters:

Parameter Type Description
$key string API key from plugin settings

Returns: string

/**
 * Load the OpenAI API key from a wp-config.php constant.
 *
 * @since 1.0
 *
 * @param string $key API key from settings.
 * @return string
 */
add_filter( 'mvs_openai_api_key', function( string $key ) {
    return defined( 'OPENAI_API_KEY' ) ? OPENAI_API_KEY : $key;
} );

mvs_ai_moderation_terms (New in 1.8.0)

Supplies the full list of moderation terms - the enabled built-in categories (nudity, violence, hate, self-harm, drugs, spam) plus the owner's custom flag terms - to AI providers that don't read Free's options directly. Unusually for a filter, Free is the consumer that registers the default callback, not the one that calls apply_filters(): Core\Plugin does add_filter( 'mvs_ai_moderation_terms', array( AIService::class, 'get_moderation_terms' ) ) so any provider (Pro's Claude/Anthropic provider calls apply_filters( 'mvs_ai_moderation_terms', self::CATEGORIES )) gets the site's real moderation criteria back instead of building its own hardcoded prompt.

Parameters:

Parameter Type Description
$terms string[] Default term list passed in by the caller (e.g. a provider's own fallback category constant)

Returns: string[] - Free's registered callback ignores the incoming default and always returns AIService::get_moderation_terms() (enabled categories + mvs_ai_moderation_custom_terms, comma-split, deduped).

/**
 * Read the site's configured AI moderation terms from a custom provider.
 *
 * @since 1.8.0
 *
 * @param string[] $terms Default terms (your provider's own fallback list).
 * @return string[]
 */
$terms = apply_filters( 'mvs_ai_moderation_terms', array( 'nudity', 'violence' ) );

Additional AI & Moderation Hooks

Hook Type Description Parameters Since
mvs_media_flagged action AI flags a media item $media_id, $result (array) 1.0
mvs_ai_result filter Filter combined AI output $output (array), $media_id 1.1
mvs_ai_moderation_result filter Filter moderation result before flagging $result (array), $media_id 1.1

11. Storage & Files

mvs_storage_driver

Resolves the storage driver instance for a given driver slug. This is the registration point for custom drivers: the filter receives the current driver (null until something supplies one) and the configured driver name, and your callback returns a StorageDriverInterface instance only when the name matches your slug - otherwise it returns the incoming $driver unchanged. If no listener returns an instance, StorageService falls back to the built-in LocalDriver. See Custom Storage Drivers for the full contract.

Parameters:

Parameter Type Description
$driver StorageDriverInterface|null The driver resolved so far (null until a listener supplies one)
$name string Configured driver slug (e.g., local, s3, bunnycdn)

Returns: StorageDriverInterface|null

/**
 * Register a custom storage driver for the `my_s3_compatible` slug.
 *
 * @since 1.0
 *
 * @param StorageDriverInterface|null $driver Driver resolved so far.
 * @param string                      $name   Configured driver slug.
 * @return StorageDriverInterface|null
 */
add_filter( 'mvs_storage_driver', function( $driver, string $name ) {
    return 'my_s3_compatible' === $name ? new MyS3CompatibleDriver() : $driver;
}, 10, 2 );

mvs_watermark_stamp_file

Stamps a watermark onto a file in place, replacing the built-in pass entirely. Return true to tell WPMediaVerse the file has already been watermarked and it should not run its own compositor.

This is the extension point for sites that watermark with an external library or a service.

Parameters:

Parameter Type Description
$stamped bool Whether the file has been watermarked. Default false.
$path string Absolute path to the file to stamp.
$mime string Detected MIME type.
$user_id int Owner of the media item.

Returns: bool

/**
 * Watermark with an external library instead of the built-in compositor.
 *
 * @param bool   $stamped Whether the file is already watermarked.
 * @param string $path    Absolute file path.
 * @param string $mime    MIME type.
 * @param int    $user_id Media owner.
 * @return bool
 */
add_filter( 'mvs_watermark_stamp_file', function( bool $stamped, string $path, string $mime, int $user_id ) {
    if ( 'image/jpeg' !== $mime ) {
        return $stamped;
    }
    my_watermarker_stamp( $path );
    return true;
}, 10, 4 );

Two related filters complete the set:

Filter Free/Pro Description Parameters
mvs_watermark_enabled Free Turn watermarking on or off at runtime, overriding the mvs_watermark_enabled option $enabled (bool)
mvs_watermark_font_path Pro Absolute path to the TTF font used for text watermarks $path (string)

Image Optimization (1.3.0)

mvs_optimize_image

Extension point for external optimizers (EWWW, Imagify, Smush, ShortPixel). Fires once per file pass: once for the lossless re-encode, once for each WebP sibling, and once for each AVIF sibling. The filter runs before the built-in pass, so a returning listener can fully replace the result.

Parameters:

Parameter Type Description
$file_path string Absolute path to the file on local disk
$context array Keys: media_id (int), variant (string, e.g. original, original-webp), mime (string), user_id (int)

Returns: string|WP_Error - Return a file path string to replace the result. Return the same $file_path for an in-place edit. Return a WP_Error to log a warning and keep the original.

/**
 * Delegate JPEG optimization to EWWW Image Optimizer.
 *
 * @since 1.3.0
 *
 * @param string $file_path Absolute path to file.
 * @param array  $context   { media_id, variant, mime, user_id }.
 * @return string
 */
add_filter( 'mvs_optimize_image', function( string $file_path, array $context ) {
    if ( 'image/jpeg' !== $context['mime'] ) {
        return $file_path;
    }
    ewww_image_optimizer( $file_path );
    return $file_path;
}, 10, 2 );

mvs_optimize_jpeg_quality

Filters the JPEG re-encode quality used by the built-in lossless pass. Range 0-100. Setting to 100 produces a near-lossless re-encode that still strips EXIF.

Parameters:

Parameter Type Description
$quality int Default 92
$context array Keys: media_id, variant, mime, user_id

Returns: int

add_filter( 'mvs_optimize_jpeg_quality', function( int $quality, array $context ) : int {
    // Use 85 for thumbnails, 92 for originals.
    return str_contains( $context['variant'] ?? '', 'thumb' ) ? 85 : $quality;
}, 10, 2 );

mvs_webp_quality

Filters the WebP encoder quality. Range 0-100.

Parameters:

Parameter Type Description
$quality int Default 82
$context array Keys: media_id, variant, mime, user_id

Returns: int

add_filter( 'mvs_webp_quality', function( int $quality ) : int {
    return 75; // Smaller files, still visually lossless for most photos.
} );

mvs_avif_quality

Filters the AVIF encoder quality. Range 0-100. AVIF encoding is CPU-intensive; only runs when the mvs_generate_avif setting is enabled.

Parameters:

Parameter Type Description
$quality int Default 50
$context array Keys: media_id, variant, mime, user_id

Returns: int

add_filter( 'mvs_avif_quality', function( int $quality ) : int {
    return 40; // Lower = smaller file; AVIF retains quality well below 50.
} );

Video Poster (1.3.0)

mvs_ffmpeg_binary

Filters the ffmpeg binary path used for video poster extraction. The plugin auto-detects common paths (/opt/homebrew/bin/ffmpeg, /usr/local/bin/ffmpeg, /usr/bin/ffmpeg, /opt/ffmpeg/bin/ffmpeg). Use this filter on hosts with a non-standard install location.

Parameters:

Parameter Type Description
$binary string Resolved path or ffmpeg if auto-detect failed

Returns: string

add_filter( 'mvs_ffmpeg_binary', function( string $binary ) : string {
    return '/opt/custom/bin/ffmpeg';
} );

mvs_default_video_poster_url

Filters the fallback poster URL shown when ffmpeg could not generate a frame (e.g. ffmpeg not installed, corrupt video). The URL is used at render time only and is never stored in media meta.

Parameters:

Parameter Type Description
$url string Default: plugin-bundled SVG asset URL

Returns: string

add_filter( 'mvs_default_video_poster_url', function( string $url ) : string {
    return get_stylesheet_directory_uri() . '/assets/video-placeholder.png';
} );

Cloud URL Serving (1.4.0)

mvs_serve_public_cloud_direct

Controls whether public media stored on a cloud driver is served via a direct CDN URL rather than through the plugin's /serve proxy. Default is true (direct). Set to false to force all traffic through the proxy (e.g. for request logging or header injection).

Parameters:

Parameter Type Description
$allowed bool Whether direct cloud serving is permitted. Default true
$media_id int Media ID

Returns: bool

add_filter( 'mvs_serve_public_cloud_direct', function( bool $allowed, int $media_id ) : bool {
    // Force all media through /serve for audit logging.
    return false;
}, 10, 2 );

mvs_public_cloud_thumbnail_url

Filters the direct CDN URL emitted for a cloud-hosted thumbnail. Use this to rewrite the URL to a custom domain (e.g. a CDN pull zone in front of S3) or return an empty string to fall back to the /serve proxy.

Parameters:

Parameter Type Description
$thumb_url string Cloud thumbnail URL
$media_id int Media ID
$size string Size key (e.g. thumb_large, thumb_medium)

Returns: string - Return empty string to force /serve proxy for this thumbnail.

add_filter( 'mvs_public_cloud_thumbnail_url', function( string $thumb_url, int $media_id, string $size ) : string {
    // Replace the raw S3 hostname with a CloudFront distribution.
    return str_replace( 'my-bucket.s3.amazonaws.com', 'cdn.example.com', $thumb_url );
}, 10, 3 );

mvs_public_cloud_file_url

Filters the direct CDN URL for a public media's original file. Companion to mvs_public_cloud_thumbnail_url for full-file reads. Return empty string to fall back to the /serve proxy.

Parameters:

Parameter Type Description
$file_url string Cloud file URL
$media_id int Media ID
$size string Always empty string for this filter (full-file context)

Returns: string

add_filter( 'mvs_public_cloud_file_url', function( string $file_url, int $media_id, string $size ) : string {
    return str_replace( 'my-bucket.s3.amazonaws.com', 'cdn.example.com', $file_url );
}, 10, 3 );

Broadcast Thumbnail Expiry (1.5.0)

mvs_broadcast_thumbnail_ttl

Sets how long, in seconds, a broadcast thumbnail access URL stays valid. These URLs are minted by MediaRepository::get_broadcast_thumbnail_url() for thumbnails embedded in long-lived surfaces such as notification emails and RSS feeds, where the link may be opened long after it was generated. The default is HOUR_IN_SECONDS (3600). The serve-time privacy check still runs on every request, so this filter only controls the link's validity window, not the access decision.

Raise the value on sites that cache those surfaces at the CDN for longer than an hour, but keep it as short as your caching allows.

Parameters:

Parameter Type Description
$ttl int Time-to-live in seconds. Default HOUR_IN_SECONDS (3600)
$media_id int Media ID the thumbnail belongs to
$size string Size key (e.g. thumb_large, thumb_medium)

Returns: int - The TTL in seconds.

add_filter( 'mvs_broadcast_thumbnail_ttl', function( int $ttl, int $media_id, string $size ) : int {
    // Widen to 6 hours on a site that caches notification emails / RSS at the CDN.
    return 6 * HOUR_IN_SECONDS;
}, 10, 3 );

Render-Stable Public URLs (1.7.0)

Public media is served through signed /serve URLs like everything else, but a fresh signature on every page render defeats browser and CDN caching. These four filters, all in SignedUrlService, control the render-stable/cacheable URL behavior for public media only - private/restricted media always keeps its rolling, per-request signature.

mvs_stable_public_urls

Whether public media gets a render-stable (cacheable) signed URL instead of a fresh signature every page load. Default true. The stable URL is bucketed to the current month plus a one-year offset, so it stays constant for about a month, then rotates. Access is still gated by privacy on every request (see serve()), so a long, stable expiry value is safe to cache. Set to false to fall back to the old per-request signature.

Parameters:

Parameter Type Description
$stable bool Whether to mint a render-stable URL. Default true
$media_id int Media ID

Returns: bool

add_filter( 'mvs_stable_public_urls', '__return_false' );

mvs_public_media_max_age

Cache-Control: max-age (seconds) sent by /serve for public media. Return 0 to keep public media on no-store (disable HTTP caching entirely).

Parameters:

Parameter Type Description
$max_age int Default WEEK_IN_SECONDS (604800)
$privacy string Media privacy level (always public when this filter runs)

Returns: int

add_filter( 'mvs_public_media_max_age', function( int $max_age, string $privacy ) : int {
    return DAY_IN_SECONDS; // Shorter cache window for a fast-moving feed.
}, 10, 2 );

mvs_public_local_thumbnail_url / mvs_public_local_file_url

Local-storage escape hatch for public media. By default, public files stored on the local driver still stream through the signed /serve proxy. If you put a reverse proxy or static-file rule in front of wp-content/uploads (e.g. Nginx location block, a CDN pull zone with no cloud driver configured), return a non-empty URL from these filters to bypass /serve and point directly at that static URL. Default '' keeps the existing /serve behavior.

Parameters (mvs_public_local_thumbnail_url):

Parameter Type Description
$url string Default '' (keeps /serve)
$media_id int Media ID
$size string Size key (e.g. thumb_large)
$rel_path string Relative storage path

Parameters (mvs_public_local_file_url):

Parameter Type Description
$url string Default '' (keeps /serve)
$media_id int Media ID
$rel_path string Relative storage path

Returns: string - non-empty to bypass /serve.

add_filter( 'mvs_public_local_file_url', function( string $url, int $media_id, string $rel_path ) : string {
    return 'https://static.example.com/wpmediaverse/' . ltrim( $rel_path, '/' );
}, 10, 3 );

Cloud, Thumbnails & Filenames

mvs_cloudops_allow_non_public_to_cloud

Controls whether a non-public (private/restricted) media item is allowed to be migrated to a cloud driver during a CloudOps migration. Default false - only public media is cloud-eligible, so private files stay on local disk. Return true to opt a specific item in.

Parameters:

Parameter Type Description
$allow bool Whether to allow the move. Default false
$media_id int Media ID
$privacy string Current privacy value
$to string Target driver slug

Returns: bool

add_filter( 'mvs_cloudops_allow_non_public_to_cloud', function( bool $allow, int $media_id, string $privacy, string $to ) : bool {
    // Allow "members" media to go to cloud, keep "private" local.
    return 'members' === $privacy ? true : $allow;
}, 10, 4 );

mvs_default_thumbnail_style

Filters the default grid thumbnail style for sites that have not explicitly saved the mvs_thumbnail_style option. The default flipped from square to original (masonry) in 1.8.0 so Explore and the media-grid show every image at its native aspect ratio instead of a center-cropped square. Use this filter to restore the old uniform-crop default without touching the site's saved option - an explicitly saved option always wins over the filtered default, since register_setting() only runs on admin_init and the frontend relies on this resolved default.

Parameters:

Parameter Type Description
$default string 'original' (masonry) or 'square' (uniform crop). Default 'original'

Returns: string

/**
 * Restore the pre-1.8.0 uniform square-crop grid default.
 *
 * @since 1.8.0
 *
 * @param string $default 'original' or 'square'.
 * @return string
 */
add_filter( 'mvs_default_thumbnail_style', static fn() => 'square' );

mvs_grid_thumb_size_key

Filters the thumbnail size rung (medium or large) served for grid/masonry tiles. 1.8.0 raised the grid's default from medium to large so tiles stay sharp on HiDPI/retina screens (the medium rung visibly upscaled inside larger masonry tiles). Byte-conscious sites can drop back to medium with this filter instead of changing the mvs_thumbnail_size setting site-wide.

Parameters:

Parameter Type Description
$key string Resolved rung, 'medium' or 'large'
$configured string The raw mvs_thumbnail_size setting value

Returns: string - must resolve to 'medium' or 'large'; any other value falls back to 'large'.

/**
 * Serve the smaller thumbnail rung for grid tiles on a bandwidth-constrained site.
 *
 * @since 1.8.0
 *
 * @param string $key        Resolved rung.
 * @param string $configured The mvs_thumbnail_size setting value.
 * @return string
 */
add_filter( 'mvs_grid_thumb_size_key', function( string $key, string $configured ) : string {
    return 'medium';
}, 10, 2 );

Storage Repair (1.8.0)

mvs_storage_repair_enabled

Owner escape hatch for the automatic post-update storage repair pass (Services\StorageRepairService). The repair heals two pre-1.8.0 inconsistencies without deleting or moving anything - absolute file paths left over from a plugin migration (rtMedia / MediaPress / BuddyBoss), and thumbnails stranded on local disk after an older "Migrate all" - copying files into the library and correcting file_path/file_url only. It runs opt-out (default true), in bounded Action Scheduler batches, and is idempotent/resumable. Also gates wp mvs repair-storage (see WP-CLI Commands).

Parameters:

Parameter Type Description
$enabled bool Whether the repair pass may run. Default true

Returns: bool

/**
 * Disable the automatic background storage repair on this site.
 *
 * @since 1.8.0
 *
 * @param bool $enabled Whether the repair pass is enabled. Default true.
 * @return bool
 */
add_filter( 'mvs_storage_repair_enabled', '__return_false' );

mvs_filename_strategy

Filters the stored-filename strategy used for new uploads. The built-in strategies are hashed (default) and original.

Parameters:

Parameter Type Description
$strategy string Resolved strategy slug
$user_id int Uploading user ID

Returns: string

add_filter( 'mvs_filename_strategy', function( string $strategy, int $user_id ) : string {
    return user_can( $user_id, 'manage_options' ) ? 'original' : $strategy;
}, 10, 2 );

Additional Storage Hooks

Hook Type Description Parameters Since
mvs_media_deleted action Media permanently deleted $media_id, $author_id 1.0
mvs_watermark_enabled filter Enable/disable watermark per media item $enabled (bool), $media_id 1.0
mvs_cloud_thumbnail_url filter Override the cloud URL stored for a generated thumbnail size at upload time. Return non-empty to use a custom URL $url (string, empty), $size_name (string), $media_id (int) 1.3.0
mvs_thumbnail_sizes filter Filter the size definitions array used for thumbnail generation $sizes (array) 1.3.0
mvs_thumbnail_size_resolved filter Filter the resolved default thumbnail size key $size (string) 1.3.0
mvs_can_repair_thumb filter Whether the "Repair thumbnail" admin row action is offered for a media item $can (bool), $media_id (int), $file_type (string), $file_path (string) 1.2.3
mvs_repair_media_thumb filter Let Pro / third parties regenerate thumbnails during an admin repair. Return the count of regenerated size-variants $regenerated (int), $media_id (int), $context (array: file_type, file_path) 1.2.3
mvs_watermark_font_path (Pro) filter Path to the TTF font used for text watermarks $path (string, empty), $config (array) 1.0
mvs_webhook_sslverify filter Whether outgoing webhook requests verify SSL (default off on local environments) $verify (bool), $url (string) 1.3.0

12. User Profiles

mvs_user_profile_url

Filters the public profile URL for a user. Override this to point at BuddyPress, a custom route, or any external profile system.

Parameters:

Parameter Type Description
$url string Default MVS profile URL
$user_id int User ID

Returns: string

/**
 * Use the WordPress author URL as the MVS profile URL.
 *
 * @since 1.0
 *
 * @param string $url     Default profile URL.
 * @param int    $user_id User ID.
 * @return string
 */
add_filter( 'mvs_user_profile_url', function( string $url, int $user_id ) {
    return get_author_posts_url( $user_id );
}, 10, 2 );

mvs_user_display_name

Filters a user's display name everywhere MVS renders it. Pro uses this internally to append a streak badge.

Parameters:

Parameter Type Description
$name string Current display name
$user_id int User ID

Returns: string

/**
 * Append a verified checkmark to display names.
 *
 * @since 1.0
 *
 * @param string $name    Current display name.
 * @param int    $user_id User ID.
 * @return string
 */
add_filter( 'mvs_user_display_name', function( string $name, int $user_id ) {
    if ( my_is_verified( $user_id ) ) {
        $name .= ' ✓';
    }
    return $name;
}, 10, 2 );

Additional Profile Hooks

Hook Type Description Parameters Since
mvs_profile_updated action Profile saved $user_id, $fields (array) 1.0
mvs_avatar_uploaded action Avatar uploaded $user_id, $attachment_id 1.0
mvs_avatar_deleted action Avatar removed $user_id 1.0
mvs_profile_data filter Filter profile data in REST response $data (array), $user_id 1.0
mvs_profile_update_fields filter Filter allowed profile update fields $fields (array), $user_id 1.0
mvs_avatar_allowed_types filter Filter allowed avatar MIME types $types (array), $user_id 1.0
mvs_avatar_max_size filter Max avatar file size in bytes $bytes (int), default 2 * MB_IN_BYTES 1.0

13. Access & Privacy

mvs_privacy_can_view

Filters the privacy access check result. Return null to use the built-in check, true to grant, or false to deny.

Parameters:

Parameter Type Description
$result bool|null Current result (null = use default logic)
$media_id int Media post ID
$user_id int Viewing user ID (0 for anonymous)
$privacy string Media privacy level slug

Returns: bool|null

/**
 * Grant access to group members using a custom group check.
 *
 * @since 1.0
 *
 * @param bool|null $result   Current access result.
 * @param int       $media_id Media post ID.
 * @param int       $user_id  Viewing user ID.
 * @param string    $privacy  Privacy level.
 * @return bool|null
 */
add_filter( 'mvs_privacy_can_view', function( $result, int $media_id, int $user_id, string $privacy ) {
    if ( 'group' === $privacy && my_user_in_media_group( $media_id, $user_id ) ) {
        return true;
    }
    return $result;
}, 10, 4 );

mvs_media_privacy_changed

Fires when a media item's privacy level changes. Fires from both MediaRepository::set() (single-field update path) and MediaRepository::update() (bulk update path). Does not fire when a row is first inserted.

Parameters:

Parameter Type Description
$media_id int Media ID
$new_privacy string New privacy value (e.g. public, members, friends, private)
$old_privacy string Previous privacy value
/**
 * Sync BuddyPress activity visibility when a media item is made private.
 *
 * @since 1.3.0
 *
 * @param int    $media_id    Media ID.
 * @param string $new_privacy New privacy value.
 * @param string $old_privacy Previous privacy value.
 */
add_action( 'mvs_media_privacy_changed', function( int $media_id, string $new_privacy, string $old_privacy ) {
    if ( 'public' === $old_privacy && 'public' !== $new_privacy ) {
        // Hide related BuddyPress activity from sitewide stream.
        my_plugin_hide_bp_activity_for_media( $media_id );
    }
}, 10, 3 );

Additional Access Hooks

Hook Type Description Parameters Since
mvs_access_rule_created action Access rule created for a media item $rule_id, $media_id, $rule_type, $rule_value 1.0
mvs_access_rule_deleted action Access rule removed $rule_id, $media_id, $rule_type 1.0
mvs_access_granted action User granted access to restricted media $grant_id, $media_id, $user_id, $source 1.0
mvs_access_revoked action User access revoked $media_id, $user_id 1.0
mvs_album_items_added action Media added to an album $album_id, $actor_id, $media_ids, $added (signature changed in 1.2.3) 1.0

Stories moved to Pro in 1.9.0. mvs_story_created and mvs_story_expired now fire from WPMediaVersePro\Stories\StoryService - see Stories (Pro). The free plugin no longer ships a StoryService; the upload block's "Also share as a story" toggle only renders when the mvs_stories_enabled option is on, which Pro sets when it registers the feature.


14. BuddyPress Integration

These hooks only fire when BuddyPress is active. Use mvs_buddynext_active to detect whether the BuddyNext integration layer is running.

mvs_buddynext_active

Filters whether the BuddyNext integration is considered active. Override this if you need to force or suppress the integration.

Parameters:

Parameter Type Description
$active bool Auto-detected active state

Returns: bool

/**
 * Force BuddyNext integration off in a staging environment.
 *
 * @since 1.0
 *
 * @param bool $active Auto-detected state.
 * @return bool
 */
add_filter( 'mvs_buddynext_active', function( bool $active ) {
    return defined( 'WP_STAGING' ) ? false : $active;
} );

Opt-in gate for cleaning up dead BuddyPress component links (/members/, /groups/, /activity/) from the site's nav menus when BuddyPress is inactive. Off by default - per Coding Rule #17, WPMediaVerse never edits a site owner's authored navigation on its own. Only enable this on a site where you specifically want the plugin to drop menu items that would 404 once BuddyPress is gone. The cleanup never removes an item that resolves to a real published page, and it fully bails when a sibling community plugin (detected via mvs_buddynext_active) owns those routes as live pages.

Parameters:

Parameter Type Description
$enabled bool Whether to run the cleanup. Default false

Returns: bool

/**
 * Opt this site into automatic dead-BP-link menu cleanup.
 *
 * @since 1.7.1
 *
 * @param bool $enabled Whether the cleanup runs. Default false.
 * @return bool
 */
add_filter( 'mvs_strip_dead_bp_links', '__return_true' );

mvs_dead_bp_link_patterns (New in 1.7.1)

Filters the URL fragments treated as "dead" BuddyPress component links when mvs_strip_dead_bp_links is enabled and BuddyPress is inactive. Only consulted when the cleanup above actually runs.

Parameters:

Parameter Type Description
$patterns string[] Default array( '/members/', '/groups/', '/activity/' )

Returns: string[]

/**
 * Also treat a custom /community/ archive as a dead BP-style link.
 *
 * @since 1.7.1
 *
 * @param string[] $patterns Default members/groups/activity archives.
 * @return string[]
 */
add_filter( 'mvs_dead_bp_link_patterns', function( array $patterns ) : array {
    $patterns[] = '/community/';
    return $patterns;
} );

mvs_suppress_bp_comment_notification (New in 2.0.0)

When a media comment is posted from inside a linked BuddyPress activity, BuddyPress already fires its own native "replied to your update" notification for the activity comment. Without this filter, NotificationIntegration also mirrors the MVS media_comment notification, giving the media owner two dropdown entries for one comment. The filter only suppresses the BP-mirrored notification (bp_notifications_add_notification()); the native MVS in-app notification row in mvs_notifications is untouched, so /me/notifications and any REST/app client still see it. Only applies when the comment's media has a linked bp_activity_id that BuddyPress itself will notify on.

Parameters:

Parameter Type Description
$suppress bool Whether to suppress the BP-mirrored notification. Default true
$media_id int Media ID the comment belongs to
$user_id int Recipient (media owner) user ID
$actor_id int Commenter user ID

Returns: bool

/**
 * Restore the old double-notify behavior on a site that wants both.
 *
 * @since 2.0.0
 *
 * @param bool $suppress Whether to suppress the BP mirror. Default true.
 * @param int  $media_id Media ID.
 * @param int  $user_id  Recipient (media owner) user ID.
 * @param int  $actor_id Commenter user ID.
 * @return bool
 */
add_filter( 'mvs_suppress_bp_comment_notification', '__return_false' );

15. Video Processing (Pro)

mvs_pro_transcode_complete (Pro)

Fires when all transcode presets for a video have finished processing.

Parameters:

Parameter Type Description
$media_id int Media post ID
$results array Per-preset result map
$final_status string Overall status: complete, partial, failed
/**
 * Notify the uploader when video transcoding is done.
 *
 * @since 1.0
 *
 * @param int    $media_id     Media post ID.
 * @param array  $results      Per-preset results.
 * @param string $final_status Overall transcode status.
 */
add_action( 'mvs_pro_transcode_complete', function( int $media_id, array $results, string $final_status ) {
    if ( 'complete' === $final_status ) {
        $author_id = (int) get_post_field( 'post_author', $media_id );
        my_send_transcode_ready_email( $author_id, $media_id );
    }
}, 10, 3 );

mvs_pro_transcode_presets (Pro) (New in 1.1)

Filters the transcode quality presets before encoding starts. Use this to add, remove, or modify quality levels.

Parameters:

Parameter Type Description
$presets array Array of preset definitions (slug, bitrate, resolution)
$media_id int Media post ID

Returns: array

/**
 * Remove the 4K preset to save storage costs.
 *
 * @since 1.1
 *
 * @param array $presets  Transcode preset definitions.
 * @param int   $media_id Media post ID.
 * @return array
 */
add_filter( 'mvs_pro_transcode_presets', function( array $presets, int $media_id ) {
    return array_filter( $presets, fn( $p ) => '2160p' !== $p['slug'] );
}, 10, 2 );

Additional Video Hooks

Hook Type Description Parameters Since
mvs_pro_captions_generated action Whisper transcription saved as WebVTT $media_id, $vtt_url 1.0
mvs_pro_poster_frame filter Filter video poster frame URL $poster_url, $media_id, $file_path 1.1

16. Analytics (Pro)

mvs_pro_analytics_event_data (Pro) (New in 1.1)

Filters analytics event data before it is stored. Return false to skip recording the event entirely.

Parameters:

Parameter Type Description
$event_data array Event payload (user_id, timestamp, meta)
$media_id int Media post ID
$event_type string Event type slug (e.g., view, download)

Returns: array|false

/**
 * Strip PII from analytics events before storage.
 *
 * @since 1.1
 *
 * @param array  $event_data Event data payload.
 * @param int    $media_id   Media post ID.
 * @param string $event_type Event type slug.
 * @return array|false
 */
add_filter( 'mvs_pro_analytics_event_data', function( $event_data, int $media_id, string $event_type ) {
    unset( $event_data['ip_address'] );
    return $event_data;
}, 10, 3 );

Additional Analytics Hooks

Hook Type Description Parameters Since
mvs_pro_analytics_recorded action Analytics event stored $media_id, $event_type, $user_id 1.1
mvs_pro_analytics_summary filter Filter dashboard analytics summary $summary (array), $media_id, $date_range 1.1

17. Layout System (Pro)

mvs_active_layout (Pro)

Filters the active layout slug. Use this to switch layouts programmatically (e.g., per page or per user role).

Parameters:

Parameter Type Description
$slug string Active layout slug from the database setting

Returns: string

/**
 * Show the Pinterest layout for mobile visitors.
 *
 * @since 1.0
 *
 * @param string $slug Active layout slug.
 * @return string
 */
add_filter( 'mvs_active_layout', function( string $slug ) {
    return wp_is_mobile() ? 'pinterest' : $slug;
} );

Additional Layout Hooks

Hook Type Description Parameters Since
mvs_layout_assets action After layout CSS/JS is enqueued $layout_instance, $slug 1.0
mvs_before_layout_render action Before layout template loads $layout_slug, $template_name 1.1
mvs_layout_modes filter Register custom layout modes $modes (slug => class) 1.0
mvs_layout_template_map filter Override template file mapping $map (array), $layout_instance 1.0
mvs_layout_config filter Filter layout configuration $config (array), $slug (string) 1.1

18. Quota System (Pro)

mvs_pro_quota_source (Pro) (New in 1.1)

Filters the quota package assigned to a user. Use this to integrate a custom membership or LMS plugin.

Parameters:

Parameter Type Description
$package array|null Package definition or null for the site default
$user_id int User ID

Returns: array|null

/**
 * Assign quota from a custom LMS based on course enrollment.
 *
 * @since 1.1
 *
 * @param array|null $package Quota package definition.
 * @param int        $user_id User ID.
 * @return array|null
 */
add_filter( 'mvs_pro_quota_source', function( $package, int $user_id ) {
    $course_id = my_lms_get_active_course( $user_id );
    if ( $course_id ) {
        return my_lms_get_quota_for_course( $course_id );
    }
    return $package;
}, 10, 2 );

mvs_pro_before_quota_check (Pro) (New in 1.1)

Fires before quota enforcement runs. Return a WP_Error to reject the action before quota is checked.

Parameters:

Parameter Type Description
$args array Quota check args (media_type, count)
$user_id int User ID

Returns: array|WP_Error

/**
 * Block uploads during a scheduled maintenance window.
 *
 * @since 1.1
 *
 * @param array $args    Quota check arguments.
 * @param int   $user_id User ID.
 * @return array|WP_Error
 */
add_filter( 'mvs_pro_before_quota_check', function( $args, int $user_id ) {
    if ( get_option( 'my_plugin_maintenance_mode' ) ) {
        return new WP_Error( 'maintenance', __( 'Uploads are paused for maintenance.', 'my-plugin' ) );
    }
    return $args;
}, 10, 2 );

Additional Quota Hooks

Hook Type Description Parameters Since
mvs_pro_credits_added action Credits added to user quota $user_id, $media_type, $amount, $source 1.0
mvs_pro_woo_package_assigned action WooCommerce order assigns a quota package $user_id, $product_id, $package_id, $order_status 1.0
mvs_pro_woo_package_reverted action WooCommerce order cancelled, package reverted $user_id, $default, $order_status 1.0
mvs_pro_memberpress_package_assigned action MemberPress membership assigns package $user_id, $membership_id, $package_id 1.0
mvs_pro_memberpress_package_reverted action MemberPress membership expired, reverted $user_id, $default 1.0
mvs_pro_pmpro_package_assigned action PMPro level assigns package $user_id, $level_id, $package_id 1.0
mvs_pro_pmpro_package_reverted action PMPro level cancelled, reverted $user_id, $default 1.0
mvs_quota_render_mapping_fields action Admin quota page renders mapping UI none 1.0
mvs_quota_save_mapping action Admin saves quota mapping none 1.0

19. Competitions (Pro)

Challenges

Hook Type Description Parameters Since
mvs_challenge_created action Challenge created $competition_id, $args, $created_by 1.0
mvs_challenge_entry_submitted action User submits an entry $challenge_id, $user_id, $media_id 1.0
mvs_challenge_activated action A scheduled challenge transitions to active $challenge_id 1.5.0
mvs_challenge_voting_started action A challenge closes entries and opens voting $challenge_id 1.5.0
mvs_challenge_winner_named action Fires once per top-3 rank when a challenge is finalized - fires before mvs_challenge_finalized $challenge_id, $user_id, $rank (1, 2, or 3) 1.2.3
mvs_challenge_finalized action Voting ends, winners determined $challenge_id, $results 1.0
mvs_competition_status_changed action Any competition (challenge/battle/tournament) changes status $competition_id, $old_status, $new_status 1.5.0
/**
 * Award scaled XP per rank without parsing the private $results shape.
 *
 * @since 1.2.3
 *
 * @param int $challenge_id Challenge competition ID.
 * @param int $user_id      Winning user ID.
 * @param int $rank         1, 2, or 3.
 */
add_action( 'mvs_challenge_winner_named', function( int $challenge_id, int $user_id, int $rank ) {
    $scale = array( 1 => 500, 2 => 250, 3 => 100 );
    if ( isset( $scale[ $rank ] ) ) {
        my_award_xp( $user_id, $scale[ $rank ] );
    }
}, 10, 3 );

Battles

Hook Type Description Parameters Since
mvs_battle_created action Battle created $competition_id, $challenger_id, $opponent_id 1.0
mvs_battle_accepted action Opponent accepts battle $battle_id, $user_id 1.0
mvs_battle_resolved action Battle voting ends, winner determined $battle_id, $winner_id, $loser_id 1.0
mvs_battle_cancelled action Battle cancelled $battle_id 1.5.0

Tournaments

Hook Type Description Parameters Since
mvs_tournament_created action Tournament created $competition_id, $args, $created_by 1.0
mvs_tournament_started action Registration closes, bracket generated $tournament_id 1.0
mvs_tournament_match_resolved action Single bracket match resolved $match_id, $winner_id 1.0
mvs_tournament_finalized action Tournament ends, champion crowned $competition_id, $champion_id 1.0
mvs_tournament_updated action Tournament settings updated $tournament_id, $args 1.5.0
mvs_tournament_cancelled action Tournament cancelled $tournament_id 1.5.0

Autopilot

Hook Type Description Parameters Since
mvs_autopilot_challenge_created action Autopilot creates a weekly challenge $competition_id, $theme 1.0
mvs_autopilot_create_failed action Autopilot failed to create a challenge $error, $theme 1.0
mvs_autopilot_no_theme_available action All themes in pool have been used none 1.0
mvs_autopilot_pool_reset action Theme pool recycled $pool 1.0

Streaks

Hook Type Description Parameters Since
mvs_streak_milestone action User reaches a streak milestone (7, 30, 100, 365 days) $user_id, $days, $xp_awarded 1.0
/**
 * Send a congratulations email at streak milestones.
 *
 * @since 1.0
 *
 * @param int $user_id    User ID.
 * @param int $days       Streak day count reached.
 * @param int $xp_awarded XP awarded for this milestone.
 */
add_action( 'mvs_streak_milestone', function( int $user_id, int $days, int $xp_awarded ) {
    $user = get_userdata( $user_id );
    wp_mail(
        $user->user_email,
        sprintf( __( 'You reached a %d-day streak!', 'my-plugin' ), $days ),
        sprintf( __( 'Congratulations! You earned %d XP.', 'my-plugin' ), $xp_awarded )
    );
}, 10, 3 );

Competition Scheduler

The competitions cron tick (CompetitionsScheduler) fires these process-stage actions on each run. They take no per-item parameters - listen to them to run side-effects around the lifecycle batch, or call them directly to force a stage. mvs_competitions_tick_ran reports the wall-clock time of the completed tick.

Hook Type Description Parameters Since
mvs_activate_scheduled_challenges action Activate challenges whose start time has passed none 1.5.0
mvs_close_challenge_entries action Close entry submission for challenges that hit the entry deadline none 1.5.0
mvs_finalize_expired_challenges action Finalize challenges whose voting window has ended none 1.5.0
mvs_start_registered_tournaments action Start tournaments whose registration window closed none 1.5.0
mvs_resolve_expired_matches action Resolve battle/tournament matches past their deadline none 1.5.0
mvs_competitions_tick_ran action Fires at the end of every scheduler tick $timestamp (int, time()) 1.5.0

Challenge Email Templates

Each of these filters lets you override the subject or body of a challenge notification email. All are string filters; return the modified string. The trailing parameters give you the context to personalize the copy.

Filter Description Parameters Since
mvs_challenge_email_created_subject Subject of the "challenge created" email $subject (string), $challenge_id (int), $args (array), $created_by (int) 1.5.0
mvs_challenge_email_created_body Body of the "challenge created" email $body (string), $challenge_id (int), $args (array), $created_by (int) 1.5.0
mvs_challenge_email_entry_subject Subject of the "entry received" email $subject (string), $challenge_id (int), $user_id (int), $media_id (int) 1.5.0
mvs_challenge_email_entry_body Body of the "entry received" email $body (string), $challenge_id (int), $user_id (int), $media_id (int) 1.5.0
mvs_challenge_email_winner_subject Subject of the "you won" email $subject (string), $challenge_id (int), $user_id (int), $rank (int) 1.5.0
mvs_challenge_email_winner_body Body of the "you won" email $body (string), $challenge_id (int), $user_id (int), $rank (int) 1.5.0
mvs_challenge_email_participant_subject Subject of the "challenge ended" email to non-winning participants $subject (string), $challenge_id (int), $user_id (int) 1.5.0
mvs_challenge_email_participant_body Body of the "challenge ended" participant email $body (string), $challenge_id (int), $user_id (int) 1.5.0

mvs_pro_leaderboard_xp_rows (Pro)

Supplies leaderboard rows for the xp metric. The Pro leaderboard renderer defers to this filter (it owns no XP store of its own), so a gamification plugin returns the ranked rows here. Return an array of ['user_id' => int, 'score' => int] rows.

Parameters:

Parameter Type Description
$rows array Default empty array
$per_page int Row limit
$window string Time window slug (e.g. all, month, week)

Returns: array

add_filter( 'mvs_pro_leaderboard_xp_rows', function( array $rows, int $per_page, string $window ) : array {
    return my_gamification_top_xp( $per_page, $window ); // [ ['user_id'=>1,'score'=>500], ... ]
}, 10, 3 );

21. Connectors (Pro)

Pro's import/export connectors (e.g. Flickr) register through mvs_connectors and fire import/export actions as media moves in and out.

mvs_connectors

Registers connector instances. Return your connector keyed by its slug.

Parameters:

Parameter Type Description
$connectors array Map of slug => connector instance. Default empty

Returns: array

add_filter( 'mvs_connectors', function( array $connectors ) : array {
    $connectors['my_service'] = new My_Service_Connector();
    return $connectors;
} );

Additional Connector Hooks

Hook Type Description Parameters Since
mvs_media_imported action A media item was imported from a connector $media_id (int), $connector (string, e.g. flickr), $remote_id (string) 1.5.0
mvs_media_exported action A media item was exported to a connector $media_id (int), $connector (string), $remote_id (string) 1.5.0

22. Common Recipes

Recipe 1: Custom upload size per user role

Give editors 50 MB and subscribers 10 MB using mvs_max_upload_size.

/**
 * Set upload limits based on user role.
 *
 * @since 1.1
 *
 * @param int $max_size Default max size in bytes.
 * @param int $user_id  Uploading user ID.
 * @return int
 */
add_filter( 'mvs_max_upload_size', function( int $max_size, int $user_id ) {
    if ( user_can( $user_id, 'edit_others_posts' ) ) {
        return 50 * MB_IN_BYTES;
    }
    if ( user_can( $user_id, 'read' ) ) {
        return 10 * MB_IN_BYTES;
    }
    return $max_size;
}, 10, 2 );

Recipe 2: Add a custom "Most Commented" sort option

Register the sort option with mvs_feed_sort_options and apply the ordering with mvs_feed_query_args.

/**
 * Register the "Most Commented" sort option.
 *
 * @since 1.1
 *
 * @param array $options Existing sort options.
 * @return array
 */
add_filter( 'mvs_feed_sort_options', function( array $options ) {
    $options['most_commented'] = __( 'Most Commented', 'my-plugin' );
    return $options;
} );

/**
 * Apply the "most_commented" ordering to the feed query.
 *
 * @since 1.1
 *
 * @param array           $query_args WP_Query arguments.
 * @param WP_REST_Request $request    Incoming REST request.
 * @return array
 */
add_filter( 'mvs_feed_query_args', function( array $query_args, $request ) {
    if ( 'most_commented' === $request->get_param( 'sort' ) ) {
        $query_args['orderby'] = 'comment_count';
        $query_args['order']   = 'DESC';
    }
    return $query_args;
}, 10, 2 );

Recipe 3: Skip AI analysis for videos

Use mvs_should_ai_analyze to prevent AI processing for video media types.

/**
 * Skip AI analysis for video media.
 *
 * @since 1.1
 *
 * @param bool $should_analyze Whether to run AI analysis.
 * @param int  $media_id       Media post ID.
 * @return bool
 */
add_filter( 'mvs_should_ai_analyze', function( bool $should_analyze, int $media_id ) {
    return 'video' !== get_post_meta( $media_id, '_mvs_media_type', true );
}, 10, 2 );

Recipe 4: Send an email for comment notifications

Use mvs_notification_created to dispatch an email when a comment notification is created.

/**
 * Email the media owner when someone comments on their upload.
 *
 * @since 1.1
 *
 * @param int    $notification_id Notification record ID.
 * @param int    $user_id         Recipient user ID.
 * @param string $type            Notification type slug.
 * @param int    $actor_id        Triggering user ID.
 * @param int    $media_id        Related media post ID.
 */
add_action( 'mvs_notification_created', function( int $notification_id, int $user_id, string $type, int $actor_id, int $media_id ) {
    if ( 'comment' !== $type ) {
        return;
    }

    $recipient = get_userdata( $user_id );
    $actor     = get_userdata( $actor_id );
    $media_url = get_permalink( $media_id );

    wp_mail(
        $recipient->user_email,
        __( 'New comment on your photo', 'my-plugin' ),
        sprintf(
            /* translators: 1: commenter name, 2: media URL */
            __( '%1$s commented on your photo. View it here: %2$s', 'my-plugin' ),
            $actor->display_name,
            $media_url
        )
    );
}, 10, 5 );

Recipe 5: Custom quota from an LMS plugin (Pro)

Use mvs_pro_quota_source to assign quota packages based on LearnDash course enrollment.

/**
 * Map LearnDash course enrollment to MVS quota packages.
 *
 * @since 1.1
 *
 * @param array|null $package Current quota package or null for site default.
 * @param int        $user_id User ID.
 * @return array|null
 */
add_filter( 'mvs_pro_quota_source', function( $package, int $user_id ) {
    // Check if the user is enrolled in the "Pro Creator" course (ID: 123).
    if ( function_exists( 'sfwd_lms_has_access' ) && sfwd_lms_has_access( 123, $user_id ) ) {
        return [
            'photo_limit' => 500,
            'video_limit' => 50,
            'label'       => 'Pro Creator',
        ];
    }
    return $package;
}, 10, 2 );

Recipe 6: Add custom fields to the media REST response

Use mvs_media_response to append custom post meta or computed values to the media API response.

/**
 * Add location and EXIF data to the media REST response.
 *
 * @since 1.0
 *
 * @param array $data     REST response data array.
 * @param int   $media_id Media post ID.
 * @return array
 */
add_filter( 'mvs_media_response', function( array $data, int $media_id ) {
    $data['location']  = get_post_meta( $media_id, '_mvs_location', true );
    $data['camera']    = get_post_meta( $media_id, '_mvs_exif_camera', true );
    $data['focal_len'] = get_post_meta( $media_id, '_mvs_exif_focal_length', true );
    return $data;
}, 10, 2 );

Recipe 7: Add a custom thumbnail size

Use mvs_before_thumbnail_generation to inject an additional size into the generation pipeline.

/**
 * Generate an 800×600 "hero" thumbnail for every upload.
 *
 * Note: Register the size with add_image_size() so WordPress
 * creates it via multi_resize. Hook into upload_dir or the
 * upload pipeline to inject it into MVS-managed sizes.
 *
 * @since 1.1
 */
add_action( 'init', function() {
    add_image_size( 'mvs-hero', 800, 600, true );
} );

/**
 * Log when MVS thumbnail generation begins.
 *
 * @since 1.1
 *
 * @param int    $media_id  Media post ID.
 * @param string $file_path Absolute path to uploaded file.
 * @param array  $sizes     Size definitions passed to multi_resize.
 */
add_action( 'mvs_before_thumbnail_generation', function( int $media_id, string $file_path, array $sizes ) {
    // Inspect $sizes; the mvs-hero size is included automatically
    // once registered with add_image_size().
    error_log( 'Generating thumbnails for media #' . $media_id );
}, 10, 3 );

Recipe 8: Redirect profile URLs to BuddyPress

Use mvs_user_profile_url to point all MVS profile links at the BuddyPress member profile.

/**
 * Use the BuddyPress member profile URL for all MVS profile links.
 *
 * @since 1.0
 *
 * @param string $url     Default MVS profile URL.
 * @param int    $user_id User ID.
 * @return string
 */
add_filter( 'mvs_user_profile_url', function( string $url, int $user_id ) {
    if ( function_exists( 'bp_core_get_user_domain' ) ) {
        return bp_core_get_user_domain( $user_id );
    }
    return $url;
}, 10, 2 );

23. Additional Hooks Reference

Every remaining hook fired by Free and Pro, grouped by area. Descriptions come from each hook's own docblock at the call site.

Hooks marked (Pro) are fired by WPMediaVerse Pro and never run when only Free is active.

Account deletion and member data

Hook Type Arguments Description
mvs_account_deletion_cancelled action $user_id Fires when a member cancels their pending account deletion.
mvs_account_deletion_executing action $user_id Fires immediately before a member's account is destroyed.
mvs_account_deletion_grace_days filter self::DEFAULT_GRACE_DAYS Filter the account-deletion grace period, in days. Return 0 to delete immediately on request.
mvs_account_deletion_password_required filter true, $user->ID Filter whether account deletion requires the account password.
mvs_account_deletion_requested action $user_id, $when Fires when a member schedules their own account deletion.
mvs_member_erase_map filter $map Filter the tables erased when a member is removed. The seam Pro uses to register its own member-bearing tables, so Pro data is covered by export, erasure, purge and verification without Pro reimplementing any of them.
mvs_member_purged action $user_id Fires after a member has been purged from the mapped tables.
mvs_member_retain_map filter $map Filter the tables retained (anonymised) when a member is erased.
mvs_member_suspension_changed action $user_id, $suspended Fires when a member's suspension state changes.
mvs_profile_privacy_levels filter $levels, $author_id, $viewer_id Filters the privacy levels a viewer may see in a single-author profile listing. Restore the pre-1.6.0 "everything except private" discoverability: add_filter( 'mvs_profile_privacy_levels', function () { return array( 'public', 'members', 'loggedin', 'friends', 'group', 'custom' ); } );

Native app, auth and REST gating

Hook Type Arguments Description
mvs_app_config_legal filter $legal Filter the legal/safety URLs handed to native clients. A site with no privacy policy set returns null for it, and the client falls back to its own.
mvs_app_connect_bridge filter $info Filter the resolved app-connect bridge for this site. Tests use it to simulate the BuddyNext-active path; a site with an unusual auth topology can point the app at its own door.
mvs_app_connect_schemes filter array( AppAuthorizeAccess::app_scheme() Filter the custom URL schemes the MediaVerse app-connect flow may deliver a credential to. Every scheme here can RECEIVE an Application Password - add a scheme only for an app you ship, never a wildcard.
mvs_app_credential_issued action $user->ID, $app_id, $app_name Fires after a member exchanges their password for an app credential. The credential itself is deliberately NOT passed.
mvs_app_page_ids filter $ids ) Filters the pages that render with the plugin's full-bleed app template. A site owner can add a page (e.g.
mvs_app_password_login_enabled filter $on Filter whether members may exchange a WordPress password for an Application Password.
mvs_app_scheme filter self::DEFAULT_APP_SCHEME Filter the mobile app's deep-link scheme.
mvs_app_template filter $resolved, $post_id Filters the fallback app-page template path.
mvs_rest_gate_enabled filter true A per-site kill switch for the REST write gate. These plugins run on thousands of sites and the gate turns some previously-allowed writes into 403s; an owner who hits an unforeseen interaction needs a way out that is not a downgrade. Since 2.1.0.
mvs_rest_gate_denied action $route, $method, $actor, $reason Fires when the write gate denies a request.
mvs_rest_gate_map filter $map Filter the REST write-gate route map. The seam Pro uses to classify its own routes, so Pro never edits a Free file.
mvs_rest_gate_resolve_targets filter array(), $resolver, $rule, $request Resolve targets for a custom gate resolver. Lets Pro (and third parties) supply resolvers for their own routes without touching this file.
mvs_rest_gate_unresolved action $route, $method, $actor See the call site.
mvs_rest_timestamp_keys filter array( 'created_at', 'updated_at', 'date', 'added_at', 'last_activity_at', 'last_read_at', Filters the response keys that receive an ISO-8601 <key>_gmt sibling. Pro / add-ons extend this to cover their own UTC timestamp fields.
mvs_user_is_suspended filter $suspended, $actor Filter whether a member is suspended and barred from every write. The seam for third-party moderation/suspension plugins.

Upload and media pipeline

Hook Type Arguments Description
mvs_apply_exif_orientation filter true, $file_path, $mime Filters whether EXIF orientation is applied on upload. Escape hatch for sites that already normalise orientation upstream (some CDNs and phone-upload apps do) and would otherwise pay for the re-encode twice. Since 2.3.0.
mvs_filename_strategy_upgrade_default filter self::DEFAULT_FRESH Filter the default filename strategy applied when a site has not explicitly chosen one. Defaults to 'hashed' since 1.6.0.
mvs_hold_uploads_for_moderation filter false, $user_id Filter: hold ALL new uploads for manual moderation before they go live. Default false - members publish immediately (the engagement-first default for this community platform; the only standing limit on a member is their Pro storage/upload quota).
mvs_media_files_orphaned action $media_id, $orphaned_files Fires with every relative file path owned by a media item that is about to be torn down, so a cleanup listener can delete the bytes from disk and cloud asynchronously.
mvs_media_replaced action $media_id, $file_data, get_current_user_id(), $media_type Fires after a file replacement has been fully processed. Use this hook (not mvs_media_uploaded) for replace-specific reactions such as re-queuing transcoding or re-generating captions.
mvs_watermark_stamp_file filter false, $path, $mime, $user_id Stamp the admin watermark into bytes a member is publishing right now. THE RULE, stated once: stamp what the member publishes now; never re-process what is already in the library.

Albums and collections

Hook Type Arguments Description
mvs_album_deleted action $post_id Fires after an album's custom-table rows are cleaned on permanent delete.
mvs_album_inherit_privacy filter true, $album_id, $media_ids Filters whether media added to an album inherits the album's privacy. A member who creates a Private album and uploads into it expects the contents to be private.
mvs_collection_deleted action $post_id Fires when a collection is permanently deleted.
mvs_collection_media_ids filter array_map( 'absint', (array) $media_ids ), $collection_id, (int) $atts['per_page'] See the call site.

Messaging

Hook Type Arguments Description
mvs_dm_unarchive_on_activity filter true, $conversation_id, $sender_id ) ) { $wpdb->update( $part_table, array( 'is_archived' See the call site.
mvs_group_conversation_created action $conv_id, $creator_id, $all_ids, $opts Fires when a group conversation is created.
mvs_message_content_check filter true, $content, $sender_id, $conversation_id See the call site.
mvs_participant_added action $conversation_id, $user_id, $role Fires when a participant is added to a conversation.
mvs_participant_removed action $conversation_id, $user_id Fires when a participant is removed from a conversation.

Templates and theming

Hook Type Arguments Description
mvs_frontend_presence_keep_handles filter array( 'mvs-rest' See the call site.
mvs_login_url filter $url, $redirect Filters the login URL MediaVerse links to across every surface.
mvs_media_alt_text filter $alt, $media_id Filter the resolved alt text for a media image.
mvs_media_single_actions action $mvs_media_id, $mvs_author_id, $mvs_is_owner Owner / add-on actions for the single-media social bar. The canonical media view every feed layout links to, so it is the one place an add-on can surface a per-media owner action (e.g.
mvs_no_sidebar_templates filter $candidates Filters the page-template slugs treated as "no sidebar" for app pages.
mvs_registration_url filter $url, $redirect Filters the registration URL MediaVerse links to.
mvs_seo_title_separator filter '-' Point active SEO plugins at a virtual route's real title + canonical. These routes emit a custom query var (mvs_media_archive / mvs_profile_user / mvs_edit_profile), so WP mis-reads the main query as the blog home and Yoast / Rank Math print the Posts page's title + canonical (e.g.
mvs_settings_render_ action . $section['renderer'], $section Fires to render custom settings section content.
mvs_single_media_redirect filter '', (int) $media['media_id'], (string) $slug Let a host redirect single-media URLs somewhere else instead of rendering the standalone page. BuddyNext uses this to send /media/{slug}/ to the activity the media was posted in, so media lives in the community feed rather than as a separate public page.
mvs_suppress_frontend_ui filter $suppressed Whether MediaVerse must NOT paint its own front-end UI on this request. The single source of truth for the frontend-presence policy.

Serving, URLs and caching

Hook Type Arguments Description
mvs_serve_expired_public_urls filter true, $media_id See the call site.
mvs_user_avatar_url filter isset( $args['url'] ) ? (string) $args['url'] : '', $user_id, $size MV-scoped avatar seam. Because every get_avatar()/get_avatar_url() call passes through pre_get_avatar_data, hooking this one filter lets BuddyNext (or any integration) override the avatar image for a user everywhere MediaVerse renders it - templates, blocks, and REST payloads alike - without touc...
mvs_viewer_thumbnail_ttl filter HOUR_IN_SECONDS, $media_id, $viewer_id, $size Filter the viewer-aware thumbnail TTL (seconds). Default 1 hour; the /serve endpoint re-checks privacy per request, so this is only a cache horizon, not a credential lifetime.
mvs_viewer_url_ttl filter HOUR_IN_SECONDS, $media_id, $viewer_id Filter the viewer-aware full-file URL TTL (seconds). Default 1 hour; the /serve endpoint re-checks privacy per request, so this is only a cache horizon, not a credential lifetime.

Social, moderation and access rules

Hook Type Arguments Description
mvs_explore_tag_cloud_limit filter 20 How many tags the Explore tag cloud requests. A community with a handful of curated tags and one with hundreds want different numbers, so this is not hardcoded. Clamped to 1-200, the range the /tags/cloud endpoint itself accepts. Honoured by both the Free Explore template and the Pro layout partial. Since 2.3.0.
mvs_access_rule_types_ui filter $rule_types Filter the access-rule types offered in the rule-builder UIs. Pro hooks here to add monetization / code-grant rule types.
mvs_ai_cost_per_call filter (float) get_option( 'mvs_ai_cost_per_call', 0.01 ), $provider_id Filter the estimated per-call AI cost used for budget tracking.
mvs_comment_duplicate_window filter 60, $media_id, $user_id Filters the duplicate-comment window, in seconds. Wide enough to absorb a double-click or a retry on a slow connection, short enough that deliberately repeating yourself later still works.
mvs_feed_media_ids filter $int_ids, $request Filter the final list of media IDs returned by the feed query. Allows Pro to reorder results (e.g.
mvs_reports_enabled filter $enabled See the call site.

BuddyPress integration

Hook Type Arguments Description
mvs_activity_max_tags filter 15 See the call site.
mvs_activity_media_ids filter $ids, $activity_id, $activity Filter the resolved media IDs for an activity, before linkage rows are written. Composer / group-post integrations should hook here to surface MVS media IDs they've collected from the form, instead of forcing this service to regex-parse saved content.
mvs_object_media_set action $object_type, $object_id, $media_ids Fires after an object's media linkage has been (re)written.

Pro

Hook Type Arguments Description
mvs_boosts_store_enqueued (Pro) action - See the call site.
mvs_pro_captions_reaped (Pro) action $media_id Fires when the reaper fails a stalled captions job.
mvs_pro_compete_summary_cache_ttl (Pro) filter MINUTE_IN_SECONDS Compete summary cache TTL in seconds. Set to 0 to disable caching.
mvs_pro_inject_compete_nav (Pro) filter false ) ) { add_filter( 'wp_nav_menu_items', array( self::class, 'inject_compete_nav_link' See the call site.
mvs_pro_leaderboard_cache_ttl (Pro) filter 5 * MINUTE_IN_SECONDS Leaderboard cache TTL in seconds. Set to 0 to disable caching.
mvs_pro_quota_default_applies_to_unassigned (Pro) filter false, $user_id The default package is assigned to NEW users at registration and is NOT applied retroactively to users without an explicit assignment, so marking or changing the default never silently re-quotas existing members (the reported bug). Unassigned = unlimited: quota is a soft blocker for selling premi...
mvs_pro_quota_package_unassigned (Pro) action $user_id, $source Fires after a lapsed subscription clears a user's quota package assignment.
mvs_pro_quota_revert_to_default_on_end (Pro) filter false, $user_id, $source ) ) { $default = $this->get_default_package( Filters whether a lapsed subscription reverts the user to the default package (pre-1.6.0 behaviour) instead of clearing the assignment.
mvs_pro_quota_widget_visible (Pro) filter $visible, $summary Filters whether the quota usage widget renders for the current user.

Other

Hook Type Arguments Description
mvs_media_privacy_clamped_by_album action $mid, $media_privacy, $effective, $album_id Fires when an item's privacy is tightened by its album.

Template Overrides

Endpoints and hooks marked (Pro) require WPMediaVerse Pro.

WPMediaVerse uses a template loading system that checks your active theme before falling back to plugin templates. This lets you fully customize media page layouts without modifying the plugin.

How It Works

The TemplateLoader class calls locate_template() to check these locations in order:

  1. Child theme: wp-content/themes/child-theme/wpmediaverse/template-name.php
  2. Parent theme: wp-content/themes/parent-theme/wpmediaverse/template-name.php
  3. Plugin default: wp-content/plugins/wpmediaverse/templates/template-name.php

Creating a Theme Override

Create a wpmediaverse/ directory inside your theme and copy the template file you want to modify:

wp-content/themes/your-theme/
└── wpmediaverse/
    ├── media-single.php       # Single media item page
    ├── album.php              # Single album page
    ├── collection.php         # Single collection page
    ├── explore.php            # Media archive / explore page
    └── profile-edit.php       # Profile edit page

Available Templates

File Used For
media-single.php Single mvs_media post page
album.php Single mvs_album post page
collection.php Single mvs_collection post page
explore.php mvs_media and mvs_album archive, taxonomy archives, and /media/@username/ profile pages
profile-edit.php /media/edit-profile/ endpoint

Available Partials

Template partials are located in templates/partials/ and loaded with TemplateLoader::get_template():

WPMediaVerse\Core\TemplateLoader::get_template( 'partials/media-card.php', array(
    'media_id' => $post->ID,
) );

Using TemplateLoader in Custom Code

use WPMediaVerse\Core\TemplateLoader;

// Load a template with data.
TemplateLoader::get_template( 'media-single.php', array(
    'media_id' => 123,
    'show_reactions' => true,
) );

// Just locate the path (without loading).
$path = TemplateLoader::locate( 'explore.php' );

Getting Media URLs in a Template (MediaUrl)

When you write a custom template you almost always need a media URL - a thumbnail for a grid card, or the full file for a lightbox. Do not hand-build these URLs from wp_upload_dir() and do not call SignedUrlService directly. A raw upload path breaks the moment a site enables cloud storage or marks media private, and calling the signing service directly means re-implementing the privacy gate.

Since 1.5.0 the read-side facade WPMediaVerse\Core\MediaUrl is the single entry point. It resolves the active storage driver, runs the privacy check, and returns either a signed /serve URL or a direct CDN URL - whichever is correct for that media's privacy and the current driver.

use WPMediaVerse\Core\MediaUrl;

// Thumbnail URL for the current viewer (size: large | medium | thumb).
$thumb = MediaUrl::thumb( $media_id, 'large' );

// Full original file URL for the current viewer.
$full = MediaUrl::file( $media_id );

if ( $thumb ) {
    printf(
        '<img src="%s" alt="%s" loading="lazy" />',
        esc_url( $thumb ),
        esc_attr( get_the_title( $media_id ) )
    );
}

Both methods return an empty string when the service isn't ready (very early bootstrap) or when the viewer's identity is rejected by the privacy gate - always guard the return value before printing, as shown above.

Public static methods

namespace WPMediaVerse\Core;

final class MediaUrl {

    // Signed /serve URL for a thumb variant. $size: large|medium|thumb.
    // $ttl 0 = service default. $user_id null = current user; 0 = broadcast surface.
    // $skip_privacy is forced to false when the resolved user id is 0.
    public static function thumb(
        int $media_id,
        string $size = 'large',
        int $ttl = 0,
        ?int $user_id = null,
        bool $skip_privacy = true
    ): string;

    // Signed /serve URL for the full original file.
    public static function file( int $media_id, ?int $user_id = null ): string;

    // The meta key holding a variant's stored URL.
    // e.g. ('large') -> 'thumb_large'; ('large','webp') -> 'thumb_large_webp'.
    public static function variant_meta_key(
        string $size,
        string $format = VariantSpec::FORMAT_PRIMARY
    ): string;

    // The meta key holding a variant's driver-agnostic relative path
    // (the URL key with a `_path` suffix). e.g. ('large') -> 'thumb_large_path'.
    public static function variant_path_meta_key(
        string $size,
        string $format = VariantSpec::FORMAT_PRIMARY
    ): string;
}

The two *_meta_key() helpers are the single source of truth for the size+format → meta-key mapping. Use them instead of hardcoding 'thumb_large_webp'-style strings when you need to read variant meta yourself:

use WPMediaVerse\Core\MediaUrl;
use WPMediaVerse\Services\VariantSpec;

$webp_key = MediaUrl::variant_meta_key( 'large', VariantSpec::FORMAT_WEBP ); // 'thumb_large_webp'
$webp_url = get_post_meta( $media_id, $webp_key, true );

Note: TemplateHelpers::get_thumb_url() is now a one-line delegate to MediaUrl::thumb(), so existing templates that already use it keep working unchanged - MediaUrl is simply the canonical name to reach for in new code.

Pro Layout Templates (Pro)

When a Pro feed layout (Instagram, Pinterest, Flickr, or Dribbble) is active (mvs_pro_feed_layout setting), Frontend\Layouts\LayoutManager hooks the same mvs_locate_template filter chain described above to swap in the layout's own explore.php / user-profile.php replacements. Because it hooks the same filter, a child-theme override you already placed under wpmediaverse/ wins automatically - LayoutManager::override_template() checks whether the resolved path already lives inside the active theme before forcing the layout file, so wp-content/themes/your-theme/wpmediaverse/explore.php (or user-profile.php) is honored over any layout's version, exactly like Free templates.

Free template name Overridden by (per layout) Layout template directory
explore.php feed.php templates/layouts/{instagram,pinterest,flickr,dribbble}/
user-profile.php profile.php templates/layouts/{instagram,pinterest,flickr,dribbble}/

Each layout also loads a body partial (feed-body.php) and further partials (e.g. Instagram's partials/stories-bar.php, partials/feed-card.php) that are outside the mvs_locate_template filter's $template_name map. These use a separate, path-based lookup (LayoutManager::theme_or_plugin()) that checks the SAME wpmediaverse/ theme directory, just keyed by the file's path relative to templates/ instead of by template name:

wp-content/themes/your-theme/
└── wpmediaverse/
    └── layouts/
        └── instagram/
            ├── feed-body.php
            └── partials/
                ├── stories-bar.php
                └── feed-card.php

Ship an override at that path (mirroring wp-content/plugins/wpmediaverse-pro/templates/layouts/{slug}/...) and it is read in preference to the plugin's copy - no filter needed.

Third-party hooks for customizing a layout without a template override: mvs_layout_modes (register additional layout modes), mvs_active_layout (force the active layout slug), mvs_layout_template_map (remap which layout file backs each free template name), mvs_layout_config (filter a layout's config array), mvs_before_layout_render (fires before a layout template loads). See Hooks & Filters Reference.

Compete-Page Templates (Pro)

The gamification frontend pages (GamificationTemplateLoader) are theme-overridable through the SAME WPMediaVerse\Core\TemplateLoader::locate() call the free plugin uses - so the wpmediaverse/ child-theme directory convention applies here too, with no separate lookup path:

Route Query var Template file
/media/battles/ mvs_battles_page battles.php
/media/challenges/ mvs_challenges_page challenges.php
/media/tournaments/ mvs_tournaments_page tournaments.php
/compete/ mvs_compete_page compete-hub.php
wp-content/themes/your-theme/
└── wpmediaverse/
    ├── battles.php
    ├── challenges.php
    ├── tournaments.php
    └── compete-hub.php

Each page 404s if its backing feature toggle (mvs_battles_enabled, mvs_challenges_enabled, mvs_tournaments_enabled) is off - Compete Hub (/compete/) 404s only when none of the three are enabled. Added in 1.9.0.

Filtering the Template Path

You can override any template path using the mvs_locate_template filter:

add_filter( 'mvs_locate_template', function( string $template, string $name, string $path ) {
    // Use a completely different directory for all WPMediaVerse templates.
    $override = WP_CONTENT_DIR . '/my-media-templates/' . $name;
    return file_exists( $override ) ? $override : $template;
}, 10, 3 );

BuddyX Theme Integration

WPMediaVerse adds the mvs-page and no-sidebar CSS body classes to all WPMediaVerse pages. The BuddyX theme (and any theme that handles these classes) renders these pages full-width without a sidebar.

Pages that receive these classes:

  • Single mvs_media, mvs_album, mvs_collection posts
  • mvs_media and mvs_album archives
  • mvs_tag and mvs_category taxonomy pages
  • /media/edit-profile/ endpoint
  • /media/@username/ profile pages
  • Any page whose ID matches an mvs_page_* option (e.g., the page containing [mvs_dashboard])

Shortcode Context in Block Templates

When a shortcode renders a block template (via Shortcodes::render_block_template()), the variable $mvs_shortcode_context is set to true. Block render.php files should check this variable before calling get_block_wrapper_attributes(), which causes a PHP warning outside a block context:

if ( empty( $mvs_shortcode_context ) ) {
    $wrapper_attrs = get_block_wrapper_attributes();
}

Custom Storage Drivers

Endpoints and hooks marked (Pro) require WPMediaVerse Pro.

WPMediaVerse uses a pluggable storage driver system. The free plugin ships with a Local driver (stores files in the WordPress uploads directory). WPMediaVerse Pro adds Amazon S3 and BunnyCDN drivers. You can implement your own driver by implementing the StorageDriverInterface.

StorageDriverInterface

namespace WPMediaVerse\Services;

interface StorageDriverInterface {

    /**
     * Store a file.
     *
     * @param string $source_path  Absolute local path of the temporary file.
     * @param string $dest_path    Relative destination path (e.g., "2025/03/photo.jpg").
     * @return bool True on success.
     */
    public function store( string $source_path, string $dest_path ): bool;

    /**
     * Delete a file.
     *
     * @param string $path Relative path.
     * @return bool True on success.
     */
    public function delete( string $path ): bool;

    /**
     * Get the public URL for a file.
     *
     * @param string $path Relative path.
     * @return string Full URL.
     */
    public function url( string $path ): string;

    /**
     * Check if a file exists.
     *
     * @param string $path Relative path.
     * @return bool
     */
    public function exists( string $path ): bool;

    /**
     * Get the absolute filesystem path for a stored file.
     *
     * @since 1.1.0
     *
     * @param string $path Relative path.
     * @return string Absolute file path.
     */
    public function get_full_path( string $path ): string;

    /**
     * Download a stored file to a local destination path.
     *
     * REQUIRED for two flows:
     *   1. The `wp mvs migrate-storage` CLI command - copies media between
     *      drivers without losing files.
     *   2. Thumbnail generation in cloud mode - images stored on S3/BunnyCDN
     *      must be pulled to a local temp file before `wp_get_image_editor()`
     *      can resize them.
     *
     * A local driver returns true immediately if `$path` and `$local_dest`
     * resolve to the same file (no-op). Cloud drivers stream the remote object
     * into `$local_dest`. Return false on any download failure (the caller is
     * responsible for retries / cleanup of partial writes).
     *
     * @since 1.2.2
     *
     * @param string $path       Relative path of the source file.
     * @param string $local_dest Absolute local filesystem path to write to.
     * @return bool True on success.
     */
    public function download( string $path, string $local_dest ): bool;
}

All six methods are required. A driver that does not implement every method will fail PHP's interface contract at load time.

Implementing a Custom Driver

use WPMediaVerse\Services\StorageDriverInterface;

class MyS3CompatibleDriver implements StorageDriverInterface {

    private string $bucket;
    private string $endpoint;

    public function __construct() {
        $this->bucket   = get_option( 'my_storage_bucket' );
        $this->endpoint = get_option( 'my_storage_endpoint' );
    }

    public function store( string $source_path, string $dest_path ): bool {
        // Upload $source_path to $this->bucket/$dest_path.
        // Return true on success.
    }

    public function delete( string $path ): bool {
        // Delete $this->bucket/$path.
    }

    public function url( string $path ): string {
        return $this->endpoint . '/' . $this->bucket . '/' . $path;
    }

    public function exists( string $path ): bool {
        // Check if object exists in bucket.
    }

    public function get_full_path( string $path ): string {
        // For remote drivers, this may return the URL or a temp local path.
        return $this->url( $path );
    }

    public function download( string $path, string $local_dest ): bool {
        // Stream $this->bucket/$path into the local file $local_dest.
        // Return true on success, false on any failure.
    }
}

Registering Your Driver

StorageService::get_driver() resolves the active driver by passing the configured driver name through the singular mvs_storage_driver filter and expecting a driver instance back. The filter receives two arguments - the current driver (null until something supplies one) and the configured driver name - and your callback returns your driver instance only when the name matches your slug, otherwise it returns $driver unchanged:

add_filter( 'mvs_storage_driver', function( $driver, string $name ) {
    return 'my_s3_compatible' === $name ? new MyS3CompatibleDriver() : $driver;
}, 10, 2 );

If no filter returns a StorageDriverInterface instance, StorageService falls back to the built-in LocalDriver.

This is exactly how WPMediaVerse Pro registers its own drivers - its callback switches on $name and returns the matching driver (s3, bunnycdn, r2, dospaces):

// Simplified from WPMediaVerse Pro.
add_filter( 'mvs_storage_driver', function( $driver, string $name ) {
    switch ( $name ) {
        case 's3':
            return new S3StorageDriver();
        case 'bunnycdn':
            return new BunnyCDNStorageDriver();
        default:
            return $driver;
    }
}, 10, 2 );

Then set the active driver to your slug so StorageService picks it:

wp option update mvs_storage_driver my_s3_compatible

(Or add your slug to the settings-page dropdown via a separate filter so site owners can switch to it from the admin UI.)

Local Driver Reference

The built-in local driver stores files at:

{wp_upload_dir['basedir']}/wpmediaverse/YYYY/MM/filename.ext

Files are served from:

{wp_upload_dir['baseurl']}/wpmediaverse/YYYY/MM/filename.ext

get_full_path() returns the absolute filesystem path, which is used by services like WatermarkService and AIService that need to read the file from disk.

Signed URLs and Private Delivery

Your driver does not generate signed URLs - that is SignedUrlService's job. The flow is:

  1. A read-side caller asks Core\MediaUrl::thumb() / ::file() (or SignedUrlService directly) for a URL.
  2. SignedUrlService::generate() / ::generate_thumbnail() runs the privacy check (PrivacyService::can_view()), then either:
    • returns a signed /serve proxy URL (HMAC-signed query params: media ID, viewer user ID, expiry, signature) for gated/private media, or
    • returns a direct driver URL for public media on a cloud driver, so the browser hits the CDN edge instead of WordPress.
  3. When the browser requests a /serve URL, SignedUrlService::serve() re-validates the signature and re-checks can_view() per request, then streams the bytes by reading the file from your driver (via get_full_path() / download()).

There is no mvs_generate_signed_url filter. The public extension points that actually exist let a cloud driver substitute its own CDN/presigned URL for the public-media direct path:

Filter Args When it fires
mvs_serve_public_cloud_direct (bool $enabled, int $media_id) Gate the "serve public cloud media directly" behavior on/off per media.
mvs_public_cloud_thumbnail_url (string $url, int $media_id, string $size) Final say on the public thumbnail URL for cloud-hosted media - return a presigned/CDN URL here.
mvs_public_cloud_file_url (string $url, int $media_id, string $context) Final say on the public full-file URL for cloud-hosted media.
// Substitute a presigned URL for your driver's public thumbnails.
add_filter( 'mvs_public_cloud_thumbnail_url', function( string $url, int $media_id, string $size ) {
    if ( 'my_s3_compatible' !== get_option( 'mvs_storage_driver' ) ) {
        return $url;
    }
    // Build the relative variant path and sign it with your SDK.
    $rel = get_post_meta( $media_id, 'thumb_' . $size . '_path', true );
    return $rel ? my_generate_presigned_url( $rel, 3600 ) : $url;
}, 10, 3 );

Private and restricted media is never eligible for the direct-cloud path - StorageService::get_driver_for_privacy() keeps non-public media on local disk, and /serve re-checks can_view() on every request. So these filters only affect public media.

Viewer-Aware Full-File URLs (2.0.0)

MediaRepositoryInterface::get_url_for_viewer() is a driver-agnostic, viewer-aware way to get a URL for a media item's full original file (not a thumbnail) that is authorized for a specific viewer rather than the current logged-in user. It exists so callers that already have a $viewer_id (a REST controller resolving on behalf of a specific user, an email/notification job, an app backend) don't have to swap the global user or hand-roll a signed URL.

namespace WPMediaVerse\Repository;

interface MediaRepositoryInterface {

    /**
     * Viewer-aware URL for the full original file.
     *
     * For public media on a cloud driver this returns the direct CDN URL (same
     * as the broadcast path); for local/non-public media it returns the gated
     * /serve URL. Returns '' when the viewer may not view the media.
     *
     * @since 2.0.0
     *
     * @param int      $media_id  Media ID.
     * @param int|null $viewer_id Viewer to authorize against. Null = current user.
     * @return string Signed URL when the viewer may view the media, else ''.
     */
    public function get_url_for_viewer( int $media_id, ?int $viewer_id = null ): string;
}
$repo = \WPMediaVerse\Core\Plugin::container()->get( 'media_repository' );

// Resolve the file URL as it would appear to a specific member (not the current request user).
$url = $repo->get_url_for_viewer( $media_id, $target_user_id );

if ( '' === $url ) {
    // $target_user_id is not authorized to view this media.
}

Like every other read-side URL helper in this plugin, do not hand-build the path or call SignedUrlService directly - get_url_for_viewer() runs the same privacy check and driver resolution as Core\MediaUrl (see Template Overrides - Getting Media URLs), just parameterized by an explicit viewer instead of the current user. Custom storage drivers do not need to implement anything extra for this to work - it composes StorageService + SignedUrlService the same way the rest of the read path does.

WP-CLI Commands

Endpoints and hooks marked (Pro) require WPMediaVerse Pro.

WPMediaVerse registers all its commands under the wp mvs namespace.

wp mvs stats

Display plugin statistics in a table.

wp mvs stats

Output:

+-------------------+-------+
| Metric            | Value |
+-------------------+-------+
| Published Media   | 342   |
| Albums            | 18    |
| Total Views       | 9841  |
| Total Reactions   | 512   |
| Total Favorites   | 203   |
| DB Version        | 5     |
| Plugin Version    | 1.0.0 |
+-------------------+-------+

wp mvs migrate

Run or check database migrations.

# Run all pending migrations.
wp mvs migrate

# Check if migrations are needed (dry run).
wp mvs migrate --check

Options:

Option Description
--check Only check if migrations are needed; do not run them

Examples:

# Typical update workflow:
wp mvs migrate --check
wp mvs migrate

wp mvs prune-views

Delete old per-view tracking records to keep the wp_mvs_media_views table from growing indefinitely.

# Prune views older than 90 days (default).
wp mvs prune-views

# Prune views older than 30 days.
wp mvs prune-views --days=30

# Preview how many rows would be deleted.
wp mvs prune-views --dry-run

# Combine options.
wp mvs prune-views --days=30 --dry-run

Options:

Option Default Description
--days=<days> 90 Retain records newer than this many days
--dry-run off Show count without deleting

wp mvs cleanup-expired

Remove expired custom access grants from the wp_mvs_access_grants table.

# Cleanup with default batch size (100 grants at a time).
wp mvs cleanup-expired

# Use a larger batch size for faster cleanup on large sites.
wp mvs cleanup-expired --batch-size=500

Options:

Option Default Description
--batch-size=<size> 100 Number of grants to process per batch

wp mvs reindex

Rebuild the wp_mvs_media_index table from all published mvs_media posts. Run this if the index becomes out of sync (e.g., after a direct database import or migration from another plugin).

# Reindex with default batch size.
wp mvs reindex

# Use smaller batches to reduce memory usage on large sites.
wp mvs reindex --batch-size=50

Options:

Option Default Description
--batch-size=<size> 100 Number of posts to process per batch

The command outputs progress as it processes each batch and shows a final count of indexed items.


wp mvs cache-flush

Flush all WPMediaVerse caches - both object cache groups and plugin-managed transients. Run this after making direct database changes or when stale data is suspected.

wp mvs cache-flush

Output:

Success: WPMediaVerse caches flushed.

wp mvs moderation-stats

Display the current moderation queue statistics broken down by status.

wp mvs moderation-stats

Output:

+----------+-------+
| Status   | Count |
+----------+-------+
| Pending  | 14    |
| Approved | 3281  |
| Rejected | 57    |
+----------+-------+

wp mvs generate-video-thumbnails

Generate poster frames for video media items that don't have a thumb_large meta entry yet. Uses ffmpeg to extract a frame at the second mark of each video. Idempotent - videos that already have a poster are skipped unless --force is passed.

Recommended after upgrading to 1.2.0 to backfill posters for any video uploaded before the fix. Without a poster, video media renders a black frame in the lightbox until playback starts; with a poster, the first frame appears immediately.

# Backfill missing posters (skips videos that already have one).
wp mvs generate-video-thumbnails

# Dry-run to count what would be processed.
wp mvs generate-video-thumbnails --dry-run

# Re-extract posters even when they already exist (e.g. after switching ffmpeg builds).
wp mvs generate-video-thumbnails --force

Requirements:

  • ffmpeg available on the host system (check with which ffmpeg).

Options:

Option Default Description
--force off Overwrite existing thumb_large posters
--dry-run off Count eligible videos without writing to disk or DB

Added in 1.2.0.


wp mvs backfill-activity-thumbnails

Backfill BuddyPress activity thumbnails for media items that were imported without them (e.g., bulk imports or migrations from another plugin). Modifies the bp_activity table directly, so take a database backup before running without --dry-run. Requires the BuddyPress Activity component to be active; the command exits early otherwise.

# Backfill every migrated source.
wp mvs backfill-activity-thumbnails

# Only backfill activities imported from rtMedia.
wp mvs backfill-activity-thumbnails --source=rtmedia

# Preview what would be updated without writing to the database.
wp mvs backfill-activity-thumbnails --dry-run

Options:

Option Default Description
--source=<source> all Only backfill activities from a specific migration source: rtmedia, mediapress, buddyboss, or all
--dry-run off Count eligible records without updating them

wp mvs sync-activity-privacy

Recompute the BuddyPress activity hide_sitewide flag for every existing media activity. Walks each mvs_media_upload activity plus every activity_update carrying _mvs_media_ids meta and recomputes hide_sitewide from the linked media's effective privacy (media + parent album, most-restrictive wins). Run once after upgrading to bring legacy activity rows in line with the privacy-sync behaviour. Idempotent - re-runs only touch rows that have actually drifted.

Requires BuddyPress with the Activity component active; the command exits early otherwise.

# Preview which activity rows would change.
wp mvs sync-activity-privacy --dry-run

# Apply the recomputed flags.
wp mvs sync-activity-privacy

Options:

Option Default Description
--dry-run off Show what would change without writing to the database

wp mvs relocalize-private

Heal pre-1.4.0 non-public media rows whose stored URL meta still points at a prior cloud bucket. When media uploaded to a cloud driver (S3 / BunnyCDN / R2 / DigitalOcean Spaces) was later switched to a restricted privacy level (members / friends / private / group / custom), the URL meta was not relocalized, so SignedUrlService 403s on every read because the cloud URL fails its wpmediaverse/ containment check. The 1.4.0 listener fixes this going forward; this command heals older rows (Basecamp #9925110293).

Safe to re-run - rows that already have local URLs are skipped.

Added in 1.4.0.

# Report what would change without writing.
wp mvs relocalize-private --dry-run

# Heal all affected rows.
wp mvs relocalize-private

# Inspect a single media row.
wp mvs relocalize-private --media-id=47

Options:

Option Default Description
--dry-run off Report what would change without writing
--media-id=<id> 0 (all) Only inspect this single media row
--limit=<n> 0 (all) Stop after inspecting this many candidate rows

wp mvs migrate-storage

Migrate every stored media file from one storage driver to another. Idempotent - re-running skips media already present on the destination. Only public media is migrated by default (non-public media stays local to preserve privacy). The active mvs_storage_driver option is NOT flipped automatically; flip it manually via wp option update once the run completes cleanly.

Added in 1.3.0.

# Dry run: report what would move without touching files.
wp mvs migrate-storage --from=local --to=s3 --dry-run

# Migrate but keep source copies for a verification window.
wp mvs migrate-storage --from=local --to=s3 --keep-source

# Full migration.
wp mvs migrate-storage --from=local --to=s3

# Migrate a single media row for testing.
wp mvs migrate-storage --from=local --to=s3 --media-id=42

Options:

Option Default Description
--from=<driver> required Source driver slug: local, s3, bunnycdn
--to=<driver> required Destination driver slug. Must differ from --from
--dry-run off Walk the media list and report without transferring files
--keep-source off Skip post-verify source-side deletion
--media-id=<id> 0 (all) Migrate one specific media row
--limit=<n> 0 (all) Stop after this many rows
--include-non-public off Also migrate non-public media (only when cloud bucket is private)

Note: s3 and bunnycdn drivers require WPMediaVerse Pro.


wp mvs cloud-thumbs-backfill

Push thumbnail variants to the active cloud driver for images uploaded before 1.3.0 (when cloud-side thumbnails landed). Downloads each original from cloud, regenerates three size variants locally, and uploads each variant. Only processes public images whose thumbnail meta still points at a local URL.

Run wp mvs migrate-storage first so original files are already on cloud.

Added in 1.3.0.

# Dry run.
wp mvs cloud-thumbs-backfill --dry-run

# Process all eligible images.
wp mvs cloud-thumbs-backfill

# Limit for testing.
wp mvs cloud-thumbs-backfill --limit=100

# Single media row.
wp mvs cloud-thumbs-backfill --media-id=42

Options:

Option Default Description
--dry-run off Report what would migrate without downloading or uploading
--media-id=<id> 0 (all) Process a single media row
--limit=<n> 0 (all) Stop after this many rows

Requirements: Active driver must not be local. Pro plugin required for s3/bunnycdn.


wp mvs cleanup-local

Delete local copies of media that are verified on the active cloud driver. Use after a wp mvs migrate-storage --keep-source run once the cloud driver has been confirmed to serve all media correctly.

Irreversible. The command verifies each file exists on cloud before deleting the local copy; a failed verification keeps the local file. Only public media is processed.

Added in 1.3.0.

# Preview what would be deleted.
wp mvs cleanup-local --dry-run

# Delete originals and thumbnail variants.
wp mvs cleanup-local

# Keep local thumbnail variants; delete originals only.
wp mvs cleanup-local --keep-thumbs

# Single media row.
wp mvs cleanup-local --media-id=42

Options:

Option Default Description
--dry-run off Walk the candidate list and report without deleting
--media-id=<id> 0 (all) Process a single media row
--limit=<n> 0 (all) Stop after this many rows
--keep-thumbs off Delete only the original file; keep local thumbnail variants

wp mvs optimize

Optimize a single media row: re-encode the original and emit WebP (and optionally AVIF) siblings. Resume-safe via the _mvs_optimized_at meta sentinel - already-optimized media is skipped unless --force is passed.

Added in 1.3.0.

wp mvs optimize 42
wp mvs optimize 42 --include-variants --force

Synopsis:

wp mvs optimize <media_id> [--include-variants] [--force]

Options:

Option Default Description
<media_id> required Media row ID to optimize
--include-variants off Also re-process every thumbnail variant
--force off Re-run even when _mvs_optimized_at is already set

Output: Reports bytes before and after, percent saved, and variant count.


wp mvs optimize-bulk

Bulk-optimize every image in the library matching the given filters. Resume-safe: rows already marked with _mvs_optimized_at are skipped automatically. Rows that previously failed (marked _mvs_optimize_failed) are also skipped unless --include-failed is passed.

Added in 1.3.0.

# Process all unoptimized images, including variants.
wp mvs optimize-bulk --include-variants

# JPEG only, dry run.
wp mvs optimize-bulk --mime=image/jpeg --dry-run

# Cap to first 500 rows.
wp mvs optimize-bulk --limit=500

# Skip first 200 rows (manual offset resume).
wp mvs optimize-bulk --offset=200

Options:

Option Default Description
--limit=<n> 0 (all) Cap rows processed
--offset=<n> 0 Skip the first N rows (manual resume)
--mime=<types> all images Comma-separated MIME types to include (e.g. image/jpeg,image/png)
--media-type=<type> all Restrict to one media_type column value (e.g. photo)
--include-variants off Process thumbnail variants alongside the original
--dry-run off Report what would be processed without writing
--include-failed off Re-process rows previously marked as failed

Output: Reports processed/skipped/failed counts and total bytes saved.


wp mvs backfill_ai

Run AI description + tagging on image media that was uploaded before AI moderation was enabled (or before mvs_ai_auto_describe / mvs_ai_auto_tag were turned on). Only images with no ai_status meta are picked up unless --force is passed. Each match is queued to the same async Action Scheduler job that runs on upload (mvs_ai_process_media) unless --sync is passed to process inline.

Added in 1.6.0.

# Preview how many images would be processed.
wp mvs backfill_ai --dry-run

# Cap the run for a first pass.
wp mvs backfill_ai --limit=200

# Process inline instead of queueing (respects the AI budget cap per item).
wp mvs backfill_ai --sync

# Reprocess every image, including ones already attempted.
wp mvs backfill_ai --force

Options:

Option Default Description
--limit=<n> 0 (all) Max media to process
--sync off Process inline instead of queueing the async job
--dry-run off List how many media would be processed without doing it
--force off Reprocess all image media, including ones already attempted

Output: Reports queued, processed inline, and failed counts.


wp mvs repair-storage

Heal two pre-1.8.0 storage inconsistencies without deleting or moving anything - the source file is only ever copied:

  • Absolute file paths left over from a plugin migration (rtMedia / MediaPress / BuddyBoss) that 404 because the path never matched a valid URL. Re-sideloaded into uploads/wpmediaverse/ as a relative path.
  • Stranded thumbnails - an older "Migrate all" moved the original to cloud but left thumbnails on local disk, so they 404 at the cloud URL. Pushed to the active cloud driver.

Idempotent and safe to re-run. On the admin side this repair already runs automatically in the background after update; use this command for headless sites or to run it on demand.

# Report how many rows need repair without changing anything.
wp mvs repair-storage --dry-run

# Apply the repair.
wp mvs repair-storage

Options:

Option Default Description
--dry-run off Report how many rows need repair without changing anything

wp mvs cert

Run the functional certification gate: a boot smoke test across every registered REST route plus a "dead toggle" oracle check (settings/options that are read but never wired to real behavior). Exits non-zero on any failure, so CI and the release pipeline can gate identically on any machine.

Added in 1.8.1.

# Run every check.
wp mvs cert

# Run only the boot smoke check.
wp mvs cert boot

# Run only the dead-toggle contract check, machine-readable output.
wp mvs cert contract --porcelain

Synopsis:

wp mvs cert [<check>] [--porcelain]

Arguments:

Argument Default Description
<check> all Which check to run: all, contract, or boot

Options:

Option Description
--porcelain Emit the machine-readable JSON ledger instead of the human-readable table

wp mvs-pro cert

Requires WPMediaVerse Pro. Added in Pro 1.8.1.

Certifies the Pro surface. Pro ships no engine of its own - the command reuses the Free CertRunner, pointed at the Pro plugin directory so the same checks read Pro's audit/manifest and audit/cert-oracles.json.

wp mvs-pro cert
wp mvs-pro cert boot
wp mvs-pro cert contract --porcelain

It takes the same [<check>] argument (all, contract, boot) and the same --porcelain flag as wp mvs cert.

The Free plugin must be active. The engine lives in Free, so with Free deactivated the command exits with an error rather than a partial pass. Run both commands in a release gate - wp mvs cert does not cover Pro routes or Pro toggles.

Output (table mode): one [PASS] / [FAIL] / [HOLE] row per check, followed by a Summary: N pass, N fail, N hole. line. A [HOLE] marks a tracked gap (not yet a regression, but not proven either) rather than a failure. The command calls WP_CLI::error() (exit code 1) when any check fails.


wp mvs competitions tick (Pro)

Run one competitions scheduler tick immediately, instead of waiting for the recurring Action Scheduler job. A tick fires every competition transition hook once - activating scheduled challenges, closing challenge entries, finalizing expired challenges, starting registered tournaments, and resolving expired matches - so any challenge or tournament whose deadline has passed advances right away. Useful for debugging on a site where Action Scheduler / WP-Cron is not firing, or to force an immediate state advance after editing competition rows.

Requires WPMediaVerse Pro with a competition feature enabled (challenges, tournaments, or battles).

wp mvs competitions tick

This command takes no options or arguments.

Output:

Success: Competitions tick executed.

wp mvs competitions recompute (Pro)

Force the one-shot competitions catch-up migration to run again. Clears the internal migration flag, re-fires every transition hook once, and re-marks the flag as done. Use this when competition DB rows are edited by hand and you need the scheduler to re-derive their state. Reports how many non-finalized, non-cancelled competitions remain after the pass.

Requires WPMediaVerse Pro with a competition feature enabled.

wp mvs competitions recompute

This command takes no options or arguments.

Output:

Success: Recomputed 3 competition(s).

Scheduling Maintenance with WP-CLI Cron

Add these commands to your server cron for automated maintenance:

# /etc/cron.d/wpmediaverse

# Prune old views weekly.
0 2 * * 0 www-data wp --path=/var/www/html mvs prune-views --days=90

# Cleanup expired access grants daily.
0 3 * * * www-data wp --path=/var/www/html mvs cleanup-expired

Exit Codes

All commands follow WP-CLI conventions:

  • 0 - success
  • 1 - error (WP_CLI::error())

Migration Tools

Endpoints and hooks marked (Pro) require WPMediaVerse Pro.

WPMediaVerse Pro includes WP-CLI commands to import media from three popular WordPress media plugins. Each command reads the source plugin's data, maps it to WPMediaVerse's mvs_media post type and wp_mvs_media_index table, and preserves the original upload dates, author attribution, and file URLs.

WPMediaVerse Pro is required. The migration commands are not available in the free plugin.

Always run with --dry-run first to review what will be imported before committing changes.

Updated in 1.2.0: the Tools > Migration admin page is now a generic shell that hosts per-platform cards (rtMedia, MediaPress, BuddyBoss). Each platform owns its own detection, batch-run, dedup, and progress logic via a WPMediaVersePro\Integrations\<Platform>\MigrationAdmin class. Two pre-existing detection bugs were fixed in the same pass: the Imported count was always 0 regardless of actual progress, and the MediaPress dedup query was running against an undefined $wpdb. All three CLI importers (import-rtmedia, import-mediapress, import-buddyboss) extend the new AbstractBatchImporter base class - same flag set, same batched-fetch loop, same dedup behaviour.


wp mvs import-rtmedia

Import media from rtMedia.

The command reads rt_rtm_media table records and their associated meta, creates mvs_media posts with matching post dates and authors, and inserts index rows.

# Preview the import without making any changes.
wp mvs import-rtmedia --dry-run

# Run the full import.
wp mvs import-rtmedia

# Use a smaller batch size to reduce memory usage on large sites.
wp mvs import-rtmedia --batch-size=50

Options:

Option Default Description
--dry-run off Preview counts and field mapping; do not write any data
--batch-size=<n> 50 Number of rtMedia records to process per batch
--skip-albums off Skip rtMedia album import (albums are recreated as WPMediaVerse albums by default)
--offset=<n> 0 Start from a specific offset (for resuming)

What is mapped:

rtMedia field WPMediaVerse destination
media_author post_author on mvs_media post
media_date post_date on mvs_media post (original date preserved)
attachment_id Resolved to file URL; stored in mvs_media_index.file_url
activity_id Stored as _mvs_source_activity_id post meta for reference
Album membership Re-created as WPMediaVerse album relationships
Media type Mapped to image, video, or audio based on MIME type

Dry-run output example:

Found 842 rtMedia records.
842 would be created (0 skipped as duplicates).
Albums: 14 would be created.
Run without --dry-run to execute.

wp mvs import-mediapress

Import media from MediaPress.

The command queries mpp-media custom posts (falling back to attachments carrying _mpp_is_mpp_media meta on older installs) and their gallery associations, then creates matching mvs_media posts and index rows.

# Preview the import.
wp mvs import-mediapress --dry-run

# Run the full import.
wp mvs import-mediapress

# Resume from a specific offset on a large site.
wp mvs import-mediapress --offset=200

Options:

Option Default Description
--dry-run off Preview counts; do not write any data
--batch-size=<n> 50 Number of MediaPress media posts to process per batch
--skip-albums off Skip gallery/album import
--offset=<n> 0 Start from a specific offset (for resuming)

What is mapped:

MediaPress field WPMediaVerse destination
post_author post_author on mvs_media post
post_date post_date on mvs_media post (original date preserved)
_mpp_media_manager_id Resolved to file URL via attachment
Gallery Re-created as WPMediaVerse album
mpp_media_type Mapped to image, video, or audio
Privacy (gallery-level) Mapped to WPMediaVerse privacy: mpp-publicpublic, mpp-membersmembers, mpp-friendsfollowers, mpp-privateprivate

wp mvs import-buddyboss

Import media from BuddyBoss Platform (BuddyBoss Media component).

The command reads BuddyBoss media entries stored as custom posts and creates matching mvs_media posts.

# Preview the import.
wp mvs import-buddyboss --dry-run

# Run the full import.
wp mvs import-buddyboss

# Import only from a specific BuddyBoss source table.
wp mvs import-buddyboss --source=video

Options:

Option Default Description
--dry-run off Preview counts; do not write any data
--batch-size=<n> 50 Number of BuddyBoss media posts to process per batch
--source=<source> all Which table to import from: media, document, video, or all
--skip-albums off Skip album import (BuddyBoss albums are recreated as WPMediaVerse albums by default)
--offset=<n> 0 Start from a specific offset (for resuming)

What is mapped:

BuddyBoss field WPMediaVerse destination
post_author post_author on mvs_media post
post_date post_date on mvs_media post (original date preserved)
bb_media_id attachment Resolved to file URL; stored in mvs_media_index.file_url
Album membership Re-created as WPMediaVerse album relationships
Group ID (_bb_media_group_id) Stored as _mvs_bp_group_id post meta for BuddyPress integration
Privacy Mapped from BuddyBoss privacy levels to WPMediaVerse equivalents

Post-Migration Steps

After running any import command, run the following to ensure the index is consistent and all view stats are initialised:

wp mvs reindex
wp mvs migrate

Check imported media at Media > All Media in wp-admin. Filter by the original author to verify counts match.

wp-admin Media list filtered by imported author


Preserving Original File URLs

All three import commands store the original file URL in mvs_media_index.file_url without moving or copying the files. Your existing URLs continue to work. If you later change the storage driver in WPMediaVerse Pro (e.g., moving to Amazon S3), use wp mvs migrate-storage to batch-transfer files.


Handling Import Errors

If a batch fails, the command outputs the failing record IDs and continues. Review errors with:

wp mvs import-rtmedia 2>&1 | tee import-log.txt

Check wp-content/debug.log for detailed error traces when WP_DEBUG_LOG is enabled:

// wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );

Exit Codes

All import commands follow WP-CLI conventions:

  • 0 - completed (including dry-run)
  • 1 - fatal error (source plugin tables not found, Pro license inactive, etc.)

Something unclear? Open a support ticket → · Refund policy

Buy WPMediaVerse