SSyncropel Docs

API Reference

HTTP endpoints for the Syncropel server.

Base URL

http://localhost:9100

The server listens on port 9100 by default. All endpoints return JSON.

Authentication

When auth.required = true (the kernel default), every /v1/* endpoint requires a bearer token:

Authorization: Bearer spl_<env>_<sa_id>_<secret>

Tokens are minted against service accounts with a closed scope list. The middleware validates the token, checks the requested endpoint's required scope, and verifies the claimed actor DID (X-Syncropel-Actor) is in the SA's allowed-actors list.

The spl CLI resolves bearer tokens in this precedence: --token <value> flag first, then the SPL_TOKEN environment variable, then ~/.syncro/token. HTTP callers pass the token directly in the Authorization header.

Exempt routes (unauthenticated even when auth is enforced):

  • GET /health
  • POST /v1/bootstrap/service-account (one-shot per namespace)
  • GET /.well-known/syncropel

For the full model — scopes, token format, rotation, federation composition — see the Authentication & Service Accounts guide. Endpoints for managing service accounts and tokens are documented in the Service accounts section below.

Health

MethodPathDescription
GET/healthHealth check — returns {"status": "ok"}

Records

MethodPathDescription
POST/v1/recordsIngest a new record
GET/v1/records/:idGet a record by ID
GET/v1/records?thread=XList records filtered by thread
GET/v1/records?actor=XList records filtered by actor
POST/v1/records/queryRich query — MongoDB-style filter document over the log
POST/v1/records/searchSemantic search — rank records by cosine similarity to an embedded query
POST/v1/records/embedBackfill embeddings for records that don't yet have one under the active provider

Ingest a Record

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:alice",
    "thread": "th_abc123...",
    "body": {"goal": "Deploy the service"},
    "clock": 0,
    "data_type": "SCALAR"
  }'

Namespace-scoped ingest

Records can target a specific namespace by setting body.namespace. The daemon walks the namespace's ancestor chain and rejects the record with 403 NAMESPACE_REJECTED if any ancestor is missing or not Active. Records that omit body.namespace fall through to the implicit default namespace and are always accepted.

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:alice",
    "thread": "th_abc123...",
    "body": {
      "namespace": "acme-corp/payments/staging",
      "goal": "Deploy v2.3 to staging"
    },
    "clock": 0,
    "data_type": "SCALAR"
  }'

If the namespace does not exist, the response is:

{
  "object": "error",
  "type": "invalid_request_error",
  "code": "NAMESPACE_REJECTED",
  "message": "namespace 'acme-corp/payments/staging' rejected: ancestor 'acme-corp/payments' is not Active in the registry. Create it first with `spl namespace create acme-corp/payments`."
}

The error always names the failing ancestor and includes the exact recovery command. To declare namespaces use the spl namespace CLI commands, which write to the well-known thread th_namespace_registry that the engine hot-reloads on every change.

Rich query

POST /v1/records/query runs a structured filter over the record log server-side. The body is the filter AST — top-level keys are AND-combined, values are either scalars (sugar for $eq) or operator documents ($in, $gt, $like, $regex, $and, $or, $not, …). Body fields are addressed with dot-paths (body.kind, body.priority, body._refs.track.id). See the query guide for the full grammar, operator semantics, and index-awareness.

curl -s -X POST http://localhost:9100/v1/records/query \
  -H "Authorization: Bearer $SPL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "act": "INTEND",
      "actor": "did:sync:agent:dev",
      "body.kind": { "$in": ["core.task.record", "core.task.record.v1"] }
    },
    "sort": { "clock": -1 },
    "limit": 20
  }'

Request body:

FieldTypeDefaultDescription
threadstringOptional thread scope. Shorthand for adding {"thread": "..."} to the filter; lets the backend use the thread index
filterobject{}MongoDB-style filter document
sortobjectSingle-key sort spec: {"clock": -1} or {"created_at": 1}
limitint100Result cap. Max 1000
offsetint0Pagination offset
explainboolfalseWhen true, response includes a plan block describing the translated SQL, bind count, and which fields used the indexed fast path vs. json_extract

Response:

{
  "object": "list",
  "data": [
    {
      "object": "record",
      "id": "7a2936eccf6b...",
      "parents": [],
      "thread": "th_abc...",
      "actor": "did:sync:agent:dev",
      "act": "INTEND",
      "body": { "kind": "core.task.record.v1", "...": "..." },
      "clock": 42,
      "data_type": "SCALAR"
    }
  ],
  "plan": {
    "sql": "SELECT ... WHERE act = ? AND actor = ? ...",
    "bind_count": 3,
    "indexed_fields": ["act", "actor", "thread"],
    "unindexed_fields": ["body.kind"]
  }
}

The plan block is only present when explain=true. Results are always tenant-filtered — records outside the caller's namespace never appear.

Error codes:

CodeNameMeaning
400INVALID_QUERYUnknown operator, unknown field path, type mismatch, malformed filter
500INTERNALStorage backend failure

Index-backed fast paths depend on the indexed field registry — declare indexed body.<field> paths via spl config add-body-kind-manifest so predicates on them use CREATE INDEX expressions instead of scanning JSON.

POST /v1/records/search embeds the query through the configured provider, ranks records by cosine similarity, and returns the top K. Envelope filters (thread, actor, kind, after_clock) narrow the result after ranking so near-misses outside the scope don't crowd out the best answer. See the semantic search guide for provider setup, CLI usage, and SDK helpers.

curl -s -X POST http://localhost:9100/v1/records/search \
  -H "Authorization: Bearer $SPL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "authentication failure logs",
    "k": 5,
    "thread": "th_incident_42"
  }'

Request body:

FieldTypeDefaultDescription
querystringFree-text query. Must be non-empty
kint10Top-K to return. Clamped to [1, 100]
threadstringRestrict to records on this thread
actorstringRestrict to records emitted by this actor DID
kindstringRestrict to records whose body.kind matches
after_clockintRestrict to records with clock > N

Response:

{
  "object": "list",
  "embedder": "ollama:nomic-embed-text",
  "k": 5,
  "data": [
    {
      "object": "record",
      "id": "3f4a...",
      "score": 0.847,
      "parents": [],
      "thread": "th_incident_42",
      "actor": "did:sync:agent:ops",
      "act": "KNOW",
      "body": { "...": "..." },
      "clock": 118,
      "data_type": "SCALAR"
    }
  ]
}

Results are tenant-filtered after scoring — a record outside the caller's namespace never reaches the response, regardless of score.

Error codes:

CodeNameMeaning
400EMPTY_QUERYquery was absent or blank
502EMBEDDER_FAILEDThe configured provider rejected the embedding request
503SEMANTIC_SEARCH_DISABLEDNo embedding provider is configured — see the embedding_provider topic

Backfill embeddings

POST /v1/records/embed walks records that don't yet have an embedding for the active provider and embeds them synchronously within the request. Use this once after enabling a provider to backfill existing archives; new records are embedded inline by the INGEST loop.

curl -s -X POST http://localhost:9100/v1/records/embed \
  -H "Authorization: Bearer $SPL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"limit": 100}'

limit defaults to 100, max 500. Response: {embedder, embedded, skipped_empty, failed, remaining, has_more}. Loop until has_more is false — or drive the CLI wrapper spl embed --loop.

Threads

MethodPathDescription
GET/v1/threadsList all threads
GET/v1/threads/:id/recordsGet all records on a thread
GET/v1/threads/:id/stateGet folded thread state
GET/v1/threads/:id/projectGet thread projection (formatted view)
GET/v1/threads/:id/participantsGet thread participants

Thread Projections

curl "http://localhost:9100/v1/threads/th_abc123.../project?format=tui"

Projection formats:

FormatDescription
tuiTurn-based view for terminal display
messagesChat-style message format

Trust

MethodPathDescription
GET/v1/trustGet all trust scores

Actors

MethodPathDescription
GET/v1/actorsList registered actors
GET/v1/actors/lookup?did=XGet actor detail by DID

Dispatch

MethodPathDescription
POST/v1/dispatchDispatch work to an actor

Dispatch Work

curl -X POST http://localhost:9100/v1/dispatch \
  -H "Content-Type: application/json" \
  -d '{
    "actor_did": "did:sync:agent:dev",
    "goal": "Fix the authentication bug",
    "thread_id": "th_abc123...",
    "budget": {"max_cost_usd": 1.0, "max_duration_secs": 600}
  }'

Decisions

MethodPathDescription
GET/v1/aitlList pending decisions
POST/v1/aitl/:id/decideApprove or reject a pending decision

Approve a Decision

curl -X POST http://localhost:9100/v1/aitl/abc123.../decide \
  -H "Content-Type: application/json" \
  -d '{"decision": "approve", "reason": "Looks correct"}'

Engine

MethodPathDescription
GET/v1/engine/healthEngine health counters
GET/v1/engine/expression_cache/statsCEL expression cache statistics (hit rate, size, avg compile time) — used by spl doctor
GET/v1/config/rulesList routing rules
GET/v1/adaptersList registered adapters

Expression cache stats

curl http://localhost:9100/v1/engine/expression_cache/stats

Returns a JSON object with hits, misses, compile_errors, hit_rate_pct, avg_compile_micros, size, capacity. Healthy steady-state: hit rate > 99%, avg compile < 100μs, size much less than capacity (1024 default). spl doctor consumes this endpoint as one of its 7 checks.

Audit

There is no dedicated /v1/audit/* endpoint family. The spl audit export CLI command produces JSONL by querying existing endpoints client-side:

  1. GET /v1/threads → enumerate threads
  2. GET /v1/threads/:id/records → fetch records per thread
  3. Filter client-side by category (system actor, AITL verdict, dispatch outcome, governance) and time window
  4. Emit one JSON object per matching record on stdout

This means spl audit export works against any Syncropel daemon without server-side changes. The trade-off is that it's O(n) over thread count — for very large stores you should pass --thread <id> to scope the query.

A future server-side /v1/audit/export?since=...&categories=... endpoint is on the roadmap. It would push the filtering into SQL and return a single JSONL stream, removing the per-thread round trips.

For SIEM integration recipes (cron rotation, Splunk/Elastic/Loki pipelines, etc.) see the SIEM Integration guide.

Permission denials are NOT in audit export today

HTTP middleware permission denials are emitted as tracing::warn! events to the daemon log (~/.syncro/logs/spl.log), not as records. They appear in the log filtered by grep "PERMISSION DENIED" but do NOT appear in spl audit export output. Promoting denials to first-class audit records is tracked as a follow-up — for now treat the daemon log and the audit export as complementary security-event streams.

Proxy

MethodPathDescription
POST/v1/proxy/messagesAnthropic Messages API proxy (translates to records)

The proxy endpoint accepts the Anthropic Messages API format and translates requests into Syncropel records. Use this to route existing AI tool calls through Syncropel for observability and trust tracking.

Capability discovery

Two endpoints expose what the daemon supports. Clients should prefer them over hard-coding feature flags — the manifest is the single source of truth.

MethodPathAuthDescription
GET/v1/capabilitiesauthenticated (optional when auth is off)Client-facing capability manifest
GET/.well-known/syncropelunauthenticatedPublic envelope — capabilities_manifest + federation_manifest as sibling keys

GET /v1/capabilities

The authenticated surface. Returns the capability manifest scoped to the caller — auth state, advertised MCP tools, supported DID methods, store backend, available transports, API version. Clients use this to enable or disable features based on what the connected daemon actually supports; SDKs feature-detect without a trial-and-error HTTP round trip.

curl -s -H "Authorization: Bearer $SPL_TOKEN" \
  http://localhost:9100/v1/capabilities | jq

Response (elided):

{
  "object": "capabilities_manifest",
  "manifest_version": "1",
  "daemon": {
    "version": "0.X.Y",
    "api_version": "v1",
    "spec_version": "0.18"
  },
  "auth": { "required": true },
  "did_methods": ["did:sync", "did:key", "did:web"],
  "record": {
    "algebra_version": "v1",
    "hashed_fields": ["parents", "thread", "actor", "act", "body", "clock", "data_type"]
  },
  "mcp_tools": ["create_thread", "emit_record", "read_thread", "trust_query", "fold_thread", "dispatch", "fan_out"],
  "transports": { "...": "..." },
  "semantic_search": { "...": "..." },
  "store_backends": ["sqlite", "memory"],
  "capabilities": {
    "records": true,
    "threads": true,
    "sync": true,
    "federation_consent": true,
    "directory": false,
    "...": "..."
  },
  "conventions": {
    "aitl_path": "/v1/aitl",
    "identity_path": "/v1/identity",
    "sync_path": "/v1/sync",
    "trust_path": "/v1/trust"
  }
}

The capabilities map is a set of boolean feature flags covering the full surface — sync, directory (gated by config.directory_enabled), federation_consent, identity, dispatch_observability, cusum_alerts, trust_detail, reconcile_queue, and more. Flags are additive across releases; absence of a flag means unsupported.

When auth is off, the endpoint is reachable without a token. When auth is on, an unauthenticated call returns 401 AUTH_REQUIRED — use /.well-known/syncropel below for the public view.

GET /.well-known/syncropel

The unauthenticated surface, served at the Well-Known URI location so any peer on the public internet can discover a daemon without prior coordination. The response envelope carries two top-level manifests as sibling keys:

KeyAudiencePurpose
capabilities_manifestProspective clientSame shape as /v1/capabilities — a peer can feature-detect before authenticating
federation_manifestProspective federation peerEd25519-signed; describes what this daemon offers the federation mesh
curl -s http://localhost:9100/.well-known/syncropel | jq 'keys'
# [ "capabilities_manifest", "federation_manifest" ]

The federation_manifest key is present only when the daemon is federation-enabled and has a signing key available. Peers that only want the client view can read capabilities_manifest and ignore the federation sibling.

Federation manifest shape

The federation_manifest is Ed25519-signed over its canonical JSON (excluding the signature field). Verification proceeds against the DID document resolved from daemon.did. Consumers — spl discover, federation directories, peer-discovery CLIs — verify the signature before trusting any advertised field.

{
  "object": "federation_manifest",
  "manifest_version": "1",
  "daemon": {
    "did": "did:web:alice.dev",
    "version": "0.X.Y",
    "federation_protocol_version": "0.13"
  },
  "federation": {
    "enabled": true,
    "pair_endpoint": "https://alice.dev/v1/sync",
    "sync_change_endpoint": "https://alice.dev/v1/sync/changes",
    "sync_record_endpoint": "https://alice.dev/v1/sync/records"
  },
  "advertises": {
    "kinds": ["music.catalog.track", "music.catalog.artist"],
    "refs": [],
    "namespaces": ["alice-music"]
  },
  "consent_policy": {
    "default_posture": "pair-then-ask",
    "accepts_pair_requests": true,
    "requires_did_method": [],
    "rejects_did_method": []
  },
  "capabilities_ref": {
    "url": "https://alice.dev/.well-known/syncropel",
    "key": "capabilities_manifest",
    "adr": "ADR-036"
  },
  "responders_ref": {
    "url": "https://alice.dev/.well-known/syncropel",
    "key": "responders_manifest",
    "adr": "ADR-043"
  },
  "directory": {
    "registered_at": [],
    "directory_registration_id": null
  },
  "signature": {
    "alg": "Ed25519",
    "key_id": "did:web:alice.dev#key-1",
    "signature": "<base64>",
    "signed_at": "2026-04-22T18:00:00Z",
    "expires_at": "2026-04-29T18:00:00Z"
  }
}

Key fields:

FieldPurpose
daemon.didThe DID this manifest is authoritative for. Must match the DID served at the domain's DID document
federation.pair_endpointEntry point for the pair handshake
advertises.kindsbody.kind values the daemon answers federation queries about. A peer advertising music.catalog.track commits to serving sync requests for that kind per its consent policy
advertises.refsContent-addressed REFERENCE identifiers this daemon holds — used by future DHT discovery as the primary announce key
advertises.namespacesNamespace handles whose records are federation-eligible (willingness, not grant)
consent_policy.default_posture"open", "pair-then-ask", "invite-only", or "closed". Informative hint for pre-handshake self-filtering
capabilities_ref / responders_refPointers back into the same envelope; clients follow them to fetch the sibling manifests
signature.expires_atForces periodic re-signing (default TTL 7 days). Expired signatures should be rejected

For the discovery workflow and how this composes with did:web, did:sync, and mDNS discovery, see the federation discovery guide.

Identity

MethodPathDescription
GET/v1/identityThe daemon's own identity (DID + public-key info)

Returns {actor, display_name, did, method, key_path, key_fingerprint}. Populated from ~/.syncro/config.toml at daemon startup. New installs generate a did:key:... on spl init; upgraded installs without a DID in config need spl init --force once.

Federation sync

Federation is pull-first HTTP replication between two daemons. Both flat (/v1/sync/*) and domain-grouped (/v1/federation/sync/*) routes are served.

Changes feed

MethodPathDescription
GET/v1/sync/changes?thread=X&since=CURSOR&limit=N&feed=MODE&target_namespace=NSCursor-paginated changes feed

Feed modes:

  • normal (default) — single response, returns up to limit (default 1000, max 10000) records
  • longpoll — holds the connection up to 30s waiting for new records
  • continuous — Server-Sent Events stream (for live subscriptions)

Response: {records: [{id, record}], next_cursor: "opaque", has_more: bool}. Records are consent-filtered per target_namespace (see consent guide).

Record batch fetch

MethodPathDescription
POST/v1/sync/recordsFetch specific records by ID (federation-flagged path requires sig)

Request body: {ids: [...]} or {records: [...]}. When the x-syncropel-federation: 1 header is set, each record must include a sig field — Ed25519 over canonical JSON. Unsigned federation requests return HTTP 422. Replay of previously-accepted records is deduped via content-addressed hash.

Pair management

MethodPathDescription
GET/v1/sync/pairsList all sync pairs on this daemon
POST/v1/sync/pairsCreate a pair (target pulls from source)
GET/v1/sync/pairs/{id}Pair detail (state, cursor, retries, last_error)
DELETE/v1/sync/pairs/{id}Remove a pair
POST/v1/sync/pairs/{id}/pausePause pulling
POST/v1/sync/pairs/{id}/resumeResume pulling
POST/v1/sync/pairs/{id}/kickForce immediate poll regardless of backoff

Create body: {peer_did, peer_url, thread_id}. Response includes pair_id and, if the daemon detects a loopback misconfiguration, a warning field with the recovery command.

The pair direction is semantically "target pulls source" — creating a pair on B with peer_did=A means records flow A→B. For bidirectional sync, create a pair on each side.

Pair state is persistent across daemon restart — the registry is rebuilt from lifecycle records on a reserved control thread at startup.

Federation health + per-pair stats

MethodPathDescription
GET/v1/sync/healthAggregate mesh health — pair counts by state, records pulled per hour/minute, mean/p50/p95 delivery latency, top slowest + error-prone pairs, active drift alerts
GET/v1/sync/pairs/{id}/statsPer-pair detail — rolling rate, latency histogram, error count last hour, poll interval state, cumulative counters

Aggregate response shape (elided):

{
  "object": "sync_health",
  "pairs_total": 5,
  "pairs_by_state": { "running": 3, "failing": 1, "paused": 1 },
  "records_pulled_last_hour": 2147,
  "mean_delivery_latency_ms": 847,
  "p95_delivery_latency_ms": 2410,
  "slowest_pairs": [...],
  "most_errored_pairs": [...],
  "drift_alerts": [...]
}

Metrics are in-memory with a 1-hour rolling window — ephemeral, reset on daemon restart. The daemon-wide /health endpoint also includes a federation summary for one-glance status.

LAN peer discovery

MethodPathDescription
GET/v1/discovery/mdnsList peers the local daemon has discovered via LAN mDNS browsing

Returns a roster of peers with DID, endpoint, namespace, handle, capabilities (all from their TXT records), and last_seen timestamps. Used by spl fleet sync peers discover --method mdns.

The daemon advertises itself on _syncropel._tcp.local when [sync.discovery] mdns_broadcast = true; it listens for other daemons' advertisements by default. Failures during mDNS init are logged but never fail the daemon (LAN discovery is a soft feature).

did:sync directory

MethodPathDescription
POST/directory/genesisCreate a new did:sync identity (genesis operation)
POST/directory/update/{hash}Rotate keys or update service endpoints
POST/directory/revoke/{hash}Revoke a did:sync identity
GET/directory/{hash}/did.jsonResolve a did:sync to its DID document
GET/directory/{hash}/operationsFull operation log for a did:sync
GET/directory/handle/{handle}Resolve a human-readable handle to its did:sync

Gated by config.directory_enabled — off by default. A daemon can act as a did:sync directory provider; most installs don't need to.

Service accounts

MethodPathScopeDescription
POST/v1/bootstrap/service-account— (unauthenticated)Create the first SA on a fresh install. One-shot per namespace
GET/v1/service-accountsadminList service accounts
POST/v1/service-accountsadminCreate a new service account + token
DELETE/v1/service-accounts/{id}adminRevoke a service account (invalidates all its tokens)
POST/v1/service-accounts/{id}/rotate-keyadminMint a new token for an SA and revoke previous tokens

Bootstrap the first service account

POST /v1/bootstrap/service-account is a one-shot, unauthenticated endpoint used to create the first SA on a fresh daemon. Once any SA exists in the target namespace, the endpoint returns 409 BOOTSTRAP_CLOSED and you must use the authenticated POST /v1/service-accounts path instead.

curl -X POST http://localhost:9100/v1/bootstrap/service-account \
  -H "Content-Type: application/json" \
  -d '{
    "service_account_id": "sa_abc123def456ghi7",
    "display_name": "First admin",
    "scopes": ["admin"],
    "namespace": "default",
    "with_token": {
      "token_id": "a3f9e7d1c8b2h6j4k5m7n9p1q3r5t7v9",
      "env_tag": "prod",
      "created_at": "2026-04-20T12:30:00Z"
    }
  }'

Response (201 Created):

{
  "object": "bootstrap_result",
  "service_account_id": "sa_abc123def456ghi7",
  "sa_record_id": "7a2936eccf6b...",
  "namespace": "default",
  "token_id": "a3f9e7d1c8b2...",
  "token_record_id": "151439fe2972..."
}

The client is responsible for generating service_account_id + token_id. The CLI (spl service-account create --bootstrap --with-token) handles this automatically.

List service accounts

curl -H "Authorization: Bearer $SPL_TOKEN" \
  http://localhost:9100/v1/service-accounts

Returns an array. api_key is always null in list responses — plaintext tokens are only disclosed at creation time.

[
  {
    "id": "sa_abc123def456ghi7",
    "name": "CI runner",
    "did": "did:sync:system:sa_abc123def456ghi7",
    "api_key": null,
    "active": true,
    "created_at": null
  }
]

Create a service account + token

curl -X POST http://localhost:9100/v1/service-accounts \
  -H "Authorization: Bearer $SPL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Webhook integration"}'

Response (201 Created) includes the plaintext bearer token once — save it immediately:

{
  "id": "sa_new123...",
  "name": "Webhook integration",
  "did": "did:sync:system:sa_new123...",
  "api_key": "spl_prod_sa_new123_a3f9e7d1c8b2h6j4k5m7n9p1q3r5t7v9",
  "active": true,
  "created_at": null
}

Revoke a service account

DELETE /v1/service-accounts/{id} performs per-SA revocation: every token ever minted for the SA becomes invalid, and future token mints for the SA are blocked.

curl -X DELETE -H "Authorization: Bearer $SPL_TOKEN" \
  http://localhost:9100/v1/service-accounts/sa_abc123def456ghi7

Response (200): {"status": "revoked"}.

Rotate an SA's key

POST /v1/service-accounts/{id}/rotate-key mints a new token first, then revokes previous tokens. Guarantees callers don't get locked out on partial failure.

curl -X POST -H "Authorization: Bearer $SPL_TOKEN" \
  http://localhost:9100/v1/service-accounts/sa_abc123def456ghi7/rotate-key

Response (200):

{
  "id": "sa_abc123def456ghi7",
  "api_key": "spl_prod_sa_abc123_new_secret_here"
}

Error codes

CodeNameMeaning
401AUTH_REQUIREDNo bearer token sent, or token is invalid / revoked
403SCOPE_FORBIDDENToken is valid but lacks the required scope for this endpoint
403DID_CLAIM_DENIEDX-Syncropel-Actor is not in the SA's allowed-actors list
409BOOTSTRAP_CLOSEDBootstrap endpoint called but namespace already has an SA

CORS

All /v1/* + /.well-known/* + /directory/* endpoints include CORS headers (tower-http::CorsLayer with permissive defaults). The daemon at localhost:9100 is reachable from web UI origins without proxy configuration.

On this page