Skip to main content

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 in api/proto/board/board_commands.proto:

Get Board Stackup

message GetBoardStackup {
    kiapi.common.types.DocumentSpecifier board = 1;
}

message BoardStackupResponse {
    kiapi.board.BoardStackup stackup = 1;
}
Example Usage:
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

  1. Always set client_name: Helps with debugging and logging
  2. Handle AS_BUSY: Some operations are blocking (zone fills, etc.)
  3. Validate token: Token changes when KiCad restarts
  4. Use timeouts: Set socket timeouts to avoid hanging
  5. Batch operations: Minimize round-trips for better performance
  6. Check status codes: Always verify AS_OK before 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

Build docs developers (and LLMs) love