Overview
KiCad’s IPC API provides a modern, language-agnostic interface for external applications to interact with KiCad. Built on Protocol Buffers and NNG (Nanomsg Next Generation), it enables real-time communication with running KiCad instances.Architecture
The API uses a client-server model:- Server: KiCad application (
KICAD_API_SERVER) - Client: External tools, scripts, or applications
- Transport: IPC sockets (named pipes on Windows, Unix domain sockets on Linux/macOS)
- Protocol: Protocol Buffers (protobuf) over NNG request-reply pattern
Core Components
// Server: include/api/api_server.h
class KICAD_API_SERVER : public wxEvtHandler
{
public:
void Start(); // Start listening
void Stop(); // Stop server
bool Running() const; // Check status
void RegisterHandler( API_HANDLER* ); // Add handler
void DeregisterHandler( API_HANDLER* ); // Remove handler
std::string SocketPath() const; // Get IPC path
const std::string& Token() const; // Auth token
};
// Handler: include/api/api_handler.h
class API_HANDLER
{
public:
API_RESULT Handle( ApiRequest& aMsg );
protected:
template <class RequestType, class ResponseType, class HandlerType>
void registerHandler(
HANDLER_RESULT<ResponseType> (HandlerType::*aHandler)(
const HANDLER_CONTEXT<RequestType>&
)
);
};
Protocol Definition
Envelope Structure
All messages are wrapped in an envelope (api/proto/common/envelope.proto):
syntax = "proto3";
package kiapi.common;
import "google/protobuf/any.proto";
enum ApiStatusCode {
AS_UNKNOWN = 0;
AS_OK = 1; // Success
AS_TIMEOUT = 2; // Request timed out
AS_BAD_REQUEST = 3; // Invalid parameters
AS_NOT_READY = 4; // KiCad still starting
AS_UNHANDLED = 5; // No handler found
AS_TOKEN_MISMATCH = 6; // Auth token mismatch
AS_BUSY = 7; // Operation in progress
AS_UNIMPLEMENTED = 8; // Not yet implemented
}
message ApiRequestHeader {
// Auth token to verify connection
string kicad_token = 1;
// Client identifier for logging
string client_name = 2;
}
message ApiRequest {
ApiRequestHeader header = 1;
google.protobuf.Any message = 2;
}
message ApiResponseHeader {
string kicad_token = 1;
}
message ApiResponseStatus {
ApiStatusCode status = 1;
string error_message = 2;
}
message ApiResponse {
ApiResponseHeader header = 1;
ApiResponseStatus status = 2;
google.protobuf.Any message = 3;
}
Board Commands
Defined inapi/proto/board/board_commands.proto:
Get Board Stackup
message GetBoardStackup {
kiapi.common.types.DocumentSpecifier board = 1;
}
message BoardStackupResponse {
kiapi.board.BoardStackup stackup = 1;
}
import kicad_api_pb2 as api
from google.protobuf.any_pb2 import Any
# Create request
request = api.ApiRequest()
request.header.client_name = "my_tool"
request.header.kicad_token = token
get_stackup = api.GetBoardStackup()
get_stackup.board.filepath = "/path/to/board.kicad_pcb"
request.message.Pack(get_stackup)
# Send via socket (pseudo-code)
response_bytes = socket.send_and_receive(request.SerializeToString())
response = api.ApiResponse.FromString(response_bytes)
if response.status.status == api.AS_OK:
stackup_response = api.BoardStackupResponse()
response.message.Unpack(stackup_response)
print(f"Copper layers: {stackup_response.stackup.copper_layer_count}")
Get/Set Layers
message GetBoardEnabledLayers {
kiapi.common.types.DocumentSpecifier board = 1;
}
message BoardEnabledLayersResponse {
uint32 copper_layer_count = 1;
repeated kiapi.board.types.BoardLayer layers = 2;
}
message SetBoardEnabledLayers {
kiapi.common.types.DocumentSpecifier board = 1;
uint32 copper_layer_count = 2;
repeated kiapi.board.types.BoardLayer layers = 3;
}
Net Management
message GetNets {
kiapi.common.types.DocumentSpecifier board = 1;
repeated string netclass_filter = 2; // Optional filter
}
message NetsResponse {
repeated kiapi.board.types.Net nets = 1;
}
message GetItemsByNet {
kiapi.common.types.ItemHeader header = 1;
repeated kiapi.common.types.KiCadObjectType types = 2;
repeated kiapi.board.types.NetCode net_codes = 3;
}
Zone Operations
// Blocking operation - returns AS_BUSY until complete
message RefillZones {
kiapi.common.types.DocumentSpecifier board = 1;
repeated kiapi.common.types.KIID zones = 2; // Empty = all zones
}
Board Origins
enum BoardOriginType {
BOT_UNKNOWN = 0;
BOT_GRID = 1; // Grid origin
BOT_DRILL = 2; // Drill/place origin
}
message GetBoardOrigin {
kiapi.common.types.DocumentSpecifier board = 1;
BoardOriginType type = 2;
}
message SetBoardOrigin {
kiapi.common.types.DocumentSpecifier board = 1;
BoardOriginType type = 2;
kiapi.common.types.Vector2 origin = 3;
}
DRC Integration
enum DrcSeverity {
DRS_UNKNOWN = 0;
DRS_WARNING = 1;
DRS_ERROR = 2;
DRS_EXCLUSION = 3;
DRS_IGNORE = 4;
DRS_INFO = 5;
DRS_ACTION = 6;
DRS_DEBUG = 7;
}
message InjectDrcError {
kiapi.common.types.DocumentSpecifier board = 1;
DrcSeverity severity = 2;
string message = 3;
kiapi.common.types.Vector2 position = 4;
repeated kiapi.common.types.KIID items = 5;
}
message InjectDrcErrorResponse {
kiapi.common.types.KIID marker = 1; // Created marker ID
}
Editor Appearance
enum InactiveLayerDisplayMode {
ILDM_NORMAL = 1; // Shown
ILDM_DIMMED = 2; // Dimmed colors
ILDM_HIDDEN = 3; // Hidden
}
enum NetColorDisplayMode {
NCDM_ALL = 1; // All copper items
NCDM_RATSNEST = 2; // Ratsnest only
NCDM_OFF = 3; // Disabled
}
enum BoardFlipMode {
BFM_NORMAL = 1; // Top view
BFM_FLIPPED_X = 2; // Bottom view, mirrored
}
message BoardEditorAppearanceSettings {
InactiveLayerDisplayMode inactive_layer_display = 1;
NetColorDisplayMode net_color_display = 2;
BoardFlipMode board_flip = 3;
RatsnestDisplayMode ratsnest_display = 4;
}
message GetBoardEditorAppearanceSettings {}
message SetBoardEditorAppearanceSettings {
BoardEditorAppearanceSettings settings = 1;
}
Interactive Commands
// Starts interactive move (returns immediately, KiCad becomes AS_BUSY)
message InteractiveMoveItems {
kiapi.common.types.DocumentSpecifier board = 1;
repeated kiapi.common.types.KIID items = 2;
}
Visibility and Selection
message GetVisibleLayers {
kiapi.common.types.DocumentSpecifier board = 1;
}
message BoardLayers {
repeated kiapi.board.types.BoardLayer layers = 1;
}
message SetVisibleLayers {
kiapi.common.types.DocumentSpecifier board = 1;
repeated kiapi.board.types.BoardLayer layers = 2;
}
message GetActiveLayer {
kiapi.common.types.DocumentSpecifier board = 1;
}
message SetActiveLayer {
kiapi.common.types.DocumentSpecifier board = 1;
kiapi.board.types.BoardLayer layer = 2;
}
Implementing a Client
Python Example
import socket
import os
from google.protobuf.any_pb2 import Any
import kicad_api.common.envelope_pb2 as envelope
import kicad_api.board.board_commands_pb2 as board_cmd
class KiCadClient:
def __init__(self, socket_path=None):
self.socket_path = socket_path or self._find_socket()
self.token = self._read_token()
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(self.socket_path)
def _find_socket(self):
"""Locate KiCad's IPC socket"""
# On Linux: /tmp/kicad-{PID}.sock
# On Windows: \\.\pipe\kicad-{PID}
import glob
sockets = glob.glob("/tmp/kicad-*.sock")
if sockets:
return sockets[0]
raise RuntimeError("KiCad not running or API not enabled")
def _read_token(self):
"""Read authentication token"""
# Token is written to same directory as socket
token_file = self.socket_path.replace(".sock", ".token")
with open(token_file, 'r') as f:
return f.read().strip()
def send_request(self, message):
"""Send a protobuf message and receive response"""
# Create envelope
request = envelope.ApiRequest()
request.header.kicad_token = self.token
request.header.client_name = "python_client"
request.message.Pack(message)
# Send request
data = request.SerializeToString()
self.sock.sendall(len(data).to_bytes(4, 'little') + data)
# Receive response
size_bytes = self.sock.recv(4)
size = int.from_bytes(size_bytes, 'little')
response_data = self.sock.recv(size)
# Parse response
response = envelope.ApiResponse()
response.ParseFromString(response_data)
if response.status.status != envelope.AS_OK:
raise RuntimeError(f"API Error: {response.status.error_message}")
return response
def get_nets(self, board_path):
"""Get all nets from a board"""
request = board_cmd.GetNets()
request.board.filepath = board_path
response = self.send_request(request)
nets_response = board_cmd.NetsResponse()
response.message.Unpack(nets_response)
return [(net.name, net.code) for net in nets_response.nets]
def refill_zones(self, board_path, zone_ids=None):
"""Refill zones (blocking operation)"""
request = board_cmd.RefillZones()
request.board.filepath = board_path
if zone_ids:
for zone_id in zone_ids:
request.zones.append(zone_id)
response = self.send_request(request)
# Returns immediately, but KiCad is AS_BUSY until complete
return response.status.status == envelope.AS_OK
def close(self):
self.sock.close()
# Usage
client = KiCadClient()
try:
nets = client.get_nets("/path/to/board.kicad_pcb")
for name, code in nets:
print(f"Net: {name} (code: {code})")
finally:
client.close()
C++ Example
#include <nng/nng.h>
#include <nng/protocol/reqrep0/req.h>
#include "api/common/envelope.pb.h"
#include "api/board/board_commands.pb.h"
class KiCadApiClient {
public:
KiCadApiClient(const std::string& socketPath, const std::string& token)
: m_token(token)
{
nng_req0_open(&m_socket);
std::string url = "ipc://" + socketPath;
nng_dial(m_socket, url.c_str(), nullptr, 0);
}
~KiCadApiClient() {
nng_close(m_socket);
}
kiapi::common::ApiResponse SendRequest(
const google::protobuf::Message& message)
{
// Create envelope
kiapi::common::ApiRequest request;
request.mutable_header()->set_kicad_token(m_token);
request.mutable_header()->set_client_name("cpp_client");
request.mutable_message()->PackFrom(message);
// Serialize and send
std::string data = request.SerializeAsString();
nng_msg* msg;
nng_msg_alloc(&msg, data.size());
memcpy(nng_msg_body(msg), data.data(), data.size());
nng_sendmsg(m_socket, msg, 0);
// Receive response
nng_msg* recv_msg;
nng_recvmsg(m_socket, &recv_msg, 0);
kiapi::common::ApiResponse response;
response.ParseFromArray(
nng_msg_body(recv_msg),
nng_msg_len(recv_msg)
);
nng_msg_free(recv_msg);
return response;
}
private:
nng_socket m_socket;
std::string m_token;
};
Advanced Features
Padstack Analysis
message CheckPadstackPresenceOnLayers {
kiapi.common.types.DocumentSpecifier board = 1;
repeated kiapi.common.types.KIID items = 2;
repeated kiapi.board.types.BoardLayer layers = 3;
}
message PadstackPresenceEntry {
kiapi.common.types.KIID item = 1;
kiapi.board.types.BoardLayer layer = 2;
PadstackPresence presence = 3; // PRESENT or NOT_PRESENT
}
Pad Shape Computation
message GetPadShapeAsPolygon {
kiapi.common.types.DocumentSpecifier board = 1;
repeated kiapi.common.types.KIID pads = 2;
kiapi.board.types.BoardLayer layer = 3;
}
message PadShapeAsPolygonResponse {
repeated kiapi.common.types.KIID pads = 1;
repeated kiapi.common.types.PolygonWithHoles polygons = 2;
}
Error Handling
def safe_api_call(client, request):
try:
response = client.send_request(request)
return response
except RuntimeError as e:
if "AS_NOT_READY" in str(e):
print("KiCad is still starting up, retry...")
time.sleep(1)
return safe_api_call(client, request)
elif "AS_BUSY" in str(e):
print("KiCad is busy, waiting...")
time.sleep(0.5)
return safe_api_call(client, request)
elif "AS_TOKEN_MISMATCH" in str(e):
print("Authentication failed - restart client")
raise
else:
raise
Best Practices
- Always set client_name: Helps with debugging and logging
- Handle AS_BUSY: Some operations are blocking (zone fills, etc.)
- Validate token: Token changes when KiCad restarts
- Use timeouts: Set socket timeouts to avoid hanging
- Batch operations: Minimize round-trips for better performance
- Check status codes: Always verify
AS_OKbefore unpacking response
Security Considerations
- Token authentication: Prevents unauthorized access
- Local sockets only: IPC sockets are not network-accessible
- File permissions: Socket and token files have restricted permissions
- No remote access: API is designed for local tool integration only
See Also
- Python Scripting - Direct Python API
- Action Plugins - Interactive plugins
- Custom File Formats - File format details