sovereign/sdk-guides
sdks Python · Go · TypeScript · Java protocol SSP 1.0 verification offline

SDK guides

Client + offline verification for the Sovereign Sign-off Protocol. Sign the upstream lifecycle, open a case for a human to sign, and verify a sealed Sovereign Record — in your language. Pick a language once and every snippet on this page follows.

two capabilities, four languages

Client — sign the upstream events (policy.committed, ai.inference.completed, guardrail.evaluated) with your Ed25519 key and open a case (single approver, or a multi-approver quorum policy); learn how it was decided via a sealed-decision callback or by polling case_status; emit outcome.executed once it's sealed. Verify — check a record.ssp.json offline with no server: canonical claim hash, the approvers' WebAuthn signatures, quorum satisfaction, the Ed25519 trust signature, and the Rekor / RFC 3161 anchors. Every language, same depth.

1 · Install

From the Sovereign GitLab package registry (you supply a token with read_package_registry). Current release: 0.4.0 everywhere. Python, npm and Maven come from the registry; Go resolves the v0.4.0 git tag on its repo — every SDK is tagged v0.4.0 at the same commit.

pip install sovereign-saa-sdk \
  --index-url "https://__token__:<read-token>@git.xor.ma/api/v4/groups/sovereign%2Fsovereign-sdk/-/packages/pypi/simple"
# with GOPRIVATE set and a token in ~/.netrc for git.xor.ma
go get git.xor.ma/sovereign/sovereign-sdk/saa-go-sdk@v0.4.0

# @latest also works; releases are git tags, so pin the tag for a
# reproducible build rather than taking whatever main resolves to
# .npmrc
@sovereign:registry=https://git.xor.ma/api/v4/groups/sovereign%2Fsovereign-sdk/-/packages/npm/
//git.xor.ma/api/v4/groups/sovereign%2Fsovereign-sdk/-/packages/npm/:_authToken=<read-token>

npm install @sovereign/saa-sdk
<dependency>
  <groupId>ma.xor.sovereign</groupId>
  <artifactId>saa-sdk</artifactId>
  <version>0.4.0</version>
</dependency>
<!-- plus the sovereign-sdk <repository> + a Deploy-Token header in settings.xml -->

2 · Open a case

Sign the upstream events and open a case via the client API. The response carries the submission_id and the approver_url where a human signs. due_at is required — every case carries an explicit approval SLA. Pass an optional quorum policy for a multi-approver sign-off; omit it for a single approver.

from sovereign.saa import Client, Signer, base_claim

signer = Signer.load("saa-client.key")          # generates + persists on first use
client = Client("https://app.sovereign.xor.ma/api", api_key="saa-dev-client-key",
                signer=signer, tenant="acme-bank")

events = [signer.sign_event({**base_claim("ai.inference.completed", "acme-bank", "subj-001"),
          "result": {"recommendation": "approve", "risk_score": 0.18, "threshold": 0.70}})]

resp = client.create_case(
    domain="finance", artifact=b"Pay vendor ACME 12,750 USD", filename="wire.txt",
    mime_type="text/plain", client_events=events, subject="subj-001",
    context={"transaction_id": "TXN-1", "amount_usd": 12750, "account_id": "ACC-1"},
    due_at="2026-12-31T17:00:00Z",                  # required: approval SLA deadline
    # optional — roles with counts ("Senior Credit Officer*2, Compliance") or
    # named users ("@jdoe, @bofficer"); the required count comes from the slots
    quorum={"dsl": "Senior Credit Officer*2, Compliance",
            "mode": "parallelRoles",                # or sequential / parallel / custom
            "workflow": {"type": "ratify"}})        # or independent + vote/veto
print(resp["submission_id"], resp["approver_url"])
import (
    "fmt"

    saa "git.xor.ma/sovereign/sovereign-sdk/saa-go-sdk"
)

signer, _ := saa.LoadSigner("saa-client.key")   // generates + persists on first use
client, _ := saa.NewClient("https://app.sovereign.xor.ma/api", "saa-dev-client-key",
    saa.WithSigner(signer), saa.WithTenant("acme-bank"))

ai := saa.BaseClaim("ai.inference.completed", "acme-bank", "subj-001")
ai["result"] = map[string]any{"recommendation": "approve", "risk_score": 0.18, "threshold": 0.70}
ev, _ := signer.SignEvent(ai)

resp, _ := client.CreateCase(saa.CaseInput{
    Domain: "finance", Artifact: []byte("Pay vendor ACME 12,750 USD"),
    Filename: "wire.txt", MimeType: "text/plain", Subject: "subj-001",
    Context:      map[string]any{"transaction_id": "TXN-1", "amount_usd": 12750, "account_id": "ACC-1"},
    ClientEvents: []map[string]any{ev},
    DueAt:        "2026-12-31T17:00:00Z", // required: approval SLA deadline
    // Optional — roles with counts or named @users; count comes from the slots.
    Quorum: map[string]any{
        "dsl":      "Senior Credit Officer*2, Compliance",
        "mode":     "parallelRoles",                        // or sequential / parallel / custom
        "workflow": map[string]any{"type": "ratify"},       // or independent + vote/veto
    },
})
fmt.Println(resp["submission_id"], resp["approver_url"])
import { Client, Signer, baseClaim } from "@sovereign/saa-sdk";

const signer = Signer.load("saa-client.key");   // generates + persists on first use
const client = new Client("https://app.sovereign.xor.ma/api", "saa-dev-client-key",
  { signer, tenant: "acme-bank" });

const ai = signer.signEvent({ ...baseClaim("ai.inference.completed", "acme-bank", "subj-001"),
  result: { recommendation: "approve", risk_score: 0.18, threshold: 0.7 } });

const resp = await client.createCase({
  domain: "finance", artifact: new TextEncoder().encode("Pay vendor ACME 12,750 USD"),
  filename: "wire.txt", mimeType: "text/plain", subject: "subj-001",
  context: { transaction_id: "TXN-1", amount_usd: 12750, account_id: "ACC-1" },
  clientEvents: [ai],
  dueAt: "2026-12-31T17:00:00Z",             // required: approval SLA deadline
  // optional — roles with counts or named @users; count comes from the slots
  quorum: { dsl: "Senior Credit Officer*2, Compliance",
            mode: "parallelRoles",           // or sequential / parallel / custom
            workflow: { type: "ratify" } },  // or independent + vote/veto
});
console.log(resp.submission_id, resp.approver_url);
import ma.xor.sovereign.saa.*;
import java.util.Map;

Signer signer = Signer.load("saa-client.key");  // generates + persists on first use
SaaClient client = new SaaClient("https://app.sovereign.xor.ma/api",
    "saa-dev-client-key", signer, "acme-bank", /*insecureTls*/ false);

Map<String,Object> ai = Signer.baseClaim("ai.inference.completed", "acme-bank", "subj-001");
ai.put("result", Map.of("recommendation", "approve", "risk_score", 0.18, "threshold", 0.70));

SaaClient.CaseInput in = new SaaClient.CaseInput();
in.domain = "finance";
in.artifact = "Pay vendor ACME 12,750 USD".getBytes("UTF-8");
in.filename = "wire.txt"; in.mimeType = "text/plain"; in.subject = "subj-001";
in.context = Map.of("transaction_id", "TXN-1", "amount_usd", 12750, "account_id", "ACC-1");
in.clientEvents = java.util.Arrays.asList(signer.signEvent(ai));
in.dueAt = "2026-12-31T17:00:00Z";                  // required: approval SLA deadline
// optional — roles with counts or named @users; count comes from the slots
in.quorum = Map.of("dsl", "Senior Credit Officer*2, Compliance",
                   "mode", "parallelRoles",         // or sequential / parallel / custom
                   "workflow", Map.of("type", "ratify"));   // or independent + vote/veto

Map<String,Object> resp = client.createCase(in);
System.out.println(resp.get("submission_id") + " " + resp.get("approver_url"));

3 · Learn the outcome

Register a webhook at create time, or poll. The webhook fires once, when the decision seals; template is a Go text/template, so the payload shape is yours. Delivery retries three times and the attempt count lands in case_status.

# 1. push — register a webhook at create time; it fires once, when sealed
resp = client.create_case(
    ...,                                     # as above
    callback={"url": "https://erp.example/hooks/sovereign-sealed",
              "token": "one-time-secret",          # Bearer + X-Callback-Token
              "content_type": "application/json",  # default
              "template": '{"id":"{{.ledger_uuid}}","outcome":"{{.outcome}}"}'})
resp["callback"]        # {"registered": True, "url": "..."}

# 2. pull — poll the case instead, or as a backstop for a missed webhook
s = client.case_status(resp["submission_id"])
s["status"]             # "pending" | "sealed"
s["outcome"], s["ledger_uuid"], s["verify_url"]   # once sealed
s.get("quorum")         # {"required": 3, "collected": 1, ...} on a quorum case
s.get("callback")       # {"attempts": 1, "delivered_at": "...", "last_error": ""}
// 1. push — register a webhook at create time; it fires once, when sealed
resp, _ := client.CreateCase(saa.CaseInput{
    /* ... as above ... */
    Callback: &saa.Callback{
        URL:         "https://erp.example/hooks/sovereign-sealed",
        Token:       "one-time-secret",  // Bearer + X-Callback-Token
        ContentType: "application/json", // default
        Template:    `{"id":"{{.ledger_uuid}}","outcome":"{{.outcome}}"}`,
    },
})
// resp["callback"] → {"registered": true, "url": "..."}

// 2. pull — poll the case instead, or as a backstop for a missed webhook
s, _ := client.CaseStatus(resp["submission_id"].(string))
// s["status"]                                      "pending" | "sealed"
// s["outcome"], s["ledger_uuid"], s["verify_url"]  once sealed
// s["quorum"]   {"required": 3, "collected": 1, ...} on a quorum case
// s["callback"] {"attempts": 1, "delivered_at": "...", "last_error": ""}
// 1. push — register a webhook at create time; it fires once, when sealed
const resp = await client.createCase({
  ...,                                       // as above
  callback: { url: "https://erp.example/hooks/sovereign-sealed",
              token: "one-time-secret",         // Bearer + X-Callback-Token
              content_type: "application/json", // default
              template: '{"id":"{{.ledger_uuid}}","outcome":"{{.outcome}}"}' },
});
resp.callback;          // { registered: true, url: "..." }

// 2. pull — poll the case instead, or as a backstop for a missed webhook
const s = await client.caseStatus(resp.submission_id as string);
s.status;               // "pending" | "sealed"
s.outcome; s.ledger_uuid; s.verify_url;      // once sealed
s.quorum;               // { required: 3, collected: 1, ... } on a quorum case
s.callback;             // { attempts: 1, delivered_at: "...", last_error: "" }
// 1. push — register a webhook at create time; it fires once, when sealed
in.callback = Map.of(
    "url", "https://erp.example/hooks/sovereign-sealed",
    "token", "one-time-secret",              // Bearer + X-Callback-Token
    "content_type", "application/json",      // default
    "template", "{\"id\":\"{{.ledger_uuid}}\",\"outcome\":\"{{.outcome}}\"}");
Map<String,Object> resp = client.createCase(in);
resp.get("callback");   // {"registered": true, "url": "..."}

// 2. pull — poll the case instead, or as a backstop for a missed webhook
Map<String,Object> s = client.caseStatus((String) resp.get("submission_id"));
s.get("status");        // "pending" | "sealed"
s.get("outcome"); s.get("ledger_uuid"); s.get("verify_url");   // once sealed
s.get("quorum");        // {"required": 3, "collected": 1, ...} on a quorum case
s.get("callback");      // {"attempts": 1, "delivered_at": "...", "last_error": ""}

Template context: submission_id, ledger_uuid, outcome, sealed_at, topic, party, domain, subcategory, tenant, verify_url, plus the sealed record and its claim.

Records carry the signer's role, never their name — and a ratify quorum has no claim.human, so guard it: {{with .claim.human}}{{.role}}{{end}}.

4 · Record the outcome

After the decision is sealed, emit a client-signed outcome.executed linked to the ledger entry.

client.record_outcome(ledger_uuid, status="executed")
client.RecordOutcome(ledgerUUID, "executed")
await client.recordOutcome(ledgerUuid, "executed");
client.recordOutcome(ledgerUuid, "executed");

5 · Verify a record offline

Verify a sealed record.ssp.json with no server. The record alone covers the claim hash, the WebAuthn signatures, quorum satisfaction and the trust wrap; add anchors.json and timestamp.tsr for the Rekor and RFC 3161 checks.

import json
from sovereign.saa import verify_record

record = json.load(open("record.ssp.json"))
trust = open("trust-public.pem").read()         # out-of-band

result = verify_record(
    record, trust, rp_id="app.sovereign.xor.ma",
    record_bytes=open("record.ssp.json", "rb").read(),   # exact bytes, for the Rekor binding
    anchors=json.load(open("anchors.json")),             # optional: Rekor
    tsa_token=open("timestamp.tsr", "rb").read(),        # optional: RFC 3161
    tsa_ca_pem=open("tsa-ca.pem").read())                # optional: chain the TSA signer
print(result)            # per-check PASS / FAIL / SKIP + overall
assert result.ok
result.skipped           # what was not covered
import (
    "fmt"
    "os"

    saa "git.xor.ma/sovereign/sovereign-sdk/saa-go-sdk"
)

record, _ := os.ReadFile("record.ssp.json")
trust, _ := os.ReadFile("trust-public.pem")     // out-of-band

anchorsBytes, _ := os.ReadFile("anchors.json")
tsr, _ := os.ReadFile("timestamp.tsr")
tsaCA, _ := os.ReadFile("tsa-ca.pem")
var anchors map[string]any
json.Unmarshal(anchorsBytes, &anchors)

res, _ := saa.VerifyRecordFull(record, trust, "app.sovereign.xor.ma",
    anchors, "", tsr, tsaCA)      // anchors/token/CA optional — omit to skip
fmt.Println(res)         // per-check PASS / FAIL / SKIP
// res.OK() == true ; res.Skipped() lists what was not covered
import { readFileSync } from "node:fs";
import { verifyRecord } from "@sovereign/saa-sdk";

const record = readFileSync("record.ssp.json");
const result = verifyRecord(record, readFileSync("trust-public.pem", "utf8"), {
  rpId: "app.sovereign.xor.ma",
  recordBytes: record,                                     // for the Rekor binding
  anchors: JSON.parse(readFileSync("anchors.json", "utf8")),
  tsaToken: readFileSync("timestamp.tsr"),
  tsaCaPem: readFileSync("tsa-ca.pem", "utf8"),
});
console.log(result.toString());   // per-check PASS / FAIL / SKIP
// result.ok === true ; result.skipped lists what was not covered
import ma.xor.sovereign.saa.*;
import java.nio.file.Files;
import java.nio.file.Paths;

byte[] record = Files.readAllBytes(Paths.get("record.ssp.json"));
byte[] trust  = Files.readAllBytes(Paths.get("trust-public.pem"));  // out-of-band

byte[] tsr   = Files.readAllBytes(Paths.get("timestamp.tsr"));
byte[] tsaCA = Files.readAllBytes(Paths.get("tsa-ca.pem"));
Map<String,Object> anchors = (Map<String,Object>) Json.parse(
    new String(Files.readAllBytes(Paths.get("anchors.json")), "UTF-8"));

VerifyResult res = Verify.verifyRecord(record, trust, "app.sovereign.xor.ma",
        anchors, null, tsr, tsaCA);   // anchors/token/CA optional — omit to skip
System.out.println(res); // per-check PASS / FAIL / SKIP
// res.ok() == true ; res.skipped() lists what was not covered

Notes

Canonicalization is RFC 8785-style JCS, byte-identical across the four SDKs — a record signed via one verifies in any other. verify_record dispatches on the record's signatures, so single-approver, ratify-quorum and independent-vote records are all handled without you picking a shape.

CheckPythonGoTypeScriptJava
Canonical claim hash
Ed25519 trust signature
Ratify quorum — slot satisfaction against the anchored policy, distinct signers, every slot filled
Independent resolution — per-vote claim-hash integrity, recomputed tally / veto and votes_root
WebAuthn user signature (COSE/CBOR)
Rekor anchor — SET, inclusion proof, entry binding
RFC 3161 timestamp — imprint, CMS signature, TSA chain

Dependencies used for verification:

SDKDepends on
Gostandard library — crypto/*, encoding/asn1
TypeScriptNode built-ins — node:crypto
JavaBouncyCastle bcprov (also its Ed25519)
Pythoncryptography; asn1crypto for RFC 3161 (rfc3161 extra)

COSE/CBOR is decoded in-SDK everywhere; DER in-SDK in TypeScript only. A malformed token or key fails its check rather than raising.