diff --git a/src/server.rs b/src/server.rs
index 703ab37..5e21bd1 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,8 +1,30 @@
+use error::APIError;
+
 mod dto;
 mod error;
 
 #[cfg(test)]
 mod test;
 
+pub enum APIMajorVersion {
+	V7,
+	V8,
+}
+
+impl TryFrom<i32> for APIMajorVersion {
+	type Error = APIError;
+
+	fn try_from(value: i32) -> Result<Self, Self::Error> {
+		match value {
+			7 => Ok(Self::V7),
+			8 => Ok(Self::V8),
+			_ => Err(APIError::UnsupportedAPIVersion),
+		}
+	}
+}
+
+pub const API_MAJOR_VERSION: i32 = 8;
+pub const API_MINOR_VERSION: i32 = 0;
+
 mod axum;
 pub use axum::*;
diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs
index c6ab3e3..343d04b 100644
--- a/src/server/axum/api.rs
+++ b/src/server/axum/api.rs
@@ -12,11 +12,10 @@ use percent_encoding::percent_decode_str;
 
 use crate::{
 	app::{config, ddns, index, lastfm, playlist, scanner, settings, thumbnail, user, vfs, App},
-	server::{dto, error::APIError},
+	server::{dto, error::APIError, APIMajorVersion, API_MAJOR_VERSION, API_MINOR_VERSION},
 };
 
 use super::auth::{AdminRights, Auth};
-use super::version::Version;
 
 pub fn router() -> Router<App> {
 	Router::new()
@@ -68,8 +67,8 @@ pub fn router() -> Router<App> {
 
 async fn get_version() -> Json<dto::Version> {
 	let current_version = dto::Version {
-		major: dto::API_MAJOR_VERSION,
-		minor: dto::API_MINOR_VERSION,
+		major: API_MAJOR_VERSION,
+		minor: API_MINOR_VERSION,
 	};
 	Json(current_version)
 }
@@ -254,16 +253,19 @@ async fn post_trigger_index(
 	Ok(())
 }
 
-fn collection_files_to_response(files: Vec<index::CollectionFile>, version: Version) -> Response {
-	match version {
-		Version::V7 => Json(
+fn collection_files_to_response(
+	files: Vec<index::CollectionFile>,
+	api_version: APIMajorVersion,
+) -> Response {
+	match api_version {
+		APIMajorVersion::V7 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
 				.collect::<Vec<dto::v7::CollectionFile>>(),
 		)
 		.into_response(),
-		Version::V8 => Json(
+		APIMajorVersion::V8 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
@@ -273,16 +275,16 @@ fn collection_files_to_response(files: Vec<index::CollectionFile>, version: Vers
 	}
 }
 
-fn songs_to_response(files: Vec<scanner::Song>, version: Version) -> Response {
-	match version {
-		Version::V7 => Json(
+fn songs_to_response(files: Vec<scanner::Song>, api_version: APIMajorVersion) -> Response {
+	match api_version {
+		APIMajorVersion::V7 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
 				.collect::<Vec<dto::v7::Song>>(),
 		)
 		.into_response(),
-		Version::V8 => Json(
+		APIMajorVersion::V8 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
@@ -292,16 +294,19 @@ fn songs_to_response(files: Vec<scanner::Song>, version: Version) -> Response {
 	}
 }
 
-fn directories_to_response(files: Vec<scanner::Directory>, version: Version) -> Response {
-	match version {
-		Version::V7 => Json(
+fn directories_to_response(
+	files: Vec<scanner::Directory>,
+	api_version: APIMajorVersion,
+) -> Response {
+	match api_version {
+		APIMajorVersion::V7 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
 				.collect::<Vec<dto::v7::Directory>>(),
 		)
 		.into_response(),
-		Version::V8 => Json(
+		APIMajorVersion::V8 => Json(
 			files
 				.into_iter()
 				.map(|f| f.into())
@@ -313,19 +318,19 @@ fn directories_to_response(files: Vec<scanner::Directory>, version: Version) ->
 
 async fn get_browse_root(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 ) -> Response {
 	let result = match index.browse(std::path::Path::new("")).await {
 		Ok(r) => r,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	collection_files_to_response(result, version)
+	collection_files_to_response(result, api_version)
 }
 
 async fn get_browse(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 	Path(path): Path<String>,
 ) -> Response {
@@ -334,24 +339,24 @@ async fn get_browse(
 		Ok(r) => r,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	collection_files_to_response(result, version)
+	collection_files_to_response(result, api_version)
 }
 
 async fn get_flatten_root(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 ) -> Response {
 	let songs = match index.flatten(std::path::Path::new("")).await {
 		Ok(s) => s,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	songs_to_response(songs, version)
+	songs_to_response(songs, api_version)
 }
 
 async fn get_flatten(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 	Path(path): Path<String>,
 ) -> Response {
@@ -360,40 +365,48 @@ async fn get_flatten(
 		Ok(s) => s,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	songs_to_response(songs, version)
+	songs_to_response(songs, api_version)
 }
 
-async fn get_random(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response {
+async fn get_random(
+	_auth: Auth,
+	api_version: APIMajorVersion,
+	State(index): State<index::Index>,
+) -> Response {
 	let directories = match index.get_random_albums(20).await {
 		Ok(d) => d,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	directories_to_response(directories, version)
+	directories_to_response(directories, api_version)
 }
 
-async fn get_recent(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response {
+async fn get_recent(
+	_auth: Auth,
+	api_version: APIMajorVersion,
+	State(index): State<index::Index>,
+) -> Response {
 	let directories = match index.get_recent_albums(20).await {
 		Ok(d) => d,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	directories_to_response(directories, version)
+	directories_to_response(directories, api_version)
 }
 
 async fn get_search_root(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 ) -> Response {
 	let files = match index.search("").await {
 		Ok(f) => f,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	collection_files_to_response(files, version)
+	collection_files_to_response(files, api_version)
 }
 
 async fn get_search(
 	_auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(index): State<index::Index>,
 	Path(query): Path<String>,
 ) -> Response {
@@ -401,7 +414,7 @@ async fn get_search(
 		Ok(f) => f,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	collection_files_to_response(files, version)
+	collection_files_to_response(files, api_version)
 }
 
 async fn get_playlists(
@@ -431,7 +444,7 @@ async fn put_playlist(
 
 async fn get_playlist(
 	auth: Auth,
-	version: Version,
+	api_version: APIMajorVersion,
 	State(playlist_manager): State<playlist::Manager>,
 	Path(name): Path<String>,
 ) -> Response {
@@ -442,7 +455,7 @@ async fn get_playlist(
 		Ok(s) => s,
 		Err(e) => return APIError::from(e).into_response(),
 	};
-	songs_to_response(songs, version)
+	songs_to_response(songs, api_version)
 }
 
 async fn delete_playlist(
diff --git a/src/server/axum/version.rs b/src/server/axum/version.rs
index db711f7..67e8791 100644
--- a/src/server/axum/version.rs
+++ b/src/server/axum/version.rs
@@ -1,15 +1,10 @@
 use axum::{async_trait, extract::FromRequestParts};
 use http::request::Parts;
 
-use crate::server::{dto, error::APIError};
-
-pub enum Version {
-	V7,
-	V8,
-}
+use crate::server::{error::APIError, APIMajorVersion};
 
 #[async_trait]
-impl<S> FromRequestParts<S> for Version
+impl<S> FromRequestParts<S> for APIMajorVersion
 where
 	S: Send + Sync,
 {
@@ -19,18 +14,14 @@ where
 		let version_header = match parts.headers.get("Accept-Version").map(|h| h.to_str()) {
 			Some(Ok(h)) => h,
 			Some(Err(_)) => return Err(APIError::InvalidAPIVersionHeader),
-			None => return Ok(Version::V7), // TODO Drop support for implicit version in future release
+			None => return Ok(APIMajorVersion::V7), // TODO Drop support for implicit version in future release
 		};
 
-		let version: dto::Version = match serde_json::from_str(version_header) {
+		let version = match str::parse::<i32>(version_header) {
 			Ok(v) => v,
 			Err(_) => return Err(APIError::APIVersionHeaderParseError),
 		};
 
-		Ok(match version.major {
-			7 => Version::V7,
-			8 => Version::V8,
-			_ => return Err(APIError::UnsupportedAPIVersion),
-		})
+		APIMajorVersion::try_from(version)
 	}
 }
diff --git a/src/server/dto.rs b/src/server/dto.rs
index 80386be..cf0bc61 100644
--- a/src/server/dto.rs
+++ b/src/server/dto.rs
@@ -1,7 +1,4 @@
 pub mod v7;
 pub mod v8;
 
-pub const API_MAJOR_VERSION: i32 = 8;
-pub const API_MINOR_VERSION: i32 = 0;
-
 pub use v8::*;
diff --git a/src/server/test.rs b/src/server/test.rs
index 9c483a9..ea0857f 100644
--- a/src/server/test.rs
+++ b/src/server/test.rs
@@ -24,6 +24,7 @@ mod web;
 
 use crate::server::dto;
 use crate::server::test::constants::*;
+use protocol::V8;
 
 pub use crate::server::axum::test::ServiceType;
 
@@ -116,7 +117,7 @@ pub trait TestService {
 		assert_eq!(response.status(), StatusCode::OK);
 
 		loop {
-			let browse_request = protocol::browse(Path::new(""));
+			let browse_request = protocol::browse::<V8>(Path::new(""));
 			let response = self
 				.fetch_json::<(), Vec<dto::CollectionFile>>(&browse_request)
 				.await;
@@ -128,7 +129,7 @@ pub trait TestService {
 		}
 
 		loop {
-			let flatten_request = protocol::flatten(Path::new(""));
+			let flatten_request = protocol::flatten::<V8>(Path::new(""));
 			let response = self.fetch_json::<_, Vec<dto::Song>>(&flatten_request).await;
 			let entries = response.body();
 			if !entries.is_empty() {
diff --git a/src/server/test/admin.rs b/src/server/test/admin.rs
index 0cefc85..4497c24 100644
--- a/src/server/test/admin.rs
+++ b/src/server/test/admin.rs
@@ -1,6 +1,7 @@
 use http::StatusCode;
 
 use crate::server::dto;
+use crate::server::test::protocol::V8;
 use crate::server::test::{protocol, ServiceType, TestService};
 use crate::test_name;
 
@@ -47,7 +48,7 @@ async fn trigger_index_golden_path() {
 	service.complete_initial_setup().await;
 	service.login_admin().await;
 
-	let request = protocol::random();
+	let request = protocol::random::<V8>();
 
 	let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
 	let entries = response.body();
diff --git a/src/server/test/auth.rs b/src/server/test/auth.rs
index 242c018..c6c4ba7 100644
--- a/src/server/test/auth.rs
+++ b/src/server/test/auth.rs
@@ -2,6 +2,7 @@ use headers::{self, HeaderMapExt};
 use http::StatusCode;
 
 use crate::server::dto;
+use crate::server::test::protocol::V8;
 use crate::server::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
@@ -45,7 +46,7 @@ async fn authentication_via_bearer_http_header_rejects_bad_token() {
 	let mut service = ServiceType::new(&test_name!()).await;
 	service.complete_initial_setup().await;
 
-	let mut request = protocol::random();
+	let mut request = protocol::random::<V8>();
 	let bearer = headers::Authorization::bearer("garbage").unwrap();
 	request.headers_mut().typed_insert(bearer);
 
@@ -67,7 +68,7 @@ async fn authentication_via_bearer_http_header_golden_path() {
 
 	service.logout().await;
 
-	let mut request = protocol::random();
+	let mut request = protocol::random::<V8>();
 	let bearer = headers::Authorization::bearer(&authorization.token).unwrap();
 	request.headers_mut().typed_insert(bearer);
 	let response = service.fetch(&request).await;
@@ -79,7 +80,7 @@ async fn authentication_via_query_param_rejects_bad_token() {
 	let mut service = ServiceType::new(&test_name!()).await;
 	service.complete_initial_setup().await;
 
-	let mut request = protocol::random();
+	let mut request = protocol::random::<V8>();
 	*request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token")
 		.parse()
 		.unwrap();
@@ -102,7 +103,7 @@ async fn authentication_via_query_param_golden_path() {
 
 	service.logout().await;
 
-	let mut request = protocol::random();
+	let mut request = protocol::random::<V8>();
 	*request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token)
 		.parse()
 		.unwrap();
diff --git a/src/server/test/collection.rs b/src/server/test/collection.rs
index e236724..41ca32b 100644
--- a/src/server/test/collection.rs
+++ b/src/server/test/collection.rs
@@ -2,13 +2,14 @@ use http::StatusCode;
 use std::path::{Path, PathBuf};
 
 use crate::server::dto;
+use crate::server::test::protocol::{V7, V8};
 use crate::server::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
 #[tokio::test]
 async fn browse_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::browse(&PathBuf::new());
+	let request = protocol::browse::<V8>(&PathBuf::new());
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -21,7 +22,7 @@ async fn browse_root() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::browse(&PathBuf::new());
+	let request = protocol::browse::<V8>(&PathBuf::new());
 	let response = service
 		.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
 		.await;
@@ -39,7 +40,7 @@ async fn browse_directory() {
 	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
-	let request = protocol::browse(&path);
+	let request = protocol::browse::<V8>(&path);
 	let response = service
 		.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
 		.await;
@@ -49,21 +50,43 @@ async fn browse_directory() {
 }
 
 #[tokio::test]
-async fn browse_bad_directory() {
+async fn browse_missing_directory() {
 	let mut service = ServiceType::new(&test_name!()).await;
 	service.complete_initial_setup().await;
 	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
-	let request = protocol::browse(&path);
+	let request = protocol::browse::<V8>(&path);
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
+#[tokio::test]
+async fn browse_directory_api_v7() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
+
+	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
+	let request = protocol::browse::<V7>(&path);
+	let response = service
+		.fetch_json::<_, Vec<dto::v7::CollectionFile>>(&request)
+		.await;
+	assert_eq!(response.status(), StatusCode::OK);
+	let entries = response.body();
+	assert_eq!(entries.len(), 5);
+	match &entries[0] {
+		dto::v7::CollectionFile::Song(s) => assert_eq!(s.artist.as_deref(), Some("Khemmis")),
+		_ => (),
+	}
+}
+
 #[tokio::test]
 async fn flatten_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::flatten(&PathBuf::new());
+	let request = protocol::flatten::<V8>(&PathBuf::new());
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -76,7 +99,7 @@ async fn flatten_root() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::flatten(&PathBuf::new());
+	let request = protocol::flatten::<V8>(&PathBuf::new());
 	let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
@@ -91,7 +114,7 @@ async fn flatten_directory() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::flatten(Path::new(TEST_MOUNT_NAME));
+	let request = protocol::flatten::<V8>(Path::new(TEST_MOUNT_NAME));
 	let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
@@ -99,21 +122,39 @@ async fn flatten_directory() {
 }
 
 #[tokio::test]
-async fn flatten_bad_directory() {
+async fn flatten_missing_directory() {
 	let mut service = ServiceType::new(&test_name!()).await;
 	service.complete_initial_setup().await;
 	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
-	let request = protocol::flatten(&path);
+	let request = protocol::flatten::<V8>(&path);
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
+#[tokio::test]
+async fn flatten_directory_api_v7() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
+
+	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
+	let request = protocol::flatten::<V7>(&path);
+	let response = service.fetch_json::<_, Vec<dto::v7::Song>>(&request).await;
+	assert_eq!(response.status(), StatusCode::OK);
+	let entries = response.body();
+	assert_eq!(entries.len(), 5);
+
+	assert_eq!(entries[0].artist.as_deref(), Some("Khemmis"));
+}
+
 #[tokio::test]
 async fn random_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::random();
+	let request = protocol::random::<V8>();
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -126,7 +167,7 @@ async fn random_golden_path() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::random();
+	let request = protocol::random::<V8>();
 	let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
@@ -141,7 +182,7 @@ async fn random_with_trailing_slash() {
 	service.index().await;
 	service.login().await;
 
-	let mut request = protocol::random();
+	let mut request = protocol::random::<V8>();
 	add_trailing_slash(&mut request);
 	let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
@@ -152,7 +193,7 @@ async fn random_with_trailing_slash() {
 #[tokio::test]
 async fn recent_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::recent();
+	let request = protocol::recent::<V8>();
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -165,7 +206,7 @@ async fn recent_golden_path() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::recent();
+	let request = protocol::recent::<V8>();
 	let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
@@ -180,7 +221,7 @@ async fn recent_with_trailing_slash() {
 	service.index().await;
 	service.login().await;
 
-	let mut request = protocol::recent();
+	let mut request = protocol::recent::<V8>();
 	add_trailing_slash(&mut request);
 	let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
@@ -191,7 +232,7 @@ async fn recent_with_trailing_slash() {
 #[tokio::test]
 async fn search_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::search("");
+	let request = protocol::search::<V8>("");
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -202,7 +243,7 @@ async fn search_without_query() {
 	service.complete_initial_setup().await;
 	service.login().await;
 
-	let request = protocol::search("");
+	let request = protocol::search::<V8>("");
 	let response = service
 		.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
 		.await;
@@ -217,7 +258,7 @@ async fn search_with_query() {
 	service.index().await;
 	service.login().await;
 
-	let request = protocol::search("door");
+	let request = protocol::search::<V8>("door");
 	let response = service
 		.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
 		.await;
diff --git a/src/server/test/playlist.rs b/src/server/test/playlist.rs
index 4ba8205..c2ee982 100644
--- a/src/server/test/playlist.rs
+++ b/src/server/test/playlist.rs
@@ -1,6 +1,7 @@
 use http::StatusCode;
 
 use crate::server::dto;
+use crate::server::test::protocol::V8;
 use crate::server::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
@@ -63,7 +64,7 @@ async fn save_playlist_large() {
 #[tokio::test]
 async fn get_playlist_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
+	let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME);
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -81,7 +82,7 @@ async fn get_playlist_golden_path() {
 		assert_eq!(response.status(), StatusCode::OK);
 	}
 
-	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
+	let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME);
 	let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
@@ -92,7 +93,7 @@ async fn get_playlist_bad_name_returns_not_found() {
 	service.complete_initial_setup().await;
 	service.login().await;
 
-	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
+	let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME);
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
diff --git a/src/server/test/protocol.rs b/src/server/test/protocol.rs
index b28f106..b452af2 100644
--- a/src/server/test/protocol.rs
+++ b/src/server/test/protocol.rs
@@ -5,6 +5,25 @@ use std::path::Path;
 use crate::server::dto;
 use crate::{app::user, server::dto::ThumbnailSize};
 
+pub trait ProtocolVersion {
+	fn header_value() -> i32;
+}
+
+pub struct V7;
+pub struct V8;
+
+impl ProtocolVersion for V7 {
+	fn header_value() -> i32 {
+		7
+	}
+}
+
+impl ProtocolVersion for V8 {
+	fn header_value() -> i32 {
+		8
+	}
+}
+
 pub fn web_index() -> Request<()> {
 	Request::builder()
 		.method(Method::GET)
@@ -145,45 +164,50 @@ pub fn trigger_index() -> Request<()> {
 		.unwrap()
 }
 
-pub fn browse(path: &Path) -> Request<()> {
+pub fn browse<VERSION: ProtocolVersion>(path: &Path) -> Request<()> {
 	let path = path.to_string_lossy();
 	let endpoint = format!("/api/browse/{}", url_encode(path.as_ref()));
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri(&endpoint)
 		.body(())
 		.unwrap()
 }
 
-pub fn flatten(path: &Path) -> Request<()> {
+pub fn flatten<VERSION: ProtocolVersion>(path: &Path) -> Request<()> {
 	let path = path.to_string_lossy();
 	let endpoint = format!("/api/flatten/{}", url_encode(path.as_ref()));
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri(&endpoint)
 		.body(())
 		.unwrap()
 }
 
-pub fn random() -> Request<()> {
+pub fn random<VERSION: ProtocolVersion>() -> Request<()> {
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri("/api/random")
 		.body(())
 		.unwrap()
 }
 
-pub fn recent() -> Request<()> {
+pub fn recent<VERSION: ProtocolVersion>() -> Request<()> {
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri("/api/recent")
 		.body(())
 		.unwrap()
 }
 
-pub fn search(query: &str) -> Request<()> {
+pub fn search<VERSION: ProtocolVersion>(query: &str) -> Request<()> {
 	let endpoint = format!("/api/search/{}", url_encode(query));
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri(&endpoint)
 		.body(())
@@ -253,9 +277,10 @@ pub fn save_playlist(
 		.unwrap()
 }
 
-pub fn read_playlist(name: &str) -> Request<()> {
+pub fn read_playlist<VERSION: ProtocolVersion>(name: &str) -> Request<()> {
 	let endpoint = format!("/api/playlist/{}", url_encode(name));
 	Request::builder()
+		.header("Accept-Version", VERSION::header_value())
 		.method(Method::GET)
 		.uri(&endpoint)
 		.body(())