diff --git a/api/proto/common/commands/project_commands.proto b/api/proto/common/commands/project_commands.proto
index ce51198d7a..8117715a24 100644
--- a/api/proto/common/commands/project_commands.proto
+++ b/api/proto/common/commands/project_commands.proto
@@ -47,3 +47,20 @@ message ExpandTextVariablesResponse
 {
   repeated string text = 1;
 }
+
+
+// returns kiapi.common.project.TextVariables
+message GetTextVariables
+{
+  kiapi.common.types.DocumentSpecifier document = 1;
+}
+
+
+message SetTextVariables
+{
+  kiapi.common.types.DocumentSpecifier document = 1;
+
+  kiapi.common.project.TextVariables variables = 2;
+  // Whether to merge or replace the existing text variables map with the contents of this message
+  kiapi.common.types.MapMergeMode merge_mode = 3;
+}
diff --git a/api/proto/common/types/base_types.proto b/api/proto/common/types/base_types.proto
index a9666a47f9..f491a1aed2 100644
--- a/api/proto/common/types/base_types.proto
+++ b/api/proto/common/types/base_types.proto
@@ -438,3 +438,13 @@ enum AxisAlignment
   AA_X_AXIS = 1;
   AA_Y_AXIS = 2;
 }
+
+enum MapMergeMode
+{
+  MMM_UNKNOWN = 0;
+  // The existing map will be merged with the incoming map; keys that are not present in the
+  // incoming map will be preserved with their original values
+  MMM_MERGE = 1;
+  // The existing map will be cleared and replaced with the incoming map
+  MMM_REPLACE = 2;
+}
diff --git a/api/proto/common/types/project_settings.proto b/api/proto/common/types/project_settings.proto
index 9dd92b3291..179bebc5c4 100644
--- a/api/proto/common/types/project_settings.proto
+++ b/api/proto/common/types/project_settings.proto
@@ -33,3 +33,8 @@ message NetClass
   string name = 1;
 
 }
+
+message TextVariables
+{
+  map<string, string> variables = 1;
+}
diff --git a/common/api/api_handler_common.cpp b/common/api/api_handler_common.cpp
index b74f352d7c..d3c91d497e 100644
--- a/common/api/api_handler_common.cpp
+++ b/common/api/api_handler_common.cpp
@@ -53,6 +53,10 @@ API_HANDLER_COMMON::API_HANDLER_COMMON() :
             &API_HANDLER_COMMON::handleExpandTextVariables );
     registerHandler<GetPluginSettingsPath, StringResponse>(
             &API_HANDLER_COMMON::handleGetPluginSettingsPath );
+    registerHandler<GetTextVariables, project::TextVariables>(
+            &API_HANDLER_COMMON::handleGetTextVariables );
+    registerHandler<SetTextVariables, Empty>(
+            &API_HANDLER_COMMON::handleSetTextVariables );
 
 }
 
@@ -263,3 +267,72 @@ HANDLER_RESULT<StringResponse> API_HANDLER_COMMON::handleGetPluginSettingsPath(
     reply.set_response( path.GetPath() );
     return reply;
 }
+
+
+HANDLER_RESULT<project::TextVariables> API_HANDLER_COMMON::handleGetTextVariables(
+        const HANDLER_CONTEXT<GetTextVariables>& aCtx )
+{
+    if( !aCtx.Request.has_document() || aCtx.Request.document().type() != DOCTYPE_PROJECT )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_UNHANDLED );
+        // No error message, this is a flag that the server should try a different handler
+        return tl::unexpected( e );
+    }
+
+    const PROJECT& project = Pgm().GetSettingsManager().Prj();
+
+    if( project.IsNullProject() )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_NOT_READY );
+        e.set_error_message( "no valid project is loaded, cannot get text variables" );
+        return tl::unexpected( e );
+    }
+
+    const std::map<wxString, wxString>& vars = project.GetTextVars();
+
+    project::TextVariables reply;
+    auto map = reply.mutable_variables();
+
+    for( const auto& [key, value] : vars )
+        ( *map )[ std::string( key.ToUTF8() ) ] = value.ToUTF8();
+
+    return reply;
+}
+
+
+HANDLER_RESULT<Empty> API_HANDLER_COMMON::handleSetTextVariables(
+    const HANDLER_CONTEXT<SetTextVariables>& aCtx )
+{
+    if( !aCtx.Request.has_document() || aCtx.Request.document().type() != DOCTYPE_PROJECT )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_UNHANDLED );
+        // No error message, this is a flag that the server should try a different handler
+        return tl::unexpected( e );
+    }
+
+    PROJECT& project = Pgm().GetSettingsManager().Prj();
+
+    if( project.IsNullProject() )
+    {
+        ApiResponseStatus e;
+        e.set_status( ApiStatusCode::AS_NOT_READY );
+        e.set_error_message( "no valid project is loaded, cannot set text variables" );
+        return tl::unexpected( e );
+    }
+
+    const project::TextVariables& newVars = aCtx.Request.variables();
+    std::map<wxString, wxString>& vars = project.GetTextVars();
+
+    if( aCtx.Request.merge_mode() == MapMergeMode::MMM_REPLACE )
+        vars.clear();
+
+    for( const auto& [key, value] : newVars.variables() )
+        vars[wxString::FromUTF8( key )] = wxString::FromUTF8( value );
+
+    Pgm().GetSettingsManager().SaveProject();
+
+    return Empty();
+}
diff --git a/include/api/api_handler_common.h b/include/api/api_handler_common.h
index 5aab987626..06f14e5128 100644
--- a/include/api/api_handler_common.h
+++ b/include/api/api_handler_common.h
@@ -57,6 +57,12 @@ private:
 
     HANDLER_RESULT<commands::StringResponse> handleGetPluginSettingsPath(
         const HANDLER_CONTEXT<commands::GetPluginSettingsPath>& aCtx );
+
+    HANDLER_RESULT<project::TextVariables> handleGetTextVariables(
+        const HANDLER_CONTEXT<commands::GetTextVariables>& aCtx );
+
+    HANDLER_RESULT<Empty> handleSetTextVariables(
+        const HANDLER_CONTEXT<commands::SetTextVariables>& aCtx );
 };
 
 #endif //KICAD_API_HANDLER_COMMON_H