diff --git a/api/CMakeLists.txt b/api/CMakeLists.txt
index 48c41b1655..824cf80bb5 100644
--- a/api/CMakeLists.txt
+++ b/api/CMakeLists.txt
@@ -132,3 +132,19 @@ if( APPLE )
             "@executable_path/../Frameworks" )
     set_target_properties( kiapi PROPERTIES BUILD_WITH_INSTALL_RPATH 1 )
 endif()
+
+if( NOT (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_BINARY_DIR} ) )
+    file( GLOB SCHEMA_FILES ${CMAKE_CURRENT_SOURCE_DIR}/schemas/*.json )
+
+    add_custom_target( api_schema_build_copy ALL
+        COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/schemas ${CMAKE_BINARY_DIR}/schemas
+        DEPENDS ${SCHEMA_FILES}
+        COMMENT "Copying API schema files into build directory"
+        )
+endif()
+
+INSTALL( DIRECTORY
+    schemas
+    DESTINATION ${KICAD_DATA}
+    COMPONENT Runtime
+)
diff --git a/api/schemas/api.v1.schema.json b/api/schemas/api.v1.schema.json
new file mode 100644
index 0000000000..8c82dfd197
--- /dev/null
+++ b/api/schemas/api.v1.schema.json
@@ -0,0 +1,108 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://go.kicad.org/api/schemas/v1",
+  "title": "KiCad API Plugin Schema",
+  "description": "KiCad IPC API Plugin and Action schema",
+  "$ref": "#/definitions/Plugin",
+  "definitions": {
+    "Plugin": {
+      "type": "object",
+      "properties": {
+        "identifier": {
+          "type": "string",
+          "description": "Plugin identifier",
+          "pattern": "^[a-zA-Z][-_a-zA-Z0-9.]{0,98}[a-zA-Z0-9]$"
+        },
+        "name": {
+          "type": "string",
+          "maxLength": 200
+        },
+        "description": {
+          "type": "string",
+          "maxLength": 500
+        },
+        "runtime": {
+          "type": "object",
+          "properties": {
+            "type": {
+              "type": "string",
+              "enum": [
+                "python",
+                "exec"
+              ],
+              "description": "How KiCad should launch the plugin"
+            },
+            "min_version": {
+              "type": "string"
+            }
+          },
+          "required": [
+            "type"
+          ]
+        },
+        "actions": {
+          "type": "array",
+          "items": {
+            "type": "object",
+            "properties": {
+              "identifier": {
+                "type": "string"
+              },
+              "name": {
+                "type": "string"
+              },
+              "description": {
+                "type": "string"
+              },
+              "show-button": {
+                "type": "boolean"
+              },
+              "scopes": {
+                "type": "array",
+                "items": {
+                  "type": "string",
+                  "enum": [
+                    "pcb",
+                    "schematic",
+                    "footprint",
+                    "symbol",
+                    "project_manager"
+                  ]
+                }
+              },
+              "entrypoint": {
+                "type": "string"
+              },
+              "icons-light": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "icons-dark": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              }
+            },
+            "required": [
+              "identifier",
+              "name",
+              "description",
+              "show-button",
+              "entrypoint"
+            ]
+          }
+        }
+      },
+      "required": [
+        "identifier",
+        "name",
+        "description",
+        "runtime",
+        "actions"
+      ]
+    }
+  }
+}
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index a17a54068b..1d5bf654d7 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -161,6 +161,7 @@ set( KICOMMON_SRCS
     gestfich.cpp
     increment.cpp
     json_conversions.cpp
+    json_schema_validator.cpp
     kidialog.cpp
     kiid.cpp
     kiway.cpp
@@ -226,6 +227,7 @@ target_link_libraries( kicommon
     kimath
     kiplatform
     nlohmann_json
+    nlohmann_json_schema_validator
     fmt::fmt
     CURL::libcurl
     picosha2
@@ -304,6 +306,7 @@ target_include_directories( kicommon
         $<TARGET_PROPERTY:expected,INTERFACE_INCLUDE_DIRECTORIES>
         $<TARGET_PROPERTY:kiapi,INTERFACE_INCLUDE_DIRECTORIES>
         $<TARGET_PROPERTY:picosha2,INTERFACE_INCLUDE_DIRECTORIES>
+        $<TARGET_PROPERTY:nlohmann_json_schema_validator,INTERFACE_INCLUDE_DIRECTORIES>
     )
 
 add_dependencies( kicommon pegtl version_header )
diff --git a/common/api/api_plugin.cpp b/common/api/api_plugin.cpp
index 750f569ee3..5ae559be4b 100644
--- a/common/api/api_plugin.cpp
+++ b/common/api/api_plugin.cpp
@@ -29,6 +29,28 @@
 #include <api/api_plugin_manager.h>
 #include <api/api_utils.h>
 #include <json_conversions.h>
+#include <json_schema_validator.h>
+
+
+class LOGGING_ERROR_HANDLER : public nlohmann::json_schema::error_handler
+{
+public:
+    LOGGING_ERROR_HANDLER() : m_hasError( false ) {}
+
+    bool HasError() const { return m_hasError; }
+
+    void error( const nlohmann::json::json_pointer& ptr, const nlohmann::json& instance,
+                const std::string& message ) override
+    {
+        m_hasError = true;
+        wxLogTrace( traceApi,
+                    wxString::Format( wxS( "JSON error: at %s, value:\n%s\n%s" ),
+                                      ptr.to_string(), instance.dump(), message ) );
+    }
+
+private:
+    bool m_hasError;
+};
 
 
 bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
@@ -54,7 +76,8 @@ bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
 
 struct API_PLUGIN_CONFIG
 {
-    API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile );
+    API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
+                       const JSON_SCHEMA_VALIDATOR& aValidator );
 
     bool valid;
     wxString identifier;
@@ -67,7 +90,8 @@ struct API_PLUGIN_CONFIG
 };
 
 
-API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile ) :
+API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
+                                      const JSON_SCHEMA_VALIDATOR& aValidator ) :
         parent( aParent )
 {
     valid = false;
@@ -94,7 +118,11 @@ API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aCo
         return;
     }
 
-    // TODO add schema and validate
+    LOGGING_ERROR_HANDLER handler;
+    aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
+
+    if( !handler.HasError() )
+        wxLogTrace( traceApi, "Plugin: schema validation successful" );
 
     // All of these are required; any exceptions here leave us with valid == false
     try
@@ -151,9 +179,9 @@ API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aCo
 }
 
 
-API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile ) :
+API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
         m_configFile( aConfigFile ),
-        m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile ) )
+        m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
 {
 }
 
diff --git a/common/api/api_plugin_manager.cpp b/common/api/api_plugin_manager.cpp
index f4c430f581..1556b6c30a 100644
--- a/common/api/api_plugin_manager.cpp
+++ b/common/api/api_plugin_manager.cpp
@@ -18,6 +18,8 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <fstream>
+
 #include <env_vars.h>
 #include <fmt/format.h>
 #include <wx/dir.h>
@@ -42,6 +44,13 @@ API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
         wxEvtHandler(),
         m_parent( aEvtHandler )
 {
+    // Read and store pcm schema
+    wxFileName schemaFile( PATHS::GetStockDataPath( true ), wxS( "api.v1.schema.json" ) );
+    schemaFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
+    schemaFile.AppendDir( wxS( "schemas" ) );
+
+    m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schemaFile );
+
     Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
 }
 
@@ -90,7 +99,7 @@ void API_PLUGIN_MANAGER::ReloadPlugins()
                 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
                                                         aFile.GetFullPath() ) );
 
-                auto plugin = std::make_unique<API_PLUGIN>( aFile );
+                auto plugin = std::make_unique<API_PLUGIN>( aFile, *m_schema_validator );
 
                 if( plugin->IsOk() )
                 {
diff --git a/common/json_schema_validator.cpp b/common/json_schema_validator.cpp
new file mode 100644
index 0000000000..122f640e0d
--- /dev/null
+++ b/common/json_schema_validator.cpp
@@ -0,0 +1,64 @@
+/*
+* This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fstream>
+#include <wx/filename.h>
+#include <wx/log.h>
+
+#include <json_schema_validator.h>
+#include <locale_io.h>
+
+
+JSON_SCHEMA_VALIDATOR::JSON_SCHEMA_VALIDATOR( const wxFileName& aSchemaFile )
+{
+    std::ifstream schema_stream( aSchemaFile.GetFullPath().fn_str() );
+    nlohmann::json schema;
+
+    try
+    {
+        // For some obscure reason on MINGW, using UCRT option,
+        // m_schema_validator.set_root_schema() hangs without switching to locale "C"
+#if defined(__MINGW32__) && defined(_UCRT)
+        LOCALE_IO dummy;
+#endif
+
+        schema_stream >> schema;
+        m_validator.set_root_schema( schema );
+    }
+    catch( std::exception& e )
+    {
+        if( !aSchemaFile.FileExists() )
+        {
+            wxLogError( wxString::Format( _( "schema file '%s' not found" ),
+                                          aSchemaFile.GetFullPath() ) );
+        }
+        else
+        {
+            wxLogError( wxString::Format( _( "Error loading schema: %s" ), e.what() ) );
+        }
+    }
+}
+
+
+nlohmann::json JSON_SCHEMA_VALIDATOR::Validate( const nlohmann::json& aJson,
+                             nlohmann::json_schema::error_handler& aErrorHandler,
+                             const nlohmann::json_uri& aInitialUri ) const
+{
+    return m_validator.validate( aJson, aErrorHandler, aInitialUri );
+}
diff --git a/include/api/api_plugin.h b/include/api/api_plugin.h
index 1eab95c921..6f7281e8c1 100644
--- a/include/api/api_plugin.h
+++ b/include/api/api_plugin.h
@@ -34,6 +34,7 @@
 
 struct API_PLUGIN_CONFIG;
 class API_PLUGIN;
+class JSON_SCHEMA_VALIDATOR;
 
 
 struct PLUGIN_DEPENDENCY
@@ -104,7 +105,7 @@ struct PLUGIN_ACTION
 class KICOMMON_API API_PLUGIN
 {
 public:
-    API_PLUGIN( const wxFileName& aConfigFile );
+    API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator );
 
     ~API_PLUGIN();
 
diff --git a/include/api/api_plugin_manager.h b/include/api/api_plugin_manager.h
index 6c8db54e07..40c6513156 100644
--- a/include/api/api_plugin_manager.h
+++ b/include/api/api_plugin_manager.h
@@ -25,6 +25,7 @@
 #include <wx/event.h>
 
 #include <api/api_plugin.h>
+#include <json_schema_validator.h>
 #include <kicommon.h>
 
 /// Internal event used for handling async tasks
@@ -98,4 +99,6 @@ private:
     };
 
     std::deque<JOB> m_jobs;
+
+    std::unique_ptr<JSON_SCHEMA_VALIDATOR> m_schema_validator;
 };
diff --git a/include/json_schema_validator.h b/include/json_schema_validator.h
new file mode 100644
index 0000000000..648fd2c963
--- /dev/null
+++ b/include/json_schema_validator.h
@@ -0,0 +1,43 @@
+/*
+* This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef JSON_SCHEMA_VALIDATOR_H
+#define JSON_SCHEMA_VALIDATOR_H
+
+#include <wx/filename.h>
+#include <kicommon.h>
+#include <json_common.h>
+#include <nlohmann/json-schema.hpp>
+
+class KICOMMON_API JSON_SCHEMA_VALIDATOR
+{
+public:
+    JSON_SCHEMA_VALIDATOR( const wxFileName& aSchemaFile );
+
+    ~JSON_SCHEMA_VALIDATOR() = default;
+
+    nlohmann::json Validate( const nlohmann::json& aJson,
+                             nlohmann::json_schema::error_handler& aErrorHandler,
+                             const nlohmann::json_uri& aInitialUri = nlohmann::json_uri("#") ) const;
+
+private:
+    nlohmann::json_schema::json_validator m_validator;
+};
+
+#endif //JSON_SCHEMA_VALIDATOR_H
diff --git a/kicad/pcm/CMakeLists.txt b/kicad/pcm/CMakeLists.txt
index 4d56751046..fe1d6f5449 100644
--- a/kicad/pcm/CMakeLists.txt
+++ b/kicad/pcm/CMakeLists.txt
@@ -40,7 +40,6 @@ target_link_libraries( pcm
     ${wxWidgets_LIBRARIES}
     common
     picosha2
-    nlohmann_json_schema_validator
 )
 
 # Copy the schema to the build directory when building outside the source tree
diff --git a/kicad/pcm/pcm.cpp b/kicad/pcm/pcm.cpp
index b56ac86828..d4ca2cc4df 100644
--- a/kicad/pcm/pcm.cpp
+++ b/kicad/pcm/pcm.cpp
@@ -26,6 +26,7 @@
 #include "core/wx_stl_compat.h"
 #include <env_vars.h>
 #include <background_jobs_monitor.h>
+#include <json_schema_validator.h>
 #include "build_version.h"
 #include "paths.h"
 #include "pcm.h"
@@ -81,32 +82,7 @@ PLUGIN_CONTENT_MANAGER::PLUGIN_CONTENT_MANAGER(
     schema_file.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
     schema_file.AppendDir( wxS( "schemas" ) );
 
-    std::ifstream  schema_stream( schema_file.GetFullPath().fn_str() );
-    nlohmann::json schema;
-
-    try
-    {
-        // For some obscure reason on MINGW, using UCRT option,
-        // m_schema_validator.set_root_schema() hangs without switching to locale "C"
-        #if defined(__MINGW32__) && defined(_UCRT)
-        LOCALE_IO dummy;
-        #endif
-
-        schema_stream >> schema;
-        m_schema_validator.set_root_schema( schema );
-    }
-    catch( std::exception& e )
-    {
-        if( !schema_file.FileExists() )
-        {
-            wxLogError( wxString::Format( _( "schema file '%s' not found" ),
-                                          schema_file.GetFullPath() ) );
-        }
-        else
-        {
-            wxLogError( wxString::Format( _( "Error loading schema: %s" ), e.what() ) );
-        }
-    }
+    m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schema_file );
 
     // Load currently installed packages
     wxFileName f( PATHS::GetUserSettingsPath(), wxT( "installed_packages.json" ) );
@@ -314,7 +290,7 @@ void PLUGIN_CONTENT_MANAGER::ValidateJson( const nlohmann::json&     aJson,
                                            const nlohmann::json_uri& aUri ) const
 {
     THROWING_ERROR_HANDLER error_handler;
-    m_schema_validator.validate( aJson, error_handler, aUri );
+    m_schema_validator->Validate( aJson, error_handler, aUri );
 }
 
 
diff --git a/kicad/pcm/pcm.h b/kicad/pcm/pcm.h
index cc8a021ddc..50a29cfedd 100644
--- a/kicad/pcm/pcm.h
+++ b/kicad/pcm/pcm.h
@@ -23,12 +23,12 @@
 
 #include "core/wx_stl_compat.h"
 #include "pcm_data.h"
+#include <json_schema_validator.h>
 #include "widgets/wx_progress_reporters.h"
 #include <functional>
 #include <iostream>
 #include <map>
-#include <json_common.h>
-#include <nlohmann/json-schema.hpp>
+
 #include <thread>
 #include <tuple>
 #include <unordered_map>
@@ -393,7 +393,7 @@ private:
     time_t getCurrentTimestamp() const;
 
     wxWindow*                                    m_dialog;
-    nlohmann::json_schema::json_validator        m_schema_validator;
+    std::unique_ptr<JSON_SCHEMA_VALIDATOR>       m_schema_validator;
     wxString                                     m_3rdparty_path;
     wxString                                     m_cache_path;
     std::unordered_map<wxString, PCM_REPOSITORY> m_repository_cache;
diff --git a/thirdparty/json_schema_validator/CMakeLists.txt b/thirdparty/json_schema_validator/CMakeLists.txt
index 32e50c83f4..f33c0aeea7 100644
--- a/thirdparty/json_schema_validator/CMakeLists.txt
+++ b/thirdparty/json_schema_validator/CMakeLists.txt
@@ -6,15 +6,14 @@ add_library( nlohmann_json_schema_validator STATIC
     string-format-check.cpp
 )
 
-add_dependencies( nlohmann_json_schema_validator kicommon )
-
 target_include_directories( nlohmann_json_schema_validator
     PUBLIC
         $<INSTALL_INTERFACE:include>
         $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
 )
 
+target_include_directories( nlohmann_json_schema_validator INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} )
+
 target_link_libraries( nlohmann_json_schema_validator
     PUBLIC nlohmann_json
-    PRIVATE kicommon
 )
diff --git a/thirdparty/json_schema_validator/json-schema-draft7.json.cpp b/thirdparty/json_schema_validator/json-schema-draft7.json.cpp
index bc07d8afca..b680e2c2d2 100644
--- a/thirdparty/json_schema_validator/json-schema-draft7.json.cpp
+++ b/thirdparty/json_schema_validator/json-schema-draft7.json.cpp
@@ -6,7 +6,6 @@
  * SPDX-License-Identifier: MIT
  *
  */
-#include <json_common.h>
 #include <nlohmann/json.hpp>
 
 namespace nlohmann
diff --git a/thirdparty/json_schema_validator/nlohmann/json-schema.hpp b/thirdparty/json_schema_validator/nlohmann/json-schema.hpp
index 582c113c1c..07befd3472 100644
--- a/thirdparty/json_schema_validator/nlohmann/json-schema.hpp
+++ b/thirdparty/json_schema_validator/nlohmann/json-schema.hpp
@@ -21,7 +21,6 @@
 #	define JSON_SCHEMA_VALIDATOR_API
 #endif
 
-#include <json_common.h>
 #include <nlohmann/json.hpp>
 
 #ifdef NLOHMANN_JSON_VERSION_MAJOR