diff --git a/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h b/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h
index 1f652fd6..5590ba14 100644
--- a/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h
+++ b/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h
@@ -6,6 +6,22 @@
 
 @property (strong, nonatomic, readonly) NSURL * _Nonnull baseURL;
 
+/// Whether the user's preferred language locale should be appended to the request.
+/// Should default to `true`.
+///
+/// - SeeAlso: `localeKey` and `localeValue` to configure the locale appendend to the request.
+@property (nonatomic, readonly) BOOL appendsPreferredLanguageLocale;
+
+/// The key with which to specify locale in the parameters of a request.
+@property (strong, nonatomic, readonly) NSString * _Nonnull localeKey;
+
+/// The value with which to specify locale in the parameters of a request.
+@property (strong, nonatomic, readonly) NSString * _Nonnull localeValue;
+
+@property (strong, nonatomic, readonly) NSURLSession * _Nonnull urlSession;
+
+@property (strong, nonatomic, readonly) void (^ _Nullable invalidTokenHandler)(void);
+
 /// - Note: `parameters` has `id` instead of the more common `NSObject *` as its value type so it will convert to `AnyObject` in Swift.
 ///         In Swift, it's simpler to work with `AnyObject` than with `NSObject`. For example `"abc" as AnyObject` over `"abc" as NSObject`.
 - (NSProgress * _Nullable)get:(NSString * _Nonnull)URLString
diff --git a/Sources/WordPressKit/Models/RemotePostParameters.swift b/Sources/WordPressKit/Models/RemotePostParameters.swift
index 81849b09..ea5f6685 100644
--- a/Sources/WordPressKit/Models/RemotePostParameters.swift
+++ b/Sources/WordPressKit/Models/RemotePostParameters.swift
@@ -177,10 +177,14 @@ private enum RemotePostWordPressComCodingKeys: String, CodingKey {
     static let postTags = "post_tag"
 }
 
-struct RemotePostCreateParametersWordPressComEncoder: Encodable {
+public struct RemotePostCreateParametersWordPressComEncoder: Encodable {
     let parameters: RemotePostCreateParameters
 
-    func encode(to encoder: Encoder) throws {
+    public init(parameters: RemotePostCreateParameters) {
+        self.parameters = parameters
+    }
+
+    public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: RemotePostWordPressComCodingKeys.self)
         try container.encodeIfPresent(parameters.type, forKey: .type)
         try container.encodeIfPresent(parameters.status, forKey: .status)
@@ -212,10 +216,14 @@ struct RemotePostCreateParametersWordPressComEncoder: Encodable {
     }
 }
 
-struct RemotePostUpdateParametersWordPressComEncoder: Encodable {
+public struct RemotePostUpdateParametersWordPressComEncoder: Encodable {
     let parameters: RemotePostUpdateParameters
 
-    func encode(to encoder: Encoder) throws {
+    public init(parameters: RemotePostUpdateParameters) {
+        self.parameters = parameters
+    }
+
+    public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: RemotePostWordPressComCodingKeys.self)
         try container.encodeIfPresent(parameters.ifNotModifiedSince, forKey: .ifNotModifiedSince)
 
@@ -228,7 +236,7 @@ struct RemotePostUpdateParametersWordPressComEncoder: Encodable {
         try container.encodeIfPresent(parameters.excerpt, forKey: .excerpt)
         try container.encodeIfPresent(parameters.slug, forKey: .slug)
         if let value = parameters.featuredImageID {
-            if let featuredImageID = value {
+            if value != nil {
                 try container.encode(parameters.featuredImageID, forKey: .featuredImageID)
             } else {
                 // Passing `null` doesn't work.
@@ -274,10 +282,14 @@ private enum RemotePostXMLRPCCodingKeys: String, CodingKey {
     static let postTags = "post_tag"
 }
 
-struct RemotePostCreateParametersXMLRPCEncoder: Encodable {
+public struct RemotePostCreateParametersXMLRPCEncoder: Encodable {
     let parameters: RemotePostCreateParameters
 
-    func encode(to encoder: Encoder) throws {
+    public init(parameters: RemotePostCreateParameters) {
+        self.parameters = parameters
+    }
+
+    public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: RemotePostXMLRPCCodingKeys.self)
         try container.encode(parameters.type, forKey: .type)
         try container.encodeIfPresent(parameters.status, forKey: .postStatus)
@@ -309,10 +321,14 @@ struct RemotePostCreateParametersXMLRPCEncoder: Encodable {
     }
 }
 
-struct RemotePostUpdateParametersXMLRPCEncoder: Encodable {
+public struct RemotePostUpdateParametersXMLRPCEncoder: Encodable {
     let parameters: RemotePostUpdateParameters
 
-    func encode(to encoder: Encoder) throws {
+    public init(parameters: RemotePostUpdateParameters) {
+        self.parameters = parameters
+    }
+
+    public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: RemotePostXMLRPCCodingKeys.self)
         try container.encodeIfPresent(parameters.ifNotModifiedSince, forKey: .ifNotModifiedSince)
         try container.encodeIfPresent(parameters.status, forKey: .postStatus)
@@ -324,7 +340,7 @@ struct RemotePostUpdateParametersXMLRPCEncoder: Encodable {
         try container.encodeIfPresent(parameters.excerpt, forKey: .excerpt)
         try container.encodeIfPresent(parameters.slug, forKey: .slug)
         if let value = parameters.featuredImageID {
-            if let featuredImageID = value {
+            if value != nil {
                 try container.encode(parameters.featuredImageID, forKey: .featuredImageID)
             } else {
                 // Passing `null` doesn't work.
diff --git a/Sources/WordPressKit/Services/PostServiceRemoteExtended.swift b/Sources/WordPressKit/Services/PostServiceRemoteExtended.swift
deleted file mode 100644
index fb7ed17e..00000000
--- a/Sources/WordPressKit/Services/PostServiceRemoteExtended.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-import Foundation
-
-public protocol PostServiceRemoteExtended: PostServiceRemote {
-    /// Creates a new post with the given parameters.
-    func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost
-
-    /// Performs a partial update to the existing post.
-    ///
-    /// - throws: ``PostServiceRemoteUpdatePostError`` or oher underlying errors
-    /// (see ``WordPressAPIError``)
-    func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost
-
-    /// Permanently deletes a post with the given ID.
-    ///
-    /// - throws: ``PostServiceRemoteUpdatePostError`` or oher underlying errors
-    /// (see ``WordPressAPIError``)
-    func deletePost(withID postID: Int) async throws
-}
-
-public enum PostServiceRemoteUpdatePostError: Error {
-    /// 409 (Conflict)
-    case conflict
-    /// 404 (Not Found)
-    case notFound
-}
diff --git a/Sources/WordPressKit/Services/PostServiceRemoteREST+Extended.swift b/Sources/WordPressKit/Services/PostServiceRemoteREST+Extended.swift
deleted file mode 100644
index 6637a785..00000000
--- a/Sources/WordPressKit/Services/PostServiceRemoteREST+Extended.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-import Foundation
-
-extension PostServiceRemoteREST: PostServiceRemoteExtended {
-    public func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost {
-        let path = self.path(forEndpoint: "sites/\(siteID)/posts/new?context=edit", withVersion: ._1_2)
-        let parameters = try makeParameters(from: RemotePostCreateParametersWordPressComEncoder(parameters: parameters))
-
-        let response = try await wordPressComRestApi.perform(.post, URLString: path, parameters: parameters).get()
-        return try await decodePost(from: response.body)
-    }
-
-    public func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost {
-        let path = self.path(forEndpoint: "sites/\(siteID)/posts/\(postID)?context=edit", withVersion: ._1_2)
-        let parameters = try makeParameters(from: RemotePostUpdateParametersWordPressComEncoder(parameters: parameters))
-
-        let result = await wordPressComRestApi.perform(.post, URLString: path, parameters: parameters)
-        switch result {
-        case .success(let response):
-            return try await decodePost(from: response.body)
-        case .failure(let error):
-            guard case .endpointError(let error) = error else {
-                throw error
-            }
-            switch error.apiErrorCode ?? "" {
-            case "unknown_post": throw PostServiceRemoteUpdatePostError.notFound
-            case "old-revision": throw PostServiceRemoteUpdatePostError.conflict
-            default: throw error
-            }
-        }
-    }
-
-    public func deletePost(withID postID: Int) async throws {
-        let path = self.path(forEndpoint: "sites/\(siteID)/posts/\(postID)/delete", withVersion: ._1_1)
-        let result = await wordPressComRestApi.perform(.post, URLString: path)
-        switch result {
-        case .success:
-            return
-        case .failure(let error):
-            guard case .endpointError(let error) = error else {
-                throw error
-            }
-            switch error.apiErrorCode ?? "" {
-            case "unknown_post": throw PostServiceRemoteUpdatePostError.notFound
-            default: throw error
-            }
-        }
-    }
-}
-
-// Decodes the post in the background.
-private func decodePost(from object: AnyObject) async throws -> RemotePost {
-    guard let dictionary = object as? [AnyHashable: Any] else {
-        throw WordPressAPIError<WordPressComRestApiEndpointError>.unparsableResponse(response: nil, body: nil)
-    }
-    return PostServiceRemoteREST.remotePost(fromJSONDictionary: dictionary)
-}
-
-private func makeParameters<T: Encodable>(from value: T) throws -> [String: AnyObject] {
-    let encoder = JSONEncoder()
-    encoder.dateEncodingStrategy = .formatted(.wordPressCom)
-    let data = try encoder.encode(value)
-    let object = try JSONSerialization.jsonObject(with: data)
-    guard let dictionary = object as? [String: AnyObject] else {
-        throw URLError(.unknown) // This should never happen
-    }
-    return dictionary
-}
diff --git a/Sources/WordPressKit/Services/PostServiceRemoteXMLRPC+Extended.swift b/Sources/WordPressKit/Services/PostServiceRemoteXMLRPC+Extended.swift
deleted file mode 100644
index b1bccf1a..00000000
--- a/Sources/WordPressKit/Services/PostServiceRemoteXMLRPC+Extended.swift
+++ /dev/null
@@ -1,77 +0,0 @@
-import Foundation
-import wpxmlrpc
-
-extension PostServiceRemoteXMLRPC: PostServiceRemoteExtended {
-    public func createPost(with parameters: RemotePostCreateParameters) async throws -> RemotePost {
-        let dictionary = try makeParameters(from: RemotePostCreateParametersXMLRPCEncoder(parameters: parameters))
-        let parameters = xmlrpcArguments(withExtra: dictionary) as [AnyObject]
-        let response = try await api.call(method: "metaWeblog.newPost", parameters: parameters).get()
-        guard let postID = (response.body as? NSObject)?.numericValue() else {
-            throw URLError(.unknown) // Should never happen
-        }
-        return try await getPost(withID: postID)
-    }
-
-    public func patchPost(withID postID: Int, parameters: RemotePostUpdateParameters) async throws -> RemotePost {
-        let dictionary = try makeParameters(from: RemotePostUpdateParametersXMLRPCEncoder(parameters: parameters))
-        var parameters = xmlrpcArguments(withExtra: dictionary) as [AnyObject]
-        if parameters.count > 0 {
-            parameters[0] = postID as NSNumber
-        }
-        let result = await api.call(method: "metaWeblog.editPost", parameters: parameters)
-        switch result {
-        case .success:
-            return try await getPost(withID: postID as NSNumber)
-        case .failure(let error):
-            guard case .endpointError(let error) = error else {
-                throw error
-            }
-            switch error.code ?? 0 {
-            case 404: throw PostServiceRemoteUpdatePostError.notFound
-            case 409: throw PostServiceRemoteUpdatePostError.conflict
-            default: throw error
-            }
-        }
-    }
-
-    public func deletePost(withID postID: Int) async throws {
-        let parameters = xmlrpcArguments(withExtra: postID) as [AnyObject]
-        let result = await api.call(method: "wp.deletePost", parameters: parameters)
-        switch result {
-        case .success:
-            return
-        case .failure(let error):
-            guard case .endpointError(let error) = error else {
-                throw error
-            }
-            switch error.code ?? 0 {
-            case 404: throw PostServiceRemoteUpdatePostError.notFound
-            default: throw error
-            }
-        }
-    }
-
-    private func getPost(withID postID: NSNumber) async throws -> RemotePost {
-        try await withUnsafeThrowingContinuation { continuation in
-            getPostWithID(postID) { post in
-                guard let post else {
-                    return continuation.resume(throwing: URLError(.unknown)) // Should never happen
-                }
-                continuation.resume(returning: post)
-            } failure: { error in
-                continuation.resume(throwing: error ?? URLError(.unknown))
-            }
-        }
-    }
-}
-
-private func makeParameters<T: Encodable>(from value: T) throws -> [String: AnyObject] {
-    let encoder = PropertyListEncoder()
-    encoder.outputFormat = .xml
-    let data = try encoder.encode(value)
-    let object = try PropertyListSerialization.propertyList(from: data, format: nil)
-    guard let dictionary = object as? [String: AnyObject] else {
-        throw URLError(.unknown) // This should never happen
-    }
-    return dictionary
-}
diff --git a/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift b/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift
index 1bb03baf..a20a1775 100644
--- a/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift
+++ b/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift
@@ -3,7 +3,7 @@ extension DateFormatter {
     /// A `DateFormatter` configured to manage dates compatible with the WordPress.com API.
     ///
     /// - SeeAlso: [https://developer.wordpress.com/docs/api/](https://developer.wordpress.com/docs/api/)
-    static let wordPressCom: DateFormatter = {
+    public static let wordPressCom: DateFormatter = {
         let formatter = DateFormatter()
         formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
         formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) as TimeZone
diff --git a/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift b/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift
index c53cf9ca..bb26a907 100644
--- a/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift
+++ b/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift
@@ -5,8 +5,8 @@ import wpxmlrpc
 ///
 /// Calling this class's url related functions (the ones that changes path, query, etc) does not modify the
 /// original URL string. The URL will be perserved in the final result that's returned by the `build` function.
-final class HTTPRequestBuilder {
-    enum Method: String, CaseIterable {
+public final class HTTPRequestBuilder {
+    public enum Method: String, CaseIterable {
         case get = "GET"
         case post = "POST"
         case put = "PUT"
diff --git a/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift b/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift
index a4680b20..508710f1 100644
--- a/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift
+++ b/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift
@@ -18,14 +18,10 @@ public enum WordPressAPIError<EndpointError>: Error where EndpointError: Localiz
     /// The API call returned an status code that's unacceptable to the endpoint.
     case unacceptableStatusCode(response: HTTPURLResponse, body: Data)
     /// The API call returned an HTTP response that WordPressKit can't parse. Receiving this error could be an indicator that there is an error response that's not handled properly by WordPressKit.
-    case unparsableResponse(response: HTTPURLResponse?, body: Data?, underlyingError: Error)
+    case unparsableResponse(response: HTTPURLResponse?, body: Data?, underlyingError: Error = URLError(.cannotParseResponse))
     /// Other error occured.
     case unknown(underlyingError: Error)
 
-    static func unparsableResponse(response: HTTPURLResponse?, body: Data?) -> Self {
-        return WordPressAPIError<EndpointError>.unparsableResponse(response: response, body: body, underlyingError: URLError(.cannotParseResponse))
-    }
-
     var response: HTTPURLResponse? {
         switch self {
         case .requestEncodingFailure, .connection, .unknown:
diff --git a/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift b/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift
index a0b9d605..c75407ef 100644
--- a/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift
+++ b/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift
@@ -95,11 +95,15 @@ open class WordPressComRestApi: NSObject {
 
     private let backgroundUploads: Bool
 
-    private let localeKey: String
+    public let localeKey: String
+
+    public var localeValue: String {
+        WordPressComLanguageDatabase().deviceLanguage.slug
+    }
 
     @objc public let baseURL: URL
 
-    private var invalidTokenHandler: (() -> Void)?
+    public var invalidTokenHandler: (() -> Void)?
 
     /**
      Configure whether or not the user's preferred language locale should be appended. Defaults to true.
@@ -173,9 +177,6 @@ open class WordPressComRestApi: NSObject {
         }
     }
 
-    @objc func setInvalidTokenHandler(_ handler: @escaping () -> Void) {
-        invalidTokenHandler = handler
-    }
 
     // MARK: Network requests
 
@@ -306,21 +307,6 @@ open class WordPressComRestApi: NSObject {
         return "\(String(describing: oAuthToken)),\(String(describing: userAgent))".hashValue
     }
 
-    private func requestBuilder(URLString: String) throws -> HTTPRequestBuilder {
-        guard let url = URL(string: URLString, relativeTo: baseURL) else {
-            throw URLError(.badURL)
-        }
-
-        var builder = HTTPRequestBuilder(url: url)
-
-        if appendsPreferredLanguageLocale {
-            let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug
-            builder = builder.query(defaults: [URLQueryItem(name: localeKey, value: preferredLanguageIdentifier)])
-        }
-
-        return builder
-    }
-
     @objc public func temporaryFileURL(withExtension fileExtension: String) -> URL {
         assert(!fileExtension.isEmpty, "file Extension cannot be empty")
         let fileName = "\(ProcessInfo.processInfo.globallyUniqueString)_file.\(fileExtension)"
@@ -330,7 +316,7 @@ open class WordPressComRestApi: NSObject {
 
     // MARK: - Async
 
-    private lazy var urlSession: URLSession = {
+    public lazy var urlSession: URLSession = {
         URLSession(configuration: sessionConfiguration(background: false))
     }()
 
@@ -360,15 +346,37 @@ open class WordPressComRestApi: NSObject {
         return configuration
     }
 
-    func perform(
-        _ method: HTTPRequestBuilder.Method,
+    public func upload(
         URLString: String,
-        parameters: [String: AnyObject]? = nil,
+        parameters: [String: AnyObject]?,
+        fileParts: [FilePart],
+        requestEnqueued: RequestEnqueuedBlock? = nil,
         fulfilling progress: Progress? = nil
     ) async -> APIResult<AnyObject> {
-        await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
-            try (JSONSerialization.jsonObject(with: $0) as AnyObject)
+        let builder: HTTPRequestBuilder
+        do {
+            let form = try fileParts.map {
+                try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.fileName, mimeType: $0.mimeType)
+            }
+            builder = try requestBuilder(URLString: URLString)
+                .method(.post)
+                .body(form: form)
+        } catch {
+            return .failure(.requestEncodingFailure(underlyingError: error))
         }
+
+        return await perform(
+            request: builder.query(parameters ?? [:]),
+            fulfilling: progress,
+            decoder: { try JSONSerialization.jsonObject(with: $0) as AnyObject },
+            session: uploadURLSession,
+            invalidTokenHandler: invalidTokenHandler,
+            taskCreated: { taskID in
+                DispatchQueue.main.async {
+                    requestEnqueued?(NSNumber(value: taskID))
+                }
+            }
+        )
     }
 
     func perform<T: Decodable>(
@@ -384,8 +392,25 @@ open class WordPressComRestApi: NSObject {
             return try decoder.decode(type, from: $0)
         }
     }
+}
+
+extension WordPressComRESTAPIInterfacing {
+
+    public typealias APIResult<T> = WordPressAPIResult<HTTPAPIResponse<T>, WordPressComRestApiEndpointError>
 
-    private func perform<T>(
+    public func perform(
+        _ method: HTTPRequestBuilder.Method,
+        URLString: String,
+        parameters: [String: AnyObject]? = nil,
+        fulfilling progress: Progress? = nil
+    ) async -> APIResult<AnyObject> {
+        await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
+            try (JSONSerialization.jsonObject(with: $0) as AnyObject)
+        }
+    }
+
+    // FIXME: This was private. It became public during the extraction. Consider whether to make it privated once done.
+    public func perform<T>(
         _ method: HTTPRequestBuilder.Method,
         URLString: String,
         parameters: [String: AnyObject]?,
@@ -394,8 +419,7 @@ open class WordPressComRestApi: NSObject {
     ) async -> APIResult<T> {
         var builder: HTTPRequestBuilder
         do {
-            builder = try requestBuilder(URLString: URLString)
-                .method(method)
+            builder = try requestBuilder(URLString: URLString).method(method)
         } catch {
             return .failure(.requestEncodingFailure(underlyingError: error))
         }
@@ -408,17 +432,25 @@ open class WordPressComRestApi: NSObject {
             }
         }
 
-        return await perform(request: builder, fulfilling: progress, decoder: decoder)
+        return await perform(
+            request: builder,
+            fulfilling: progress,
+            decoder: decoder,
+            session: urlSession,
+            invalidTokenHandler: invalidTokenHandler
+        )
     }
 
-    private func perform<T>(
+    // FIXME: This was private. It became public during the extraction. Consider whether to make it privated once done.
+    public func perform<T>(
         request: HTTPRequestBuilder,
         fulfilling progress: Progress?,
         decoder: @escaping (Data) throws -> T,
-        taskCreated: ((Int) -> Void)? = nil,
-        session: URLSession? = nil
+        session: URLSession,
+        invalidTokenHandler: (() -> Void)?,
+        taskCreated: ((Int) -> Void)? = nil
     ) async -> APIResult<T> {
-        await (session ?? self.urlSession)
+        await session
             .perform(request: request, taskCreated: taskCreated, fulfilling: progress, errorType: WordPressComRestApiEndpointError.self)
             .mapSuccess { response -> HTTPAPIResponse<T> in
                 let object = try decoder(response.body)
@@ -426,7 +458,7 @@ open class WordPressComRestApi: NSObject {
                 return HTTPAPIResponse(response: response.response, body: object)
             }
             .mapUnacceptableStatusCodeError { response, body in
-                if let error = self.processError(response: response, body: body, additionalUserInfo: nil) {
+                if let error = self.processError(response: response, body: body, additionalUserInfo: nil, invalidTokenHandler: invalidTokenHandler) {
                     return error
                 }
 
@@ -444,45 +476,28 @@ open class WordPressComRestApi: NSObject {
             }
     }
 
-    public func upload(
-        URLString: String,
-        parameters: [String: AnyObject]?,
-        fileParts: [FilePart],
-        requestEnqueued: RequestEnqueuedBlock? = nil,
-        fulfilling progress: Progress? = nil
-    ) async -> APIResult<AnyObject> {
-        let builder: HTTPRequestBuilder
-        do {
-            let form = try fileParts.map {
-                try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.fileName, mimeType: $0.mimeType)
-            }
-            builder = try requestBuilder(URLString: URLString)
-                .method(.post)
-                .body(form: form)
-        } catch {
-            return .failure(.requestEncodingFailure(underlyingError: error))
+    func requestBuilder(URLString: String) throws -> HTTPRequestBuilder {
+        let locale: (String, String)?
+        if appendsPreferredLanguageLocale {
+            locale = (localeKey, localeValue)
+        } else {
+            locale = nil
         }
 
-        return await perform(
-            request: builder.query(parameters ?? [:]),
-            fulfilling: progress,
-            decoder: { try JSONSerialization.jsonObject(with: $0) as AnyObject },
-            taskCreated: { taskID in
-                DispatchQueue.main.async {
-                    requestEnqueued?(NSNumber(value: taskID))
-                }
-            },
-            session: uploadURLSession
-        )
+        return try HTTPRequestBuilder.with(URLString: URLString, relativeTo: baseURL, appendingLocale: locale)
     }
-
 }
 
 // MARK: - Error processing
 
-extension WordPressComRestApi {
+extension WordPressComRESTAPIInterfacing {
 
-    func processError(response httpResponse: HTTPURLResponse, body data: Data, additionalUserInfo: [String: Any]?) -> WordPressComRestApiEndpointError? {
+    func processError(
+        response httpResponse: HTTPURLResponse,
+        body data: Data,
+        additionalUserInfo: [String: Any]?,
+        invalidTokenHandler: (() -> Void)?
+    ) -> WordPressComRestApiEndpointError? {
         // Not sure if it's intentional to include 500 status code, but the code seems to be there from the very beginning.
         // https://github.com/wordpress-mobile/WordPressKit-iOS/blob/1.0.1/WordPressKit/WordPressComRestApi.swift#L374
         guard (400...500).contains(httpResponse.statusCode) else {
@@ -528,7 +543,7 @@ extension WordPressComRestApi {
         if mappedError == .invalidToken {
             // Call `invalidTokenHandler in the main thread since it's typically used by the apps to present an authentication UI.
             DispatchQueue.main.async {
-                self.invalidTokenHandler?()
+                invalidTokenHandler?()
             }
         }
 
@@ -564,6 +579,28 @@ extension WordPressComRestApi {
         )
     }
 }
+
+extension HTTPRequestBuilder {
+
+    static func with(
+        URLString: String,
+        relativeTo baseURL: URL,
+        appendingLocale locale: (key: String, value: String)?
+    ) throws -> HTTPRequestBuilder {
+        guard let url = URL(string: URLString, relativeTo: baseURL) else {
+            throw URLError(.badURL)
+        }
+
+        let builder = Self.init(url: url)
+
+        guard let locale else {
+            return builder
+        }
+
+        return builder.query(defaults: [URLQueryItem(name: locale.key, value: locale.value)])
+    }
+
+}
 // MARK: - Anonymous API support
 
 extension WordPressComRestApi {
diff --git a/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift b/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift
index 2698c89f..4bb825eb 100644
--- a/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift
+++ b/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift
@@ -180,7 +180,7 @@ open class WordPressOrgXMLRPCApi: NSObject {
     /// - Parameters:
     ///   - streaming: set to `true` if there are large data (i.e. uploading files) in given `parameters`. `false` by default.
     /// - Returns: A `Result` type that contains the XMLRPC success or failure result.
-    func call(method: String, parameters: [AnyObject]?, fulfilling progress: Progress? = nil, streaming: Bool = false) async -> WordPressAPIResult<HTTPAPIResponse<AnyObject>, WordPressOrgXMLRPCApiFault> {
+    public func call(method: String, parameters: [AnyObject]?, fulfilling progress: Progress? = nil, streaming: Bool = false) async -> WordPressAPIResult<HTTPAPIResponse<AnyObject>, WordPressOrgXMLRPCApiFault> {
         let session = streaming ? uploadURLSession : urlSession
         let builder = HTTPRequestBuilder(url: endpoint)
             .method(.post)
diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj
index 393460ce..7a663384 100644
--- a/WordPressKit.xcodeproj/project.pbxproj
+++ b/WordPressKit.xcodeproj/project.pbxproj
@@ -13,9 +13,6 @@
 		0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; };
 		0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0847B92B2A4442730044D32F /* IPLocationRemote.swift */; };
 		08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */; };
-		0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */; };
-		0C1C08432B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */; };
-		0C1C08452B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */; };
 		0C9CD7992B9A107E0045BE03 /* RemotePostParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9CD7982B9A107E0045BE03 /* RemotePostParameters.swift */; };
 		0CB1905E2A2A5E83004D3E80 /* BlazeCampaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */; };
 		0CB190612A2A6A13004D3E80 /* blaze-campaigns-search.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */; };
@@ -757,9 +754,6 @@
 		0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = "<group>"; };
 		0847B92B2A4442730044D32F /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = "<group>"; };
 		08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = "<group>"; };
-		0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceRemoteExtended.swift; sourceTree = "<group>"; };
-		0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostServiceRemoteREST+Extended.swift"; sourceTree = "<group>"; };
-		0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostServiceRemoteXMLRPC+Extended.swift"; sourceTree = "<group>"; };
 		0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
 		0C9CD7982B9A107E0045BE03 /* RemotePostParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePostParameters.swift; sourceTree = "<group>"; };
 		0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaign.swift; sourceTree = "<group>"; };
@@ -1779,15 +1773,12 @@
 				E1BD95141FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift */,
 				E13EE1461F33258E00C15787 /* PluginServiceRemote.swift */,
 				740B23B21F17EC7300067A2A /* PostServiceRemote.h */,
-				0C1C08402B9CD79900E52F8C /* PostServiceRemoteExtended.swift */,
 				740B23BC1F17ECB500067A2A /* PostServiceRemoteOptions.h */,
 				740B23B31F17EC7300067A2A /* PostServiceRemoteREST.h */,
 				740B23B41F17EC7300067A2A /* PostServiceRemoteREST.m */,
-				0C1C08422B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift */,
 				9AF4F2FB218331DC00570E4B /* PostServiceRemoteREST+Revisions.swift */,
 				740B23B51F17EC7300067A2A /* PostServiceRemoteXMLRPC.h */,
 				740B23B61F17EC7300067A2A /* PostServiceRemoteXMLRPC.m */,
-				0C1C08442B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift */,
 				F181EA0127184D3C00F26141 /* ProductServiceRemote.swift */,
 				74A44DCA1F13C533006CD8F4 /* PushAuthenticationServiceRemote.swift */,
 				C7A09A4F284104DB003096ED /* QR Login */,
@@ -3342,18 +3333,15 @@
 				436D56352118D85800CEAA33 /* WPCountry.swift in Sources */,
 				74A44DCB1F13C533006CD8F4 /* NotificationSettingsServiceRemote.swift in Sources */,
 				FAD1344525908F5F00A8FEB1 /* JetpackBackupServiceRemote.swift in Sources */,
-				0C1C08452B9CDB0B00E52F8C /* PostServiceRemoteXMLRPC+Extended.swift in Sources */,
 				F1BB7806240FB90B0030ADDC /* AtomicAuthenticationServiceRemote.swift in Sources */,
 				404057CE221C38130060250C /* StatsTopVideosTimeIntervalData.swift in Sources */,
 				7E0D64FF22D855700092AD10 /* EditorServiceRemote.swift in Sources */,
-				0C1C08412B9CD79900E52F8C /* PostServiceRemoteExtended.swift in Sources */,
 				9AF4F2FF2183346B00570E4B /* RemoteRevision.swift in Sources */,
 				FE6C673A2BB739950083ECAB /* Decodable+Dictionary.swift in Sources */,
 				17D936252475D8AB008B2205 /* RemoteHomepageType.swift in Sources */,
 				74BA04F41F06DC0A00ED5CD8 /* CommentServiceRemoteREST.m in Sources */,
 				74C473AC1EF2F75E009918F2 /* SiteManagementServiceRemote.swift in Sources */,
 				74585B971F0D54B400E7E667 /* RemoteDomain.swift in Sources */,
-				0C1C08432B9CD8D200E52F8C /* PostServiceRemoteREST+Extended.swift in Sources */,
 				3FFCC0492BAB98130051D229 /* DateFormatter+WordPressCom.swift in Sources */,
 				74A44DD01F13C64B006CD8F4 /* RemoteNotification.swift in Sources */,
 				E1D6B558200E473A00325669 /* TimeZoneServiceRemote.swift in Sources */,