SoyaOS

HTTP API

The wire format SoyaOS speaks — OpenAI-compatible chat completions plus the Scope event channel.

A Moon exposes one HTTP surface that’s deliberately small and OpenAI-shaped: clients that already speak /v1/chat/completions work without code changes. The Scope event stream rides alongside on a parallel channel for clients that want it.

Everything below is stable within v0; new endpoints will be added, never re-shaped.

Base URL

https://moon.example.com/v1

For solo (laptop) deployments: http://127.0.0.1:7474/v1.

Authentication

Authorization: Bearer <api_key>. Mint keys with soyaos auth keys create.

Solo: no auth required. The Moon refuses any non-loopback bind on solo to keep this honest.

POST /v1/chat/completions

OpenAI-compatible chat completions. The body shape matches OpenAI almost exactly; SoyaOS-specific extensions live under soya: prefixed fields and are silently ignored by upstream-compatible clients.

Request

{
  "model": "soya:compo",
  "messages": [
    { "role": "user", "content": "outline an essay about X" }
  ],
  "stream": true,
  "soya": {
    "input": { "topic": "X", "tone": "academic" },
    "pin_version": "1.4.0",
    "cache_mode": "rw"
  }
}
FieldRequiredNotes
modelyessoya:<name>[@<version>] or a pass-through name (claude-…, gpt-…).
messagesyesStandard OpenAI shape. The last user message is the canonical input.
streamnoDefault false. Server-Sent Events when true.
temperaturenoPassed through to the upstream LLM if model is a pass-through.
soya.inputnoTyped input matching the SoyaPack’s inputs schema. If present, takes precedence over messages.
soya.pin_versionnoPin to a specific SoyaPack version (overrides @version in model).
soya.cache_modenooff / read / write / rw. Default rw.

Response (non-streaming)

{
  "id": "run_018f3a4b1c2d3e4f",
  "object": "chat.completion",
  "created": 1715670703,
  "model": "soya:[email protected]",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{ \"summary\": { ... } }"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 1842, "completion_tokens": 612, "total_tokens": 2454 }
}

The content is the Agent’s final artifact JSON — same thing you’d see at the bottom of soyaos run. Clients that just want plain text can configure the Agent to emit a string artifact.

Response (streaming, stream: true)

Server-Sent Events, two channels multiplexed by event::

event: message
data: { "id":"…","choices":[{"delta":{"content":"…"}}] }

event: scope
data: { "kind":"stage_started","run_id":"…","stage":"outline","ts":"…" }

event: scope
data: { "kind":"tool_called","run_id":"…","tool":"fetch","args":{…},"ts":"…" }

event: message
data: { "id":"…","choices":[{"delta":{"content":"…"}}] }

event: done
data: [DONE]

Clients that only know OpenAI ignore event: scope lines and see a normal OpenAI stream. Clients that want observability subscribe to both — every Scope event listed in Scope events appears here as it happens.

GET /v1/models

{
  "object": "list",
  "data": [
    { "id": "soya:compo",       "object": "model", "owned_by": "soya", "soya": { "kind": "Agent", "version": "1.4.0" } },
    { "id": "soya:newsbeam",    "object": "model", "owned_by": "soya", "soya": { "kind": "Agent", "version": "0.7.2" } },
    { "id": "claude-sonnet-4-6","object": "model", "owned_by": "anthropic" }
  ]
}

OpenAI-shaped; the soya: extension fields are extra. Pass-through model names appear here if the Moon is configured to advertise them.

GET /v1/runs/{run_id}

Fetch the metadata for a completed run.

{
  "run_id":   "run_018f3a4b1c2d3e4f",
  "pack":     "soya:[email protected]",
  "tenant":   "acme",
  "started":  "2026-05-14T08:11:43.512Z",
  "completed":"2026-05-14T08:11:48.731Z",
  "ok":       true,
  "artifacts":[
    { "name": "outline", "schema": "outline.v1", "url": "https://…/outline.json" },
    { "name": "guide",   "schema": "guide.v1",   "url": "https://…/guide.json" }
  ],
  "cost": { "comet_seconds": 5.2, "upstream_tokens": 2454 }
}

GET /v1/runs/{run_id}/scope

Replay the Scope event stream for a finished run, as NDJSON:

curl -H "Authorization: Bearer $KEY" \
  https://moon.example.com/v1/runs/run_018f3a/scope | jq -c .
{"kind":"run_started","run_id":"run_018f3a4b1c2d3e4f","ts":"…"}
{"kind":"stage_started","run_id":"run_018f3a4b1c2d3e4f","stage":"outline","ts":"…"}

{"kind":"run_completed","run_id":"run_018f3a4b1c2d3e4f","ok":true,"ts":"…"}

Use this for “what really happened on run X” investigation. The same events that streamed live are replayable later.

POST /v1/packs (push)

Used by soyaos pack push. Multipart body containing the canonicalized bundle plus the .soya/signature.json. The Moon verifies the signature, allocates an immutable version slot, returns:

{
  "pack":          "soya:[email protected]",
  "bundle_sha256": "8d2c…",
  "signed_by":     "chzealot-2026",
  "status":        "published"
}

See Sign and publish a SoyaPack for the full flow.

Error envelope

All errors share this shape:

{
  "error": {
    "type":    "capability_violation",
    "message": "egress to api.anthropic.com:443 not in allowlist",
    "code":    "EGRESS_NOT_ALLOWED",
    "run_id":  "run_018f3a4b1c2d3e4f"
  }
}

Top-level HTTP status:

StatusMeaning
200Success.
400Malformed request body or invalid input schema.
401Missing / invalid API key.
403Authenticated but lacking permission (e.g. capability_violation).
404Unknown pack / run / model name.
410Pack version is deprecated.
429Rate-limited (per-key or per-pack).
500Internal Moon error.
502Upstream LLM error (the body’s error.upstream is the verbatim upstream payload).

Rate limits

Per API key: 60 requests/minute by default; bumpable by an admin. Per pack: configurable via the SoyaPack’s sla: block (v1).

Webhooks

Configure via moon.config.yaml (see Self-host a Moon on a VPS). Payload is a JSON array of Scope events, signed by the Moon with HMAC-SHA256 over the body:

X-Soya-Signature: sha256=…

Verify with the per-tenant webhook secret.

OpenAPI

The full schema is served at /v1/openapi.json (auth-gated). Build SDK code from it the standard way:

curl -H "Authorization: Bearer $KEY" https://moon.example.com/v1/openapi.json > soya.json
openapi-generator-cli generate -i soya.json -g typescript-fetch -o ./client

Edit this page on GitHub