Start here

Connect an existing app

Connect your app to Agent Control Panel (ACP) without restructuring it: register it, set four environment variables, install the agent-control-panel SDK (Node.js or Python 3.10+) and wait for the first heartbeat.

Create the app and agents

Register the app and create its agents in the dashboard. Copy the agent ID into ACP_AGENT_ID. Each SDK client represents one agent; create a client for each agent you want to report.

Set four environment variables

Server environment
ACP_URL=https://acp.skaigroup.tech
ACP_API_KEY=acp_…          # shown once at creation
ACP_AGENT_ID=agt_84c1       # the agent this process reports as
ACP_WEBHOOK_SECRET=…       # shared with the receiver

createClient throws at startup if url, apiKey, agentId or webhookSecret is missing (Python’s create_client raises ValueError); it refuses to accept unsigned control commands. Everything after startup is fail-open.

Install the SDK and mount one route

Save the generated file as acp.ts. The webhook handler reads the raw body to verify the HMAC, so it must not sit behind express.json().

acp.ts · agent-control-panel
// npm i agent-control-panel
import { createClient } from "agent-control-panel";

export const acp = createClient({
  url: process.env.ACP_URL,
  apiKey: process.env.ACP_API_KEY,
  agentId: process.env.ACP_AGENT_ID,
  webhookSecret: process.env.ACP_WEBHOOK_SECRET,
});

acp.start();

// Mount without express.json() — the handler needs the raw body.
app.post("/api/acp/webhook", acp.expressWebhookHandler());

Python. The Python SDK has the same behaviour and wire contract, with zero runtime dependencies. Pass the raw request body to webhook_response: the signature covers the exact bytes.

acp.py · agent-control-panel (PyPI)
# pip install agent-control-panel
import os
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from agent_control_panel import create_client

acp = create_client(
    url=os.environ["ACP_URL"],
    api_key=os.environ["ACP_API_KEY"],
    agent_id=os.environ["ACP_AGENT_ID"],
    webhook_secret=os.environ["ACP_WEBHOOK_SECRET"],
)
acp.start()  # heartbeats every 30 s, flushes every 5 s, on daemon threads

app = FastAPI()

@app.post("/api/acp/webhook")
async def acp_webhook(request: Request) -> JSONResponse:
    status, body = acp.webhook_response(request.headers, await request.body())
    return JSONResponse(body, status_code=status)

Flask and Django receivers are in the Python SDK README. Without a public URL (local development, a worker, a cron job) the Python client still receives pause, stop, resume and model swaps with the next heartbeat, within 30 seconds.

The long-form generated client is still available for apps that cannot add a dependency. It is the same protocol written out by hand.

Confirm the first heartbeat

The client sends one heartbeat immediately, then every 30 seconds. The dashboard marks the app connected on the first accepted beat.

Connected on first accepted heartbeat