diff --git a/api/proto/common/commands/editor_commands.proto b/api/proto/common/commands/editor_commands.proto
index b1b8ff7103..7718256f53 100644
--- a/api/proto/common/commands/editor_commands.proto
+++ b/api/proto/common/commands/editor_commands.proto
@@ -47,6 +47,46 @@ message GetOpenDocumentsResponse
   repeated kiapi.common.types.DocumentSpecifier documents = 1;
 }
 
+message SaveDocument
+{
+  kiapi.common.types.DocumentSpecifier document = 1;
+}
+
+// Saves the given document to a new location and opens the new copy
+// Note, this is not going to be implemented anytime soon as we don't currently
+// want to allow API access to changing which project is open
+//message SaveDocumentAs
+//{
+//  kiapi.common.types.DocumentSpecifier document = 1;
+//
+//  string path = 2;
+//}
+
+message SaveOptions
+{
+  // Overwrite destination file(s) if they exist
+  bool overwrite = 1;
+
+  // If the file being saved normally requires a project (for example, a board or schematic),
+  // this flag will cause a new project to be saved alongside the new file
+  bool include_project = 2;
+}
+
+// Saves the given document to a new location and does not open the new copy
+message SaveCopyOfDocument
+{
+  kiapi.common.types.DocumentSpecifier document = 1;
+
+  string path = 2;
+
+  SaveOptions options = 3;
+}
+
+message RevertDocument
+{
+  kiapi.common.types.DocumentSpecifier document = 1;
+}
+
 /*
  * Runs a TOOL_ACTION using the TOOL_MANAGER of a given frame.
  * WARNING: The TOOL_ACTIONs are specifically *not* an API.
diff --git a/pcbnew/api/api_handler_pcb.cpp b/pcbnew/api/api_handler_pcb.cpp
index 51473f41a6..d657571585 100644
--- a/pcbnew/api/api_handler_pcb.cpp
+++ b/pcbnew/api/api_handler_pcb.cpp
@@ -37,6 +37,7 @@
 #include <pcb_text.h>
 #include <pcb_textbox.h>
 #include <pcb_track.h>
+#include <pcbnew_id.h>
 #include <project.h>
 #include <tool/tool_manager.h>
 #include <tools/pcb_actions.h>
@@ -58,7 +59,9 @@ API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* aFrame ) :
     registerHandler<RunAction, RunActionResponse>( &API_HANDLER_PCB::handleRunAction );
     registerHandler<GetOpenDocuments, GetOpenDocumentsResponse>(
             &API_HANDLER_PCB::handleGetOpenDocuments );
-
+    registerHandler<SaveDocument, Empty>( &API_HANDLER_PCB::handleSaveDocument );
+    registerHandler<SaveCopyOfDocument, Empty>( &API_HANDLER_PCB::handleSaveCopyOfDocument );
+    registerHandler<RevertDocument, Empty>( &API_HANDLER_PCB::handleRevertDocument );
 
     registerHandler<GetItems, GetItemsResponse>( &API_HANDLER_PCB::handleGetItems );
 
@@ -147,6 +150,103 @@ HANDLER_RESULT<GetOpenDocumentsResponse> API_HANDLER_PCB::handleGetOpenDocuments
 }
 
 
+HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSaveDocument(
+        const HANDLER_CONTEXT<SaveDocument>& aCtx )
+{
+    if( std::optional<ApiResponseStatus> busy = checkForBusy() )
+        return tl::unexpected( *busy );
+
+    HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
+
+    if( !documentValidation )
+        return tl::unexpected( documentValidation.error() );
+
+    frame()->Files_io_from_id( ID_SAVE_BOARD );
+    return Empty();
+}
+
+
+HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSaveCopyOfDocument(
+        const HANDLER_CONTEXT<SaveCopyOfDocument>& aCtx )
+{
+    if( std::optional<ApiResponseStatus> busy = checkForBusy() )
+        return tl::unexpected( *busy );
+
+    HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
+
+    if( !documentValidation )
+        return tl::unexpected( documentValidation.error() );
+
+    wxFileName boardPath( frame()->Prj().AbsolutePath( wxString::FromUTF8( aCtx.Request.path() ) ) );
+
+    if( !boardPath.IsOk() || !boardPath.IsDirWritable() )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_BAD_REQUEST );
+        e.set_error_message( fmt::format( "save path '{}' could not be opened",
+                                          boardPath.GetFullPath().ToStdString() ) );
+        return tl::unexpected( e );
+    }
+
+    if( boardPath.FileExists()
+        && ( !boardPath.IsFileWritable() || !aCtx.Request.options().overwrite() ) )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_BAD_REQUEST );
+        e.set_error_message( fmt::format( "save path '{}' exists and cannot be overwritten",
+                                          boardPath.GetFullPath().ToStdString() ) );
+        return tl::unexpected( e );
+    }
+
+    if( boardPath.GetExt() != FILEEXT::KiCadPcbFileExtension )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_BAD_REQUEST );
+        e.set_error_message( fmt::format( "save path '{}' must have a kicad_pcb extension",
+                                          boardPath.GetFullPath().ToStdString() ) );
+        return tl::unexpected( e );
+    }
+
+    BOARD* board = frame()->GetBoard();
+
+    if( board->GetFileName().Matches( boardPath.GetFullPath() ) )
+    {
+        frame()->Files_io_from_id( ID_SAVE_BOARD );
+        return Empty();
+    }
+
+    bool includeProject = true;
+
+    if( aCtx.Request.has_options() )
+        includeProject = aCtx.Request.options().include_project();
+
+    frame()->SavePcbCopy( boardPath.GetFullPath(), includeProject, /* aHeadless = */ true );
+
+    return Empty();
+}
+
+
+HANDLER_RESULT<Empty> API_HANDLER_PCB::handleRevertDocument(
+        const HANDLER_CONTEXT<RevertDocument>& aCtx )
+{
+    if( std::optional<ApiResponseStatus> busy = checkForBusy() )
+        return tl::unexpected( *busy );
+
+    HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
+
+    if( !documentValidation )
+        return tl::unexpected( documentValidation.error() );
+
+    wxFileName fn = frame()->Prj().AbsolutePath( frame()->GetBoard()->GetFileName() );
+
+    frame()->GetScreen()->SetContentModified( false );
+    frame()->ReleaseFile();
+    frame()->OpenProjectFiles( std::vector<wxString>( 1, fn.GetFullPath() ), KICTL_REVERT );
+
+    return Empty();
+}
+
+
 void API_HANDLER_PCB::pushCurrentCommit( const std::string& aClientName, const wxString& aMessage )
 {
     API_HANDLER_EDITOR::pushCurrentCommit( aClientName, aMessage );
diff --git a/pcbnew/api/api_handler_pcb.h b/pcbnew/api/api_handler_pcb.h
index 3605b353f8..f4d1a43824 100644
--- a/pcbnew/api/api_handler_pcb.h
+++ b/pcbnew/api/api_handler_pcb.h
@@ -63,6 +63,14 @@ private:
     HANDLER_RESULT<commands::GetOpenDocumentsResponse> handleGetOpenDocuments(
             const HANDLER_CONTEXT<commands::GetOpenDocuments>& aCtx );
 
+    HANDLER_RESULT<Empty> handleSaveDocument( const HANDLER_CONTEXT<commands::SaveDocument>& aCtx );
+
+    HANDLER_RESULT<Empty> handleSaveCopyOfDocument(
+            const HANDLER_CONTEXT<commands::SaveCopyOfDocument>& aCtx );
+
+    HANDLER_RESULT<Empty> handleRevertDocument(
+            const HANDLER_CONTEXT<commands::RevertDocument>& aCtx );
+
     HANDLER_RESULT<commands::GetItemsResponse> handleGetItems(
             const HANDLER_CONTEXT<commands::GetItems>& aCtx );
 
diff --git a/pcbnew/files.cpp b/pcbnew/files.cpp
index b4849fbf4a..a75d7b0a09 100644
--- a/pcbnew/files.cpp
+++ b/pcbnew/files.cpp
@@ -1122,14 +1122,17 @@ bool PCB_EDIT_FRAME::SavePcbFile( const wxString& aFileName, bool addToHistory,
 }
 
 
-bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject )
+bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject, bool aHeadless )
 {
     wxFileName pcbFileName( EnsureFileExtension( aFileName, FILEEXT::KiCadPcbFileExtension ) );
 
     if( !IsWritable( pcbFileName ) )
     {
-        DisplayError( this, wxString::Format( _( "Insufficient permissions to write file '%s'." ),
-                                              pcbFileName.GetFullPath() ) );
+        if( !aHeadless )
+        {
+            DisplayError( this, wxString::Format( _( "Insufficient permissions to write file '%s'." ),
+                                                  pcbFileName.GetFullPath() ) );
+        }
         return false;
     }
 
@@ -1149,9 +1152,12 @@ bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject
     }
     catch( const IO_ERROR& ioe )
     {
-        DisplayError( this, wxString::Format( _( "Error saving board file '%s'.\n%s" ),
-                                              pcbFileName.GetFullPath(),
-                                              ioe.What() ) );
+        if( !aHeadless )
+        {
+            DisplayError( this, wxString::Format( _( "Error saving board file '%s'.\n%s" ),
+                                                  pcbFileName.GetFullPath(),
+                                                  ioe.What() ) );
+        }
 
         return false;
     }
@@ -1171,14 +1177,17 @@ bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject
     if( aCreateProject && currentRules.FileExists() && !rulesFile.FileExists() )
         KiCopyFile( currentRules.GetFullPath(), rulesFile.GetFullPath(), msg );
 
-    if( !msg.IsEmpty() )
+    if( !msg.IsEmpty() && !aHeadless )
     {
         DisplayError( this, wxString::Format( _( "Error saving custom rules file '%s'." ),
                                               rulesFile.GetFullPath() ) );
     }
 
-    DisplayInfoMessage( this, wxString::Format( _( "Board copied to:\n%s" ),
-                                                pcbFileName.GetFullPath() ) );
+    if( !aHeadless )
+    {
+        DisplayInfoMessage( this, wxString::Format( _( "Board copied to:\n%s" ),
+                                                    pcbFileName.GetFullPath() ) );
+    }
 
     return true;
 }
diff --git a/pcbnew/pcb_edit_frame.h b/pcbnew/pcb_edit_frame.h
index 3e3cdfc284..f1a361548a 100644
--- a/pcbnew/pcb_edit_frame.h
+++ b/pcbnew/pcb_edit_frame.h
@@ -423,9 +423,11 @@ public:
      *
      * @param aFileName The file name to write.
      * @param aCreateProject will create an empty project alongside the board file
+     * @param aHeadless will suppress informational output (e.g. to be used from the API)
      * @return True if file was saved successfully.
      */
-    bool SavePcbCopy( const wxString& aFileName, bool aCreateProject = false );
+    bool SavePcbCopy( const wxString& aFileName, bool aCreateProject = false,
+                      bool aHeadless = false );
 
     /**
      * Delete all and reinitialize the current board.