Verifiable Settlement Layer (VSL)

Overview

The Verifiable Settlement Layer settles an exchange between two parties in one API call. SDCStudio validates the data instance against its published model, evaluates the governance decision for the stated transition, binds both parties and the release condition into a Settlement Receipt, and signs it. Each party then signs a trigger with a key they control. Anyone holding the Receipt verifies it offline against published keys, with the open-source sdcreceipt tool, no account required.

Three things to hold onto:

  • A Receipt carries hashes and identifiers, never your data. The instance is validated, evaluated and discarded; only its hash persists. See the Privacy Policy, Section 6.
  • Verification needs nothing from us. Keys are published at fixed locations, the procedure is a document, and the verifier is Apache-2.0.
  • One price. A Receipt is 1,000 credits ($1.00), charged when it is issued. Verifying is free, forever.

Before you start

  1. An SDCStudio account with a funded wallet. See Wallet & Billing. A $10 purchase covers ten Receipts.
  2. An API key. Go to Settings > API Keys and click Generate. The key is shown once; store it. Send it on every settle request as Authorization: Token <key>. The same page revokes and rotates it.
  3. A published data model. Only a published model can be settled against. Publishing makes the model's schema resolvable by identifier (below), whatever the project's visibility.
  4. Two parties with published keys. Each party runs sdcreceipt init --key-id <their URI> once and publishes the resulting key document at that URI (an https:// URL or a did:web: identifier). The issuer never generates or holds party keys.
pip install sdcreceipt

The endpoints

Method Path Auth Purpose
POST /api/v1/vsl/settle API key Issue a Settlement Receipt. Charged.
POST /api/v1/vsl/trigger none A party signs the release condition for a Receipt.
GET /api/v1/vsl/receipt/{receipt_id} none Fetch a Receipt and its settlement state. Cacheable.
GET /dmlib/dm-{ct_id}.xsd none The schema a Receipt names, byte for byte.
GET /.well-known/sdcstudio-signing-keys.json none The issuer's key document.

Interactive reference: Swagger UI and ReDoc; the raw schema is at /api/schema/.

1. Settle

Send the instance, the transition, the release condition and the parties.

sdcreceipt settle instance.xml \
    --endpoint https://sdcstudio.axius-sdc.com/api/v1/vsl/settle \
    --token "$SDCRECEIPT_TOKEN" \
    --current-state draft --target-state review \
    --condition '{"on": "goods received"}' \
    --party https://vendor.example/.well-known/vsl-key.json \
    --party did:web:partner.example \
    --out receipt.json

Or as a plain request:

POST /api/v1/vsl/settle
Authorization: Token <your api key>
Content-Type: application/json

{
  "payload": "<?xml version=\"1.0\"?> ... the SDC4 XML instance as a string ...",
  "current_state": "draft",              # optional: the instance's own <current-state> is authoritative; if given it must match
  "target_state": "review",
  "condition": {"on": "goods received"},
  "parties": ["https://vendor.example/.well-known/vsl-key.json", "did:web:partner.example"],
  "previous_receipt_id": "optional: chain onto an earlier Receipt"
}

What happens, in order. The model is resolved from the instance's own identifier. The instance is validated; a structural failure stops here. Governance evaluates the transition with sdcgovernance. A Receipt is built, signed with the issuer key, stored, and charged. The wallet is debited last, inside the same transaction as the write: you never pay for a Receipt that does not exist, and a Receipt that exists was always paid for.

The response (201):

{
  "receipt": { "...": "the signed Settlement Receipt, version 1.0" },
  "governance": {"decision": "PERMIT", "settleable": true},
  "verification": {
    "tool": "pip install sdcreceipt",
    "command": "sdcreceipt verify receipt.json --keys keys.json",
    "keys_url": "https://sdcstudio.axius-sdc.com/.well-known/sdcstudio-signing-keys.json",
    "schema_url": "https://semanticdatacharter.com/dmlib/dm-<ct_id>.xsd",
    "schema_resolver_url": "https://sdcstudio.axius-sdc.com/dmlib/dm-<ct_id>.xsd"
  },
  "wallet": {"charged": "1.00", "balance": "4.00"}
}

Hand receipt to both parties. verification tells them where the keys and the schema are.

The three verdicts

Decision What happened Receipt Charged Settleable
PERMIT governance allows the transition issued yes yes
DENY governance refused the transition issued, records the refusal yes no: triggers are refused (422) and nothing can chain onto it
INDETERMINATE the engine could not evaluate (usually no readable workflow in the instance) none no n/a

A DENY is a verdict, and a signed record that a transition was refused is a result a counterparty can hold, so it is charged like any Receipt. The 201 for a DENY carries governance.settleable: false, allowed_transitions and the model's full state map, so you do not pay a second dollar to learn what would have worked.

Errors

Status Meaning Body
400 not an SDC4 instance, no published model for its identifier, unknown previous_receipt_id, or chaining onto a DENY Receipt error
401 missing or wrong API key
402 insufficient credits; nothing was written error, cost, balance
422 structural validation failed; the model defines no governance; current_state disagrees with the instance's own <current-state> (body carries instance_state and stated_state); or INDETERMINATE error, and for governance refusals current_state, allowed_transitions, workflow, model_defines_workflow_states, hint
503 the signer was unavailable; nothing was written error

sdcreceipt settle exits 2 on a 422 that carries allowed_transitions and prints them; 1 on anything else.

2. Trigger

Each party signs {condition_hash, receipt_id} with their own key. Including the receipt_id is what stops a signature being replayed into another Receipt that happens to share the same release condition.

sdcreceipt trigger receipt.json \
    --key vsl-party.pem \
    --key-id https://vendor.example/.well-known/vsl-key.json \
    --submit https://sdcstudio.axius-sdc.com/api/v1/vsl/trigger

Without --submit the signed trigger is printed and nothing is sent. As a plain request:

POST /api/v1/vsl/trigger
Content-Type: application/json

{
  "receipt_id": "k7m2p9x4qw8vzn3hjt6bcdfg",
  "key_id": "https://vendor.example/.well-known/vsl-key.json",
  "signature": "<86 base64url characters, ES256 P1363>",
  "timestamp": "2026-09-16T15:00:00Z"
}

The issuer checks that key_id is a listed party before resolving anything, fetches the party's published key over HTTPS from that key_id, verifies the signature, refuses a replay, and appends the trigger. When every listed party has triggered the Receipt is settled and can never change again.

Response (200): settled, receipt_id, status (open or settled), decision, settleable, parties, triggered, awaiting.

Status Meaning
400 bad signature, replayed trigger (code: replayed_trigger), or a key_id that points inside the issuer's network
403 key_id is not a party to this settlement
404 no such Receipt
409 already settled
422 the Receipt records a DENY; a refused transition cannot be settled
502 the party's key document could not be fetched or holds no P-256 key

3. Verify

Anyone holding a Receipt verifies it with the issuer's key document and each party's:

curl -O https://sdcstudio.axius-sdc.com/.well-known/sdcstudio-signing-keys.json
curl -o vendor-keys.json  https://vendor.example/.well-known/vsl-key.json
curl -o partner-keys.json https://partner.example/.well-known/did.json
sdcreceipt verify receipt.json \
    --keys sdcstudio-signing-keys.json --keys vendor-keys.json --keys partner-keys.json
PASS  receipt_hash: matches the canonical content
PASS  signature[sdcstudio-signing-key-v1]: verifies over receipt_hash
PASS  trigger[https://vendor.example/.well-known/vsl-key.json]: verifies over {condition_hash, receipt_id}
PASS  trigger[did:web:partner.example]: verifies over {condition_hash, receipt_id}
PASS  triggers.unique: one trigger per party
PASS  settlement.complete: every listed party has triggered

VERIFIED

Every check is reported, not just the first failure, and the command exits 0 only when all pass. With only the issuer's document the signature and hashes are checked and settlement.complete is recorded as unestablished, so the Receipt does not verify: that is the honest answer when no trigger signature was checked. Pass --payload or --governance to check those hashes too if you hold them.

Never take a key from a location named inside the Receipt. Keys come from the issuer's well-known path and from the parties' published key_ids, decided before the document is read. Keep copies: a party who later loses a domain must not be able to change what their old signatures mean.

Fetching a Receipt

curl https://sdcstudio.axius-sdc.com/api/v1/vsl/receipt/k7m2p9x4qw8vzn3hjt6bcdfg

Returns receipt plus the settlement state. Unauthenticated: verification must not depend on our permission. A settled Receipt is served with a one-year immutable cache header; an open one for 60 seconds. Anyone who holds a Receipt id can read the Receipt, so treat the id as you would the Receipt.

Key discovery

The issuer. https://sdcstudio.axius-sdc.com/.well-known/sdcstudio-signing-keys.json is a key document with a keys array, newest first, one entry per key version the issuer has ever used. Each entry has key_id, alg (ES256), public_key_pem, status (active, retired or revoked), not_before and not_after. A key that has signed a Receipt is never removed: after a rotation it is retired with not_after set to the moment its successor took over, and Receipts it signed keep verifying. Only a compromised key is revoked, and sdcreceipt fails every signature that names one. The signature on a Receipt names the key_id it was made with. /.well-known/sdcstudio-signing-key.pem serves the current key alone, as PEM. Keep a copy of the document with the Receipt: verification then survives the issuer, and the window is what to compare the Receipt's timestamp against.

The parties. A party's key_id is the URI their key document is published at. sdcreceipt init writes the document and prints the exact URL it must be reachable from: the URL itself for https:// identifiers, /.well-known/did.json (or the path segments plus /did.json) for did:web:. A key marked status: revoked in its document fails any signature it is named on.

Schema resolution

A Receipt names its schema as https://semanticdatacharter.com/dmlib/dm-{ct_id}.xsd, the same identifier the instance carries in xsi:schemaLocation, and records schema_hash. Anyone holding the SDC4 library resolves the identifier offline through the shipped XML catalog. Anyone else fetches it from the issuer:

curl -O https://sdcstudio.axius-sdc.com/dmlib/dm-<ct_id>.xsd
sha256sum dm-<ct_id>.xsd     # equals receipt.schema_hash

The route serves the stored bytes of any published model, whatever the project's visibility, with an immutable cache header and an ETag. Unpublished or unknown identifiers answer 404. If a model's definition must not leave your organization, do not publish it and do not settle against it.

What is in a Receipt

Field What it is
version, canonicalization 1.0, rfc8785. The wire format is frozen.
receipt_id, timestamp Assigned at issue.
payload_hash, schema_ct_id, schema_url, schema_hash, rm_version What was validated, against what.
validation valid, error counts, ev_injections (each an xpath, an ExceptionalValue type and a value-free reason), validator version.
provenance_hash A keyed hash binding the Receipt to the issuing account, linkable only by the issuer.
governance decision, dimensions_checked, and the hash of the sdcgovernance Receipt that produced it.
settlement condition_hash, parties, triggers.
previous_hash, receipt_hash, signatures The chain, the hash over everything except itself, signatures and triggers, and the issuer's ES256 signature over that hash.

The payload, the release condition and any value from the payload are never in it.

For an agent

sdcreceipt-mcp exposes verify_receipt, sign_trigger and settle over MCP stdio. The signing key, its identity and the issuer endpoint are fixed when the operator starts the server; no tool takes a URL, a key path or an identity. The sdcvalidator and sdcgovernance MCP servers let an agent validate and evaluate before it settles.


Need help? Contact contact@axius-sdc.com.