Documentation for v2.11.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": "7eca0ddf-e2ea-4a68-8f05-9182714c81cb",
  "name": "claude-code",
  "scope": "read_only",
  "token": "san_…"
}

MCP: the token · POST /auth/tokens · Scopes

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"
  ]
}

The validator · POST /validate

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"
}

The server proposes · Version conflicts are self-service

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"
}
# payments/, release pipeline
$ SANSHAIN_GA=true sanshain-go -trunk provide
Providing OpenAPI payments (stability: ga) to https://sanshain.corp...
✓ Provided payments v2.0.0 (ga): 3 new, 0 updated, 0 deleted endpoints
Successfully provided spec(s).

The rules of a version line · Clients: Go

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
}

Going GA · Glossary: promotion

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

Concepts: pins · GET /require

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

The spec viewer · GET /admin/producers/{name}/diff

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-23T19:19:22Z",
    "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-23T19:19:31Z",
    "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-23T19:19:31Z",
    "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-23T19:19:31Z",
  "created_by": "root",
  "source": "trunk",
  "as_of": "2026-10-23T19:19:31Z"
}

Cutting a branch · POST /admin/branches

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"
}

Reports · Using the API: reports and scopes

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)

# orders/, hotfix pipeline of release-2026.10
$ sanshain-conan --tag release-2026.10 require
Requiring 1 endpoints from payments (version: 1.0.0, type: openapi)...
Saved to generated/payments/payments.yaml

Streams from the pipeline · Clients: Conan

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)

# payments/, trunk CI
$ mvn -s settings.xml -Dsanshain.trunk=true io.github.paxel.sanshain:sanshain-maven-plugin:2.2.0:catalog
[INFO] Scanning for projects...
[INFO] 
[INFO] -------------------------< com.acme:payments >--------------------------
[INFO] Building payments 2.1.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- sanshain:2.2.0:catalog (default-cli) @ payments ---
[INFO] Pushing catalog for payments: catalog-info.yaml
[INFO] Catalog recorded for payments (main+dev): 2 resources
[INFO]   resource:default/bank-gateway
[INFO]   resource:default/payments-db
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.683 s
[INFO] Finished at: 2026-10-23T19:19:33Z
[INFO] ------------------------------------------------------------------------

The catalog push · PUT /catalog/{systemname}

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: 2495

{
  "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/\",\"tags\":[]},\"domain\":null,\"endpoints\":[{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments\",\"summary\":\"Create a payment\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments/{id}/capture\",\"summary\":\"Capture an authorised payment\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"POST\",\"path\":\"/payments/{id}/refund\",\"summary\":\"Refund a captured payment\",\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"deprecated\":false,\"method\":\"GET\",\"path\":\"/payments/{paymentId}\",\"summary\":\"One payment\",\"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-23T19:19:31Z\",\"origin\":{\"build_url\":null,\"change_url\":null,\"note\":null,\"tickets\":[],\"trunk_build\":false},\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":false,\"version\":\"2.1.0\"},{\"api_type\":\"openapi\",\"endpoint_count\":3,\"last_provided_at\":\"2026-10-23T19:19:31Z\",\"origin\":{\"build_url\":null,\"change_url\":null,\"note\":null,\"tickets\":[],\"trunk_build\":false},\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":true,\"version\":\"2.0.0\"},{\"api_type\":\"openapi\",\"endpoint_count\":2,\"last_provided_at\":\"2026-10-23T19:19:22Z\",\"origin\":{\"build_url\":null,\"change_url\":null,\"note\":null,\"tickets\":[],\"trunk_build\":false},\"provided_by\":\"root\",\"stability\":\"ga\",\"superseded\":true,\"version\":\"1.0.0\"}]}"
      }
    ],
    "isError": false
  }
}

MCP: the tools · MCP: client configuration

Recorded against v2.6.0.