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
- An SDCStudio account with a funded wallet. See Wallet & Billing. A $10 purchase covers ten Receipts.
- 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. - 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.
- Two parties with published keys. Each party runs
sdcreceipt init --key-id <their URI>once and publishes the resulting key document at that URI (anhttps://URL or adid: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.
Related
- Wallet & Billing: credits, the Receipt price, what is and is not charged.
- Privacy Policy, Section 6: what a Receipt discloses, and to whom.
- sdcreceipt on PyPI and source, Apache-2.0.
python examples/settle.pyruns a whole settlement on your own machine. - Verifiable Settlement Layer on axius-sdc.com.
Need help? Contact contact@axius-sdc.com.