API Surface
Harbor APIs should be typed, boring, and explicit.
API_SURFACE.md
API philosophy
Harbor APIs should be typed, boring, and explicit.
No raw-secret endpoints. No generic "proxy my arbitrary HTTP request with hidden creds" endpoints.
Harbor Node API starter surface
See HARBOR_AGENT_WORKFLOW.md for live PowerShell, curl, and Python examples. See HARBOR_RESPONSE_PIPELINES.md for compact response and Business pipeline guidance.
Health
GET /health- returns
service,version,nodeId,nodeName,status, andtimestamp
Status
GET /v1/status- returns persistent node identity plus
version, ports, audit count, and timestamps
Ports
GET /v1/portsPOST /v1/portsGET /v1/ports/:portIdPATCH /v1/ports/:portIdGET /v1/ports/:portId/actionsGET /v1/ports/:portId/oauth/statusPOST /v1/ports/:portId/oauth/startPOST /v1/ports/:portId/oauth/refreshPOST /v1/ports/:portId/oauth/disconnectGET /v1/ports/:portId/action-draftsPOST /v1/ports/:portId/action-draftsPOST /v1/ports/:portId/openapi-importPOST /v1/ports/:portId/pack-export
Integrations
POST /v1/integrations/import-urlPOST /v1/integrations/import-manifest
OAuth callbacks
GET /v1/oauth/callback/google
Actions
GET /v1/actions/:actionIdPOST /v1/actions/:actionId/executeGET /v1/action/:actionRefPOST /v1/action/:actionRef/runPOST /v1/action/:actionRef/:operation
Action drafts
GET /v1/action-draftsGET /v1/action-drafts/:draftIdPOST /v1/action-drafts/:draftId/validatePOST /v1/action-drafts/:draftId/testPOST /v1/action-drafts/:draftId/request-publishPOST /v1/action-drafts/:draftId/publishPOST /v1/action-drafts/:draftId/reject
Audit
GET /v1/audit
Packs
POST /v1/packs/import
Approvals
GET /v1/approvalsGET /v1/approvals/:approvalIdPOST /v1/approvals/:approvalId/approvePOST /v1/approvals/:approvalId/reject
Manager response pipelines
GET /v1/manager/response-pipelinesPUT /v1/manager/response-pipelines
Response artifacts
GET /v1/response-artifactsGET /v1/response-artifacts/:artifactId
Agent execute compaction
Harbor keeps the full execute response as the default. Agent callers may opt into a smaller reply envelope on action execute routes.
Canonical request fields:
responseMode: "full" | "compact" | "ok"responsePipelineId: string
Compact aliases:
rm: "f" | "c" | "o"rp: "<pipeline-id>"
Compact response envelope:
{
"ok": true,
"s": "approved",
"x": "exec_123",
"o": {
"summary": "small filtered payload"
}
}
Fields:
ok: whether the action completed successfullys:approved,pending_approval, ordeniedx: execution ida: optional approval idas: optional approval statuso: optional output payload forcompacte: optional{ c, m }error object
Saved response pipelines are Business-only in v1. They can project a filtered response artifact from the full execution output and reply to agents in either compact or ok mode.
What is real now
- Harbor Node persists local state in SQLite
- node identity is created on first start and reused on later starts
- the built-in
system-localport is seeded durably - configured
webhookports can be created locally and stored in SQLite - configured
discord_webhookports can be created locally with hidden Discord webhook URLs - configured
http_apiports can be created locally with hidden auth config and stored action templates http_apiports also support durable action drafts that can be created, validated, tested, published, and rejected- Harbor distinguishes local caller modes so operators manage ports while agents help with draft authoring
- existing
http_apiports can import a narrow OpenAPI subset into drafts - existing
http_apiports can export safe Harbor Packs - Harbor can import a Harbor Pack into a new local
http_apiport shell plus drafts - Harbor can import Dock integration manifests by URL into a local
http_apiport and sync live actions onto it - imported Gmail
http_apiports can use an operator-initiated local OAuth connect flow - Google OAuth provider metadata is served by the cloud API, but access and refresh tokens stay local to Harbor Node
- configured
webhookanddiscord_webhookports can be updated safely without re-entering hidden values - built-in actions currently include
system.echo,system.time,system.rotate-token, andsystem.shutdown - configured webhook ports expose fixed actions like
<port-id>.echo-remoteand<port-id>.send-event - configured Discord webhook ports expose
<port-id>.send-message - configured HTTP API ports expose
<port-id>.say-helloand<port-id>.echo - stored action records now carry durable metadata such as
slug,method,path,requestBodyMode,resultMode, andenabled - stored HTTP API action paths may be fixed relative routes or bounded templates like
/repos/{owner}/{repo}/issues - published actions can carry
sourceDraftIdso Harbor can trace the live action back to its originating draft - policy decisions are local and explicit: allow, deny, or require approval
- approval-required actions create durable local approval records and do not execute until approved
- audit events are recorded locally newest-first
- hidden config such as auth tokens is stored locally but never returned through Harbor APIs, approval responses, or audit events
- Discord webhook URLs are stored locally but never returned through Harbor APIs, approval responses, or audit events
- HTTP API auth tokens are stored locally but never returned through Harbor APIs, approval responses, or audit events
- hidden values pass through a dedicated storage abstraction so Harbor can adopt stronger local-at-rest protection later without changing the API surface
- real outbound execution is bounded by a timeout and classified into safe failure kinds
- draft test execution uses the same bounded Harbor execution path and does not make the draft live
What is still future work
- real third-party Harbor Ports
- broader custom action authoring for configured ports
- richer action authoring UX beyond the first draft lifecycle
- policy editing UI and advanced filtering
- encryption-at-rest for hidden config
- cloud sync and Harbor Fleet features
Configured webhook port request
{
"kind": "webhook",
"label": "Local Mock Webhook",
"description": "Configured local verification port",
"baseUrl": "http://127.0.0.1:11821/_mock/webhook/tenant-abc/opaque-xyz",
"authToken": "super-secret-demo-token"
}
Call it with:
POST /v1/ports
Configured webhook port response
{
"id": "<port-id>",
"slug": "<port-slug>",
"kind": "webhook",
"name": "Local Mock Webhook",
"description": "Configured local verification port",
"status": "ready",
"available": true,
"actionCount": 2,
"createdAt": "2026-03-23T18:30:00.000Z",
"updatedAt": "2026-03-23T18:30:00.000Z",
"configSummary": {
"baseOrigin": "http://127.0.0.1:11821",
"host": "127.0.0.1:11821",
"hasPath": true,
"authConfigured": true
}
}
The hidden auth token is intentionally absent from the response. Harbor also omits the actual stored pathname and any embedded URL credentials from public responses. Once saved, hidden config remains write-only and is not re-shown by Harbor.
Configured Discord webhook port request
{
"kind": "discord_webhook",
"label": "Discord Alerts",
"description": "Send a narrow plain text message to Discord",
"webhookUrl": "https://discord.com/api/webhooks/123456789012345678/example-token"
}
Configured Discord webhook port response
{
"id": "<discord-port-id>",
"slug": "<discord-port-slug>",
"kind": "discord_webhook",
"name": "Discord Alerts",
"description": "Send a narrow plain text message to Discord",
"status": "ready",
"available": true,
"actionCount": 1,
"createdAt": "2026-03-23T20:30:00.000Z",
"updatedAt": "2026-03-23T20:30:00.000Z",
"configSummary": {
"baseOrigin": "https://discord.com",
"host": "discord.com",
"hasPath": true,
"webhookConfigured": true
}
}
The full Discord webhook URL is intentionally absent from the response.
Configured HTTP API port request
{
"kind": "http_api",
"label": "Local HTTP API",
"description": "Authenticated API behind Harbor",
"baseUrl": "http://127.0.0.1:18081",
"authToken": "harbor-demo-key",
"authHeaderName": "x-api-key",
"authTokenPrefix": ""
}
Configured HTTP API port response
{
"id": "<http-api-port-id>",
"slug": "<http-api-port-slug>",
"kind": "http_api",
"name": "Local HTTP API",
"description": "Authenticated API behind Harbor",
"status": "ready",
"available": true,
"actionCount": 2,
"createdAt": "2026-03-23T21:10:00.000Z",
"updatedAt": "2026-03-23T21:10:00.000Z",
"configSummary": {
"baseOrigin": "http://127.0.0.1:18081",
"host": "127.0.0.1:18081",
"hasPath": false,
"authConfigured": true,
"authMode": "header_token",
"authHeaderName": "x-api-key"
}
}
The hidden auth token is intentionally absent from the response. Harbor also omits any stored path detail beyond the hasPath flag.
For OAuth-backed Gmail imports, the same safe config summary shape is used, but authMode becomes oauth_google and oauthProvider is google_gmail.
HTTP API action draft lifecycle
Harbor action drafts are durable candidate actions attached to an existing port. They are stored separately from live actions and do not appear in GET /v1/ports/:portId/actions until published.
Draft status values:
draftvalidatedpublishedrejected
Caller modes:
operatoragentscript
Current local behavior:
- only
operatormay create or edit ports agentandscriptmay read safe metadata and work with drafts under existing portsagentandscriptmay request publish- direct non-operator publish is disabled unless Harbor is started with
HARBOR_ALLOW_NON_OPERATOR_PUBLISH=true - OpenAPI import is operator-only for the MVP
- Harbor Pack import/export is operator-only for the MVP
- Dock import is operator-only for the MVP
- templated paths may use explicit placeholders like
{owner}and{repo} - callers provide those values through
input.path
Minimum draft fields:
idportIdsluglabeldescriptionmethodpathapprovalModerequestBodyModeresultModestatusvalidationSummarylatestTestSummarypublishedActionId- timestamps
Path guidance:
- use a fixed relative path like
/user/reposwhen the route is fully static - use a bounded template like
/repos/{owner}/{repo}/issueswhen the route needs a small explicit placeholder set - do not include query strings in
path - do not use arbitrary full URLs
Example draft creation request:
{
"slug": "draft-hello",
"label": "Draft Hello",
"description": "Draft GET hello action",
"method": "GET",
"path": "/v1/hello",
"approvalMode": "automatic",
"requestBodyMode": "none",
"resultMode": "json_summary"
}
Call it with:
POST /v1/ports/:portId/action-drafts
Example draft response:
{
"id": "draft_12345678-1234-1234-1234-123456789abc",
"portId": "<http-api-port-id>",
"slug": "draft-hello",
"label": "Draft Hello",
"description": "Draft GET hello action",
"method": "GET",
"path": "/v1/hello",
"approvalMode": "automatic",
"requestBodyMode": "none",
"resultMode": "json_summary",
"status": "draft",
"createdAt": "2026-03-26T17:20:00.000Z",
"updatedAt": "2026-03-26T17:20:00.000Z"
}
Validation:
POST /v1/action-drafts/:draftId/validate
Use an explicit JSON body in PowerShell:
{}
Testing:
POST /v1/action-drafts/:draftId/test
Example test request:
{
"input": {},
"requestedBy": {
"kind": "script",
"name": "powershell-example"
}
}
Publishing:
POST /v1/action-drafts/:draftId/publish
Use an explicit JSON body in PowerShell:
{}
Rejecting:
POST /v1/action-drafts/:draftId/reject
Example reject request:
{
"note": "Not publishing this draft"
}
Request publish without publishing directly:
POST /v1/action-drafts/:draftId/request-publish
Recommended caller headers for agent-safe draft work:
Authorization: Bearer <agent-token>Content-Type: application/json- optional
harbor-name: OpenClaw
Once published, the live action appears in:
GET /v1/ports/:portId/actionsGET /v1/actions/:actionId
Published actions keep a sourceDraftId link when they originated from a Harbor draft.
OpenAPI import MVP
Harbor can import a small OpenAPI document into drafts under an existing http_api port.
Call it with:
POST /v1/ports/:portId/openapi-import
Example request:
{
"document": {
"openapi": "3.0.3",
"info": {
"title": "Custom HTTP API",
"version": "1.0.0"
},
"servers": [
{
"url": "http://127.0.0.1:18081"
}
],
"paths": {
"/v1/hello": {
"get": {
"operationId": "imported-hello",
"summary": "Imported Hello"
}
},
"/v1/echo": {
"post": {
"operationId": "imported-echo",
"summary": "Imported Echo",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
}
}
}
}
Supported now:
- JSON OpenAPI input
- import into an existing
http_apiport only GEToperations with no request bodyPOSToperations withapplication/jsonrequest bodies- relative paths under the existing port base URL
operationIdor a derived Harbor slug
Skipped or rejected now:
- non-matching
serversorigins - callbacks and webhooks
- path parameters
- operation parameters
- unsupported auth schemes
- unsupported methods
- unsupported request body types
- conflicting slugs
Imported operations remain drafts and do not become live actions until they are reviewed, tested, and published through the existing draft lifecycle.
Harbor Pack foundation
Harbor Packs are a local JSON format for safe reusable http_api templates.
Current MVP boundaries:
- export from an existing compatible
http_apiport only - import into a new local
http_apiport shell only - operator-only import/export
- imported action templates remain drafts by default
- no hidden auth values or raw secrets in the pack payload
Export:
POST /v1/ports/:portId/pack-export
Use an explicit JSON body in PowerShell:
{}
Import:
POST /v1/packs/import
What gets exported:
- pack metadata
- a safe
http_apiport template with fields likebaseUrl,authHeaderName, andauthTokenPrefix - action template metadata such as
slug,method,path,approvalMode,requestBodyMode, andresultMode
What does not get exported:
authToken- hidden runtime config
- audit history
- approval records
Import behavior:
- creates a new local
http_apiport shell - copies only safe config defaults from the pack
- creates action drafts under the new port
- does not create live actions
- does not auto-publish imported drafts
The imported port starts degraded until the operator writes hidden auth locally with PATCH /v1/ports/:portId.
Dock integration import
Dock is the hosted catalog layer for safe Harbor integration metadata.
Harbor import routes:
POST /v1/integrations/import-urlPOST /v1/integrations/import-manifest
Current MVP behavior:
- operator-only
- imports safe metadata only
- creates or updates a local
http_apiport shell - syncs live local actions onto that port
- defaults Dock-backed ports to
autoUpdate: true - skips workflow templates with an explicit reason
- never copies hidden credentials from Dock
Example import-by-URL request:
{
"url": "http://127.0.0.1:11826/p/google/gmail"
}
Example response shape:
{
"status": "imported",
"sourceUrl": "http://127.0.0.1:11826/p/google/gmail",
"integration": {
"slug": "gmail",
"title": "Gmail",
"version": "0.1.0",
"publisher": {
"slug": "google",
"name": "Google"
}
},
"port": {
"id": "<imported-port-id>",
"kind": "http_api",
"status": "degraded"
},
"createdDrafts": [],
"syncedActions": [
{
"id": "<imported-port-id>.get-profile",
"slug": "get-profile",
"enabled": true
}
],
"updatedExistingPort": false,
"importedWorkflows": [
{
"slug": "triage-inbox",
"status": "skipped",
"reason": "Workflow templates are not installable yet in this MVP."
}
],
"warnings": [
"Credentials must be configured locally before use."
]
}
Configured webhook port update semantics
Call it with:
PATCH /v1/ports/:portId
Behavior:
- omit
authTokento preserve the current hidden value - set
authTokento a new non-empty value to replace the current hidden value - set
clearAuthToken: trueto clear the current hidden value - do not send
authTokenandclearAuthToken: truetogether
Example metadata-only update:
{
"label": "Local Mock Webhook Updated",
"description": "Updated without re-entering the hidden token",
"baseUrl": "http://127.0.0.1:11821/_mock/webhook/tenant-next/opaque-next"
}
Example hidden-value replacement:
{
"authToken": "replacement-secret-token"
}
Example hidden-value clear:
{
"clearAuthToken": true
}
Configured Discord webhook port update semantics
Call it with:
PATCH /v1/ports/:portId
Behavior:
- omit
webhookUrlto preserve the current hidden Discord webhook URL - set
webhookUrlto a new non-empty value to replace the current hidden value - set
clearWebhookUrl: trueto clear the current hidden value - do not send
webhookUrlandclearWebhookUrl: truetogether - clearing the webhook URL leaves the port record in place but marks it
degradeduntil a new hidden URL is saved
Example metadata-only update:
{
"label": "Discord Alerts Updated",
"description": "Updated without re-entering the hidden webhook URL"
}
Example hidden-value replacement:
{
"webhookUrl": "https://discord.com/api/webhooks/123456789012345678/replacement-token"
}
Example hidden-value clear:
{
"clearWebhookUrl": true
}
Configured HTTP API port update semantics
Call it with:
PATCH /v1/ports/:portId
Behavior:
- omit
authTokento preserve the current hidden auth token - set
authTokento a new non-empty value to replace the current hidden value - set
clearAuthToken: trueto clear the current hidden value baseUrl,label,description,authHeaderName, andauthTokenPrefixcan be updated without re-entering the hidden value- set
dockAutoUpdateon a Dock-imported port to choose whether Harbor should keep that port synced to newer Dock versions - imported Dock action sync keeps actions enabled by default
- clearing the hidden auth token leaves the port record in place but marks it
degradeduntil a new hidden value is saved
Example hidden-value replacement:
{
"authToken": "replacement-secret-token"
}
Example hidden-value clear:
{
"clearAuthToken": true
}
Example action request
{
"input": {
"message": "hello"
},
"requestedBy": {
"kind": "agent",
"name": "openclaw"
}
}
Templated path example:
{
"input": {
"path": {
"owner": "BreakwaterNinja",
"repo": "harbor"
},
"body": {
"title": "Add bounded path templates",
"body": "Document repo-scoped GitHub actions."
}
},
"requestedBy": {
"kind": "agent",
"name": "openclaw"
}
}
Call it with:
POST /v1/actions/system.echo/execute
Example action result
{
"actionId": "system.echo",
"portId": "system-local",
"status": "approved",
"executionId": "exec_123",
"decision": "allow",
"output": {
"echoedInput": {
"message": "hello"
}
}
}
Example configured action result
{
"actionId": "<port-id>.echo-remote",
"portId": "<port-id>",
"status": "approved",
"executionId": "exec_321",
"decision": "allow",
"output": {
"remoteOk": true,
"remoteStatus": 200,
"targetOrigin": "http://127.0.0.1:11821",
"routeKey": "echo_remote",
"remoteBody": {
"ok": true,
"route": "echo"
}
}
}
Example Discord action result
{
"actionId": "<discord-port-id>.send-message",
"portId": "<discord-port-id>",
"status": "approved",
"executionId": "exec_654",
"decision": "allow",
"output": {
"delivered": true,
"remoteStatus": 200,
"targetHost": "discord.com",
"messageId": "135791357913579135"
}
}
Example HTTP API action result
{
"actionId": "<http-api-port-id>.say-hello",
"portId": "<http-api-port-id>",
"status": "approved",
"executionId": "exec_765",
"decision": "allow",
"output": {
"delivered": true,
"remoteStatus": 200,
"targetOrigin": "http://127.0.0.1:18081",
"routeKey": "say_hello",
"remoteBody": {
"ok": true,
"message": "hello from custom-http-api"
}
}
}
Example approval-required result
{
"actionId": "system.rotate-token",
"portId": "system-local",
"status": "pending_approval",
"executionId": "exec_456",
"decision": "require_approval",
"approvalId": "approval_123",
"approvalStatus": "pending",
"error": {
"code": "APPROVAL_REQUIRED",
"message": "Token rotation is intentionally approval-gated in the MVP."
}
}
Example approval record
{
"id": "approval_123",
"status": "approved",
"actionId": "system.rotate-token",
"portId": "system-local",
"policyDecision": "require_approval",
"policyReason": "Token rotation is intentionally approval-gated in the MVP.",
"requestedAt": "2026-03-23T19:00:00.000Z",
"resolvedAt": "2026-03-23T19:01:00.000Z",
"requestedBy": {
"kind": "script",
"name": "powershell-example"
},
"resolvedBy": {
"kind": "user",
"name": "local-operator"
},
"requestInput": {
"reason": "rotate local test token"
},
"decisionNote": "approved locally",
"execution": {
"status": "succeeded",
"executionId": "exec_987",
"executedAt": "2026-03-23T19:01:01.000Z",
"output": {
"requested": true
}
},
"createdAt": "2026-03-23T19:00:00.000Z",
"updatedAt": "2026-03-23T19:01:01.000Z"
}
Example denied result
{
"actionId": "system.shutdown",
"portId": "system-local",
"status": "denied",
"executionId": "exec_789",
"decision": "deny",
"error": {
"code": "POLICY_DENIED",
"message": "Shutdown remains denied in the MVP to prove hard-stop policy behavior."
}
}
Example audit event
{
"id": "audit_123",
"type": "action_executed",
"actionId": "system.echo",
"portId": "system-local",
"decision": "allow",
"requestedBy": {
"kind": "agent",
"name": "openclaw"
},
"createdAt": "2026-03-23T17:00:00.000Z",
"detail": {
"executionId": "exec_123"
}
}
Approval lifecycle events now include:
approval_requestedapproval_approvedapproval_rejectedapproval_execution_startedapproval_execution_succeededapproval_execution_failed
Configured-port update lifecycle events now include:
port_createdport_updatedport_hidden_value_replacedport_hidden_value_cleared
Classified outbound execution failures
When a real outbound configured action fails, Harbor returns a safe ACTION_EXECUTION_FAILED payload with bounded details.
Current failure kinds:
timeoutnetwork_errorremote_4xxremote_5xxinvalid_remote_responsemisconfigured_port
Example bounded failure:
{
"code": "ACTION_EXECUTION_FAILED",
"message": "The Harbor action failed during execution.",
"details": {
"actionId": "<discord-port-id>.send-message",
"message": "Discord webhook responded with status 404.",
"failureKind": "remote_4xx",
"remoteStatus": 404,
"retryable": false
}
}
Current seeded built-in actions
system.echo=> allowsystem.time=> allowsystem.rotate-token=> require approvalsystem.shutdown=> deny
Configured webhook policy defaults
<port-id>.echo-remote=> allow<port-id>.send-event=> require approval<discord-port-id>.send-message=> allow<http-api-port-id>.say-hello=> allow<http-api-port-id>.echo=> allow
This keeps one configured action immediately executable for local verification while proving that configured outbound actions still go through Harbor Guard. /_mock/webhook/* is a test-only local verification surface and not part of Harbor's intended public connector model.
Cloud API starter surface
Health
GET /health
Enrollment
POST /v1/enroll
License
POST /v1/license/validate
Account
GET /v1/account/self
SDK goals
- simple install
- clean typed methods
- no surprises
- map directly to Harbor Node action model