Documentation for v2.7.0. Read the latest, v2.12.0.
Developer
Developer recipes
One task per section, each with the command, the answer a 2.6.0 instance gave, and where to read
more. The answers are the recordings of A team's story; where a task is
not in the story, the answer is quoted from the
REST API reference. Set SANSHAIN to your instance
and SANSHAIN_TOKEN to a token; the recordings show the address as
https://sanshain.corp.
Mint a read-only token for an AI client#
From a session (log in first, or use the account page), with "scope": "read_only";
full is the default for CI.
curl -s -X POST $SANSHAIN/auth/tokens \
-H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
-d '{"name":"claude-code","expires_in_days":90,"scope":"read_only"}'
Recorded (story, step 12)
POST /auth/tokens HTTP/1.1
Authorization: Bearer <session token>
Content-Type: application/json
{
"name": "claude-code",
"expires_in_days": 90,
"scope": "read_only"
}
HTTP/1.1 200 OK
content-type: application/json
content-length: 165
{
"id": "376e96e4-3808-4c5f-a6c4-0e4c9deacd14",
"name": "claude-code",
"scope": "read_only",
"token": "san_…"
}
Validate a spec before publishing#
No token, nothing stored; the answer is always 200 with a verdict.
curl -s -X POST $SANSHAIN/validate -H "Content-Type: application/json" \
-d "{\"api_type\":\"openapi\",\"content\":$(jq -Rs . < openapi.yaml)}"
Recorded (story, step 11)
POST /validate HTTP/1.1
Content-Type: application/json
{
"api_type": "openapi",
"content": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 2.2.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n \"201\":\n description: Created\n /payments/{paymentId}:\n get:\n summary: One payment\n parameters:\n - name: paymentId\n in: path\n required: true\n schema: { type: string }\n responses:\n \"200\":\n description: OK\n /payments/{id}/capture:\n post:\n summary: Capture an authorised payment\n responses:\n \"200\":\n description: OK\n /payments/{id}/refund:\n post:\n summary: Refund a captured payment\n responses:\n \"200\":\n description: OK"
}
HTTP/1.1 200 OK
content-type: application/json
content-length: 190
{
"valid": true,
"api_type": "openapi",
"version": "2.2.0",
"endpoint_count": 4,
"endpoints": [
"POST /payments",
"GET /payments/{paymentId}",
"POST /payments/{id}/capture",
"POST /payments/{id}/refund"
]
}
Check a pull request without storing anything #
A provide with "dry_run": true runs the version rules too (the validator does not)
and persists nothing. Expect the same answers as a real provide: 202, or
409 with proposed_version.
curl -f -X POST $SANSHAIN/provide \
-H "Authorization: Bearer $SANSHAIN_TOKEN" -H "Content-Type: application/json" \
-d "{\"producername\":\"payments\",\"stability\":\"ga\",\"openapi_yaml\":$(jq -Rs . < openapi.yaml),\"dry_run\":true}"
Not in the story; the answer shapes are the two below. CI integration: dry runs · POST /provide
Fix a forgotten version bump#
The 409 names the number; set it in info.version and provide again.
Recorded (story, step 4)
POST /provide HTTP/1.1
Authorization: Bearer san_…
Content-Type: application/json
{
"producername": "payments",
"stability": "ga",
"openapi_yaml": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 1.0.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n \"201\":\n description: Created\n /payments/{id}:\n get:\n summary: One payment\n parameters:\n - name: id\n in: path\n required: true\n schema: { type: string }\n responses:\n \"200\":\n description: OK\n /payments/{id}/capture:\n post:\n summary: Capture an authorised payment\n responses:\n \"200\":\n description: OK",
"trunk": true
}
HTTP/1.1 409 Conflict
content-type: application/json
content-length: 202
{
"error": "version 1.0.0 of openapi 'payments' is GA and immutable, but the submitted content differs — did you forget to increment info's version? Publish as 1.1.0 instead",
"proposed_version": "1.1.0"
}
Publish a breaking change#
A removed or renamed path behind a minor bump is refused; the major the answer proposes goes through. With a client, the ga switch on the release pipeline does the publish.
SANSHAIN_GA=true sanshain-go -trunk provide # or: cargo sanshain provide --ga --trunk
Recorded (story, steps 5 and 6)
POST /provide HTTP/1.1
Authorization: Bearer san_…
Content-Type: application/json
{
"producername": "payments",
"stability": "ga",
"openapi_yaml": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 1.1.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n \"201\":\n description: Created\n /payments/{paymentId}:\n get:\n summary: One payment\n parameters:\n - name: paymentId\n in: path\n required: true\n schema: { type: string }\n responses:\n \"200\":\n description: OK\n /payments/{id}/capture:\n post:\n summary: Capture an authorised payment\n responses:\n \"200\":\n description: OK",
"trunk": true
}
HTTP/1.1 409 Conflict
content-type: application/json
content-length: 199
{
"error": "version 1.1.0 of openapi 'payments' is breaking relative to GA 1.0.0 but does not bump the major — publish as 2.0.0 instead: Path '/payments/{id}' was removed",
"proposed_version": "2.0.0"
}
# recorded with curl; the sanshain-go invocation is:
$ SANSHAIN_GA=true sanshain-go -trunk provide
Promote a snapshot to GA#
Provide the same content with "stability": "ga"; the answer says
promoted: true.
Recorded (story, step 7)
POST /provide HTTP/1.1
Authorization: Bearer san_…
Content-Type: application/json
{
"producername": "payments",
"stability": "ga",
"openapi_yaml": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 2.1.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n \"201\":\n description: Created\n /payments/{paymentId}:\n get:\n summary: One payment\n parameters:\n - name: paymentId\n in: path\n required: true\n schema: { type: string }\n responses:\n \"200\":\n description: OK\n /payments/{id}/capture:\n post:\n summary: Capture an authorised payment\n responses:\n \"200\":\n description: OK\n /payments/{id}/refund:\n post:\n summary: Refund a captured payment\n responses:\n \"200\":\n description: OK",
"trunk": true
}
HTTP/1.1 202 Accepted
content-type: application/json
content-length: 189
{
"version": "2.1.0",
"stability": "ga",
"content_hash": "sha256:bf9e1de18117a28ac401082d4971041fe250238fe9854d727ba9063318bea904",
"changes": {
"inserts": 0,
"updates": 0,
"deletes": 0
},
"promoted": true
}
Require one endpoint by hand#
The exact pin and one endpoint; the snippet comes back as YAML with the
x-sanshain-* headers.
curl -g -s "$SANSHAIN/require?consumername=orders&producername=payments&version=1.0.0&path=/payments&method=POST&trunk=true" \
-H "Authorization: Bearer $SANSHAIN_TOKEN"
Recorded (story, step 3)
GET /require?consumername=orders&producername=payments&version=1.0.0&path=/payments&method=POST&trunk=true HTTP/1.1
Authorization: Bearer san_…
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
etag: "3251ba1a99d001bd0ce58d03f79fcd014c43e49557c45c13005d6cf3d9d117b9"
x-sanshain-resolution: served
x-sanshain-version: 1.0.0
x-sanshain-stability: ga
content-length: 181
openapi: 3.0.3
info:
title: Payments
version: 1.0.0
paths:
/payments:
post:
summary: Create a payment
responses:
'201':
description: Created
Compare two versions#
A session call; add path and method to diff one endpoint's snippet.
curl -s "$SANSHAIN/admin/producers/payments/diff?api_type=openapi&from=1.0.0&to=2.0.0" \
-H "Authorization: Bearer $SESSION"
Recorded (story, step 8)
GET /admin/producers/payments/diff?api_type=openapi&from=1.0.0&to=2.0.0 HTTP/1.1
Authorization: Bearer <session token>
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 686
--- payments 1.0.0
+++ payments 2.0.0
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Payments
- version: 1.0.0
+ version: 2.0.0
paths:
/payments:
post:
@@ -9,14 +9,20 @@
responses:
"201":
description: Created
- /payments/{id}:
+ /payments/{paymentId}:
get:
summary: One payment
parameters:
- - name: id
+ - name: paymentId
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
+ /payments/{id}/capture:
+ post:
+ summary: Capture an authorised payment
+ responses:
+ "200":
+ description: OK
See who changed an endpoint#
The trail of one endpoint across the version line; an API token suffices.
curl -s "$SANSHAIN/endpoint-versions?service=payments&api_type=openapi&path=/payments&method=POST" \
-H "Authorization: Bearer $SANSHAIN_TOKEN"
Recorded (story, step 8)
GET /endpoint-versions?service=payments&api_type=openapi&path=/payments&method=POST HTTP/1.1
Authorization: Bearer san_…
HTTP/1.1 200 OK
content-type: application/json
content-length: 958
[
{
"version": "1.0.0",
"stability": "ga",
"provided_by": "root",
"updated_at": "2026-10-18T16:07:54Z",
"yaml_content": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 1.0.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n '201':\n description: Created\n",
"changed": true
},
{
"version": "2.0.0",
"stability": "ga",
"provided_by": "root",
"updated_at": "2026-10-18T16:08:02Z",
"yaml_content": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 2.0.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n '201':\n description: Created\n",
"changed": true
},
{
"version": "2.1.0",
"stability": "ga",
"provided_by": "root",
"updated_at": "2026-10-18T16:08:02Z",
"yaml_content": "openapi: 3.0.3\ninfo:\n title: Payments\n version: 2.1.0\npaths:\n /payments:\n post:\n summary: Create a payment\n responses:\n '201':\n description: Created\n",
"changed": true
}
]
GET /admin/endpoint-versions (the session twin)
Cut a release branch#
Needs the releaser role and a session; source and as_of are optional
(trunk, now).
curl -s -X POST $SANSHAIN/admin/branches \
-H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
-d '{"name":"release-2026.10"}'
Recorded (story, step 9)
POST /admin/branches HTTP/1.1
Authorization: Bearer <session token>
Content-Type: application/json
{
"name": "release-2026.10"
}
HTTP/1.1 201 Created
content-type: application/json
content-length: 137
{
"id": 1,
"name": "release-2026.10",
"created_at": "2026-10-18T16:08:03Z",
"created_by": "root",
"source": "trunk",
"as_of": "2026-10-18T16:08:03Z"
}
Get the report for a release#
scope is dev, main or a branch name, the latter two
optionally @<RFC 3339 instant>; /report/markdown and
/report/isolation take the same parameter.
curl -s "$SANSHAIN/report?scope=release-2026.10" -H "Authorization: Bearer $SANSHAIN_TOKEN"
Recorded (story, step 9)
GET /report?scope=release-2026.10 HTTP/1.1
Authorization: Bearer san_…
HTTP/1.1 200 OK
content-type: application/json
content-length: 356
{
"unused_endpoints": [],
"missing_endpoints": [],
"dependency_graph": [
{
"api_type": "openapi",
"client": "orders",
"service": "payments",
"version": "1.0.0",
"stability": "ga",
"path": "/payments",
"method": "POST",
"deprecated": false
}
],
"service_tags": {},
"trunk_graph": [],
"harvested_subscriptions": [],
"peripheral_dependencies": [],
"systems": {},
"scope_label": "release-2026.10"
}
Build on a release branch#
A hotfix pipeline tags its provides and requires with the branch; unknown tags do not fail the
build but land in the dev graph with a Sanshain-Warning.
sanshain-conan --tag release-2026.10 require # also: --tag on every client, SANSHAIN_TAG in CI
Recorded (story, step 9)
# recorded with curl; the sanshain-conan invocation is:
$ sanshain-conan --tag release-2026.10 require
Push the catalog#
With catalogInfo in sanshain.yaml every client pushes on each build;
by hand it is one PUT.
curl -s -X PUT $SANSHAIN/catalog/payments \
-H "Authorization: Bearer $SANSHAIN_TOKEN" -H "Content-Type: application/json" \
-d "{\"catalog_info_yaml\":$(jq -Rs . < catalog-info.yaml),\"trunk\":true}"
Recorded with the Maven plugin (story, step 10)
# recorded with curl; the the Maven plugin invocation is:
$ mvn io.github.paxel.sanshain:sanshain-maven-plugin:2.2.0:catalog -Dsanshain.trunk=true
Ask the catalog over MCP#
What a client sends; in practice the assistant does this for you once the endpoint is configured.
curl -s -X POST $SANSHAIN/mcp \
-H "Authorization: Bearer $SANSHAIN_TOKEN" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"describe_service","arguments":{"name":"payments"}}}'
Recorded (story, step 12)
POST /mcp HTTP/1.1
Authorization: Bearer san_… (read_only)
Content-Type: application/json
Accept: application/json, text/event-stream
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe_service",
"arguments": {
"name": "payments"
}
}
}
HTTP/1.1 200 OK
content-type: application/json
content-length: 2025
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"at\":null,\"consumers\":[{\"api_type\":\"openapi\",\"endpoints\":[{\"method\":\"POST\",\"path\":\"/payments\"}],\"name\":\"orders\",\"version\":\"1.0.0\"}],\"context\":{\"description\":\"Takes payments for the shop and hands them to the bank.\",\"links\":[{\"icon\":null,\"title\":\"Runbook\",\"type\":\"docs\",\"url\":\"https://wiki.acme.corp/payments\"},{\"icon\":null,\"title\":\"Dashboard\",\"type\":null,\"url\":\"https://grafana.acme.corp/d/payments\"}],\"owner\":{\"kind\":\"group\",\"name\":\"team-payments\"},\"source_location\":\"https://github.com/acme/payments/tree/main/\"},\"domain\":null,\"endpoints\":[{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"GET\",\"path\":\"/payments/{paymentId}\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments/{id}/capture\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments/{id}/refund\",\"version\":\"2.1.0\"}],\"icon\":null,\"is_consumer\":false,\"is_producer\":true,\"name\":\"payments\",\"periphery\":[{\"resource\":\"resource:default/bank-gateway\",\"shared_with\":[]},{\"resource\":\"resource:default/payments-db\",\"shared_with\":[]}],\"requires\":[],\"scope\":\"dev\",\"versions\":[{\"api_type\":\"openapi\",\"endpoint_count\":4,\"last_provided_at\":\"2026-10-18T16:08:02Z\",\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":false,\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"endpoint_count\":3,\"last_provided_at\":\"2026-10-18T16:08:02Z\",\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":true,\"version\":\"2.0.0\"},{\"api_type\":\"openapi\",\"endpoint_count\":2,\"last_provided_at\":\"2026-10-18T16:07:54Z\",\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":true,\"version\":\"1.0.0\"}]}"
}
],
"isError": false
}
}
Recorded against v2.6.0.