Skip to content

Server API

riddlg serve runs a localhost HTTP service exposing riddlg's capabilities to other programs. It is how Synapify drives riddlg, and it is a supported integration point for your own tooling.

riddlg serve                       # http://127.0.0.1:8910
riddlg serve --host 0.0.0.0 --port 9000 --provider anthropic

The server binds 127.0.0.1:8910 by default and locks its AI provider at startup — restart to change it, or override per request on /generate/riddl.

There is no authentication on the server itself

The default bind is loopback-only for good reason. Anything that can reach the port can use it; only Pro entitlement is checked, never identity. Think twice before --host 0.0.0.0.

Endpoints

Method Path Tier Purpose
GET /health Free Liveness check
GET /model/status Free Local model presence / download progress
POST /generate/docs Free Documentation in six formats
POST /generate/api Free Smithy, gRPC, OpenAPI, JSON Schema, or AsyncAPI
POST /generate/sql Free SQL DDL
POST /generate/dbml Free A DBML logical schema
POST /generate/backstage Free A Backstage software catalog
POST /generate/catalog Free An EventCatalog site
POST /generate/confluence Pro Confluence storage-format pages
POST /generate/riddl Free* RIDDL from a description (AI)
POST /generate/code Pro Quarkus code from RIDDL
POST /ai/messages Free* One chat turn, Anthropic Messages shape
POST /mcp Free MCP over Streamable HTTP
GET /ws/echo Free WebSocket echo (connectivity test only)

* Free on the local model; Pro when the effective provider is a cloud one.

Errors are always {"error": "..."} with an appropriate status. Unmatched paths return 404 with an empty body.

GET /health

Returns 200 with the plain-text body ok. Not JSON.

GET /model/status

Reports whether the local model is present, and download progress if it is being fetched. Synapify uses this to draw a progress banner.

{
  "status": "model-downloading",
  "model": "qwen2.5-coder-32b-instruct-q5_k_m.gguf",
  "bytesDownloaded": 4294967296,
  "bytesTotal": 21474836480
}
status Meaning
ready The model file is present and usable
absent No model file, and none is being fetched
model-downloading A download is in progress (or awaiting console confirmation)
error The download failed; an extra error field carries the reason

bytesTotal is 0 when the server could not probe the download size.

absent is normal on cloud providers

When the active profile is a cloud one, no local model is needed and this endpoint reports absent. That is not an error condition.

The /generate endpoints

Every file-producing generator shares one request and one response shape. See Generators for what each one actually emits.

{"riddl": "domain Shop is { ??? }", "format": "mkdocs"}
Field Required Description
riddl yes RIDDL source text
format per endpoint Format, or the endpoint's single parameter — see the table below

Response 200 returns the generated files in memory — nothing is written to disk, and path is relative to your chosen output root:

{"files": [{"path": "index.adoc", "content": "= Shop\n...", "format": "asciidoc"}]}

400 for invalid JSON, RIDDL parse/validation errors, or an unsupported format. 402 when the requested output is Pro and you lack entitlement.

What format means on each endpoint:

Endpoint format carries Accepted values
/generate/docs Output format asciidoc, mkdocs, hugo-book, hugo-geekdoc, hugo; docbook and dita need Pro
/generate/api Output format smithy, grpc, openapi, json-schema, asyncapi
/generate/sql Fallback dialect postgres, mysql, ansi, oracle, sqlserver
/generate/backstage Fallback owner Any team name
/generate/confluence Space key Any Confluence space key
/generate/dbml unused
/generate/catalog unused
/generate/code unused Quarkus is the only target

/generate/api also accepts protocol (default kafka), which the AsyncAPI generator uses.

format is effectively required on /generate/api

It defaults to asciidoc, which /generate/api does not accept — omitting it returns 400 {"error":"unsupported format: asciidoc"}.

asciidoc is a reserved value on the parameter-carrying endpoints

Because format defaults to asciidoc, /generate/sql, /generate/backstage, and /generate/confluence treat that exact string as "not supplied" and fall back to the model's own options. A Backstage owner or Confluence space literally named asciidoc cannot be set over HTTP — use the model option instead.

POST /generate/riddl

Generate a validated RIDDL model from a natural-language description.

{
  "description": "an order-management system",
  "maxRetries": 2,
  "provider": "anthropic",
  "model": "claude-opus-4-8"
}
Field Required Description
description yes The natural-language brief
maxRetries no Validation retry attempts (default 2)
provider no A configured profile name; overrides the server's provider for this request only
model no Model id / GGUF path for the effective provider

Response 200:

{
  "riddl": "domain Shop is { ... }",
  "valid": false,
  "errorCount": 0,
  "warningCount": 3,
  "attempts": 1,
  "messages": "..."
}

valid is stricter than you probably want

valid is true only when there are no errors and no warnings. Freshly generated skeletal models almost always carry advisory completeness warnings, so valid: false with errorCount: 0 is the normal, healthy result. Check errorCount to decide whether the model is usable.

Supplying provider/model opens a transient provider for that one request and closes it afterwards, leaving the server's resident provider untouched. Generation is serialized, so concurrent requests queue.

Status When
202 The local model is downloading — body is the model status shape
400 Invalid JSON, unknown profile, or unusable profile configuration
402 A cloud profile was requested without Pro entitlement
500 Generation failed
503 The model download failed, or the provider is unavailable

A per-request provider/model override skips model provisioning, so it never returns 202.

POST /generate/code (Pro)

Same request and response shape as the other /generate endpoints, but emits a Quarkus project; returned files carry format java. The format field is ignored — Quarkus is the only target. Returns 402 without a Pro subscription. Note that the server route generates the skeleton only; the AI-filled variant is riddlg gen code --fill on the CLI.

POST /ai/messages

Answers one message turn in the Anthropic Messages shape. This exists so a client can hold a conversation through riddlg without ever handling API keys itself — keys and provider choice live in riddlg's configuration, never in the request.

{
  "messages": [{"role": "user", "content": "What is a bounded context?"}],
  "system": "You are a RIDDL expert.",
  "max_tokens": 1024,
  "tools": []
}
Field Required Description
messages yes Non-empty array; content may be a string or a block array
system no System prompt string
max_tokens no Capped at the profile's max-tokens
tools no Anthropic tool definitions — Anthropic profiles only
stream no true for an SSE response — see Streaming

Response 200:

{
  "role": "assistant",
  "content": [{"type": "text", "text": "A bounded context is..."}],
  "stop_reason": "end_turn",
  "model": "claude-sonnet-5"
}

Exactly those four fields are returned; upstream id and usage are dropped.

Behavior depends on the active provider:

  • Anthropic profile — a thin passthrough. messages, system, and tools go through verbatim, tool_use blocks come back intact, and sampling parameters are stripped. This is the tool-calling path.
  • Any other provider — plain multi-turn chat. The transcript is flattened into a single completion. Tool-bearing requests are refused with 501 rather than faking a capability the provider lacks.

The agentic loop stays on your side: tool definitions go in, tool_use blocks come back, you execute them (over /mcp, typically) and return tool_result blocks on the next turn.

Streaming

Send "stream": true and the response is text/event-stream in Anthropic's event shape, whatever provider is behind it:

message_start → content_block_start → content_block_delta … → content_block_stop
              → message_delta → message_stop
  • Anthropic profiles forward the upstream Messages SSE verbatim, so you get full fidelity including tool_use blocks.
  • Every other provider gets an equivalent synthesized sequence, with text arriving as {"type": "text_delta", "text": "..."} deltas.

That means one client code path regardless of provider. Errors that occur mid-stream arrive as an error event rather than an HTTP status, since the 200 has already been sent; failures detected before the stream opens still use the status codes below.

Frames are emitted after generation, not during it

The provider interface is synchronous, so riddlg produces the answer and then emits the event frames. The event shapes are byte-identical to true streaming and clients need no special handling — but you should not expect the first token to arrive noticeably sooner than the last.

Status When
202 The local model is downloading
400 Malformed body, empty messages, or a non-text block on a non-Anthropic provider
402 The resident profile is a cloud one and you lack Pro
501 tools were supplied to a non-Anthropic provider
503 Provider unavailable / download failed

POST /mcp

Serves the MCP tools over the Model Context Protocol's Streamable HTTP transport. One JSON-RPC 2.0 message per request; the tools are identical to riddlg mcp stdio mode.

curl -s localhost:8910/mcp -H 'Content-Type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"check-completeness","arguments":{"source":"domain D is { ??? }"}}
}'

Sessions. initialize mints a session id and returns it in both the Mcp-Session-Id and X-Session-ID response headers. Send either back on subsequent requests. The id is client affinity only — the server holds no per-session state.

Status codes are unusual for HTTP, but correct for JSON-RPC:

Status When
200 Any request with an idincluding JSON-RPC errors, which ride in the response body
202 A notification (no response is due)
404 GET /mcp — there is no SSE stream. Clients use this to detect pre-MCP riddlg versions

Do not treat a 200 as success without checking for an error member in the body.

Client Notes

A few behaviors are worth knowing before you integrate:

  1. Authorization: Bearer <token> is optional and additive. Pro-gated routes accept an Ossum access token, verified against the identity realm. The caller's tier is used for that request, so a Pro-signed-in client gets Pro behavior even from a server that is logged out.
  2. A bad Bearer token never returns 401. It is treated as absent, falling back to the server's own session — a forged token can never exceed what the server already has, and you'll simply see the usual 402.
  3. 402 message text varies by route. Rely on the status code and the error field, not the wording.
  4. /ws/echo reflects frames verbatim. It is a connectivity test, not a generation protocol. For streaming, use /ai/messages with "stream": true — it is the only streaming endpoint.

See Also