VDA Witness · docs
← Home

Developer docs

Seal governed AI-agent decisions into tamper-evident, Ed25519-signed records — and verify them offline, against public infrastructure, with zero dependency on VDA. Three ways in: SDK, MCP, raw REST. OpenAPI spec →

Jump to: Get a keyDurable account + renewVerify (keyless, 30s)SealShaped seals (taxonomy)Admission credentialsCustomer-managed custodyRead your recordsSuite-service resolutionPricingMCP connectorsSchemas & limitsReport

1 · Get an API key

Fastest first success needs no keyverify is public (skip to Verify). To seal, get a key:

instant · no human in the loop   Sealed-tier key (one click on the home page, or:)

curl -X POST https://witness.getvda.ai/api/witness/test-key -H "content-type: application/json" -d '{"email":"you@example.com"}'
# -> { "apiKey":"wtn.xxxx.yyyy", "accountId":"acct_...", "tier":"test", "compliance":false, "expiresInDays":7 }

Autonomous agents: this is discoverable in-band — the A2A card's provisioning.selfServeKey field and the MCP get_test_key tool point here, so an agent can bootstrap a key with no homepage scraping.

⚠️ The Sealed tier is signed + hash-chained + independently verifiable OFFLINE, but NOT externally anchored. Records are not committed to Rekor/TSAs, so integrity is not "provable even against VDA" on this tier — and this is the terminal state here (a Sealed record stays sealed; it does not become anchored). For the Anchored tier (externally committed, provable even against us) → talk to us (concierge-provisioned, not self-serve).

Every response exposes sealed (always true), anchored (true/false), and sealedState: anchored · anchoring_pending (Anchored tier, the next hourly tick commits it) · not_anchored (Sealed tier, terminal — reason anchoring_not_enabled_for_account). The three-state verdict (ANCHORED_VALID/SIGNED_PENDING/BROKEN) is unchanged; tier/compliance are deprecated aliases.

Use the key as a bearer token: Authorization: Bearer <key> (the x-witness-key: <key> header also works). Rotate/replace anytime.

1b · Durable account + agent-provable renewal — THE provisioning model

This is the way to provision, not an upgrade to the quick-start key above: bind a controller key you hold, then mint your own short-lived keys by renewing. An API key is a short-lived credential; the account is the durable identity that outlives it — so there is no standing credential to leak, and an agent re-keys its OWN account indefinitely with no human and no permanent secret. Concierge/service provisioning goes further: supply a controller and no bootstrap key is issued at all — your first key comes from a renewal (apiKey:null at creation). A returned key with no controller is a quick-start exception; a non-expiring key is an explicit, audited exception — neither is the norm.

1. Claim a durable account — generate an Ed25519 keypair, send the public JWK:

curl -X POST https://witness.getvda.ai/api/witness/test-key -H "content-type: application/json" \
  -d '{"controllerPublicKeyJwk":{"kty":"OKP","crv":"Ed25519","x":"<base64url>"}}'
# -> { "apiKey":"wtn...", "accountId":"acct_...", "durable":true, "controllerBound":true, "keyExpiresAt":"...", "keyTtlSec":86400 }

The account is now durable (no 7-day reaper); the key is short-lived (~24h). Already hold a legacy key? Bind a controller to that existing account: POST https://witness.getvda.ai/api/witness/account/bind-controller (auth: your current key) with the same JWK.

2. Renew unattended when the key expires — same account, same chain:

# a. get a challenge nonce
curl -X POST https://witness.getvda.ai/api/witness/renew/challenge -H "content-type: application/json" -d '{"accountId":"acct_..."}'
# -> { "nonce":"...", "expiresInSec":300 }
# b. Ed25519-sign the exact string:  vda.witness.renew/1|<accountId>|<nonce>   with your controller PRIVATE key
# c. exchange the signature for a fresh key
curl -X POST https://witness.getvda.ai/api/witness/renew -H "content-type: application/json" \
  -d '{"accountId":"acct_...","nonce":"...","signature":"<base64url>"}'
# -> { "apiKey":"wtn...(fresh)", "accountId":"acct_...(same)", "keyExpiresAt":"...", "keyTtlSec":86400 }

Prior keys stay valid until they expire (overlap = zero-downtime rotation). Renewal is authorised by your controller-key signature, rate-limited per account, and not subject to the anonymous-mint caps — you proved you own the account. Also discoverable in-band: agent card provisioning.renewKey + MCP tools renew_challenge / renew_key.

A leaked short-TTL key dies within a day; the account never dies. Seals continue at the next seq with an unbroken prev-hash. Upgrading a test account to anchored (compliance) later preserves the same account id and chains — earlier records stay honestly labelled non-anchored.

2 · Verify a record — keyless, 30 seconds

SDK (offline, no VDA call)

npm i vda-witness        # or: pip install "vda-witness[verify]"
import { offlineVerify } from "vda-witness/verify";
const v = await offlineVerify({ record, chain, didDocument, anchor }); // "bundled" | "live-public"
// v.state === "ANCHORED_VALID" | "SIGNED_PENDING" | "BROKEN" | "INSUFFICIENT_PROOF"
Chain continuity is checkable OFFLINE with zero Witness calls. A single record can't prove its position without predecessors, so fetch a self-contained bundle (account-scoped, Bearer key):
curl "https://witness.getvda.ai/api/witness/records/<recordId>?proof=chain" -H "authorization: Bearer $KEY"
# -> { record, chain:[genesis..head], anchor:{rekor SET + TSA tokens}, didDocument, proofType:"hash-chain-predecessor-path" }
curl "https://witness.getvda.ai/api/witness/chains/<chainKey>/proof" -H "authorization: Bearer $KEY"   # whole trail in one pass
Feed {record, chain, anchor, didDocument} to offlineVerify — it checks signature + key-at-signing + chain continuity + anchor, never calling witness.getvda.ai. INSUFFICIENT_PROOF means the bundle was incomplete (missing predecessors) — it is not tampering; BROKEN/chain is reserved for a genuinely altered/removed/reordered chain. Size is linear in chain length (predecessor path to the anchored head), not a Merkle inclusion proof.

REST (server-side verify, public)

curl -X POST https://witness.getvda.ai/api/witness/verify -H "content-type: application/json" \
  -d '{"record": { ...a witness record... }}'
# -> { "ok": true, "bodyHash": "sha256:...", "signatureValid": true, "errors": [] }

MCP

The verify tool needs no key — a connector is useful read-only before you add one. See MCP connectors.

3 · Seal a decision

SDK

import { Witness } from "vda-witness";
const witness = new Witness({ apiKey: process.env.WITNESS_API_KEY });
await witness.seal(
  { agent: "Refund Agent", inputs: { amountEur: 150 }, verdict: "PASS", reasoning: "<= 200, good standing" },
  { ruleId: "refund.auto", ruleText: "Agents MAY auto-approve refunds up to EUR200." },
);   // fail-open: never throws, never blocks your agent

REST

curl -X POST https://witness.getvda.ai/api/witness/seal \
  -H "authorization: Bearer $WITNESS_API_KEY" -H "content-type: application/json" \
  -d '{
    "decision":  { "agent":"Refund Agent", "inputs":{"amountEur":150}, "verdict":"PASS", "reasoning":"<= 200, good standing" },
    "governingRule": { "ruleId":"refund.auto", "ruleText":"Agents MAY auto-approve refunds up to EUR200." },
    "chainKey": "default",
    "decisionId": "optional-idempotency-key"
  }'
# -> { "record": {...signed body + proof...}, "bodyHash":"sha256:...", "stored":true, "chainKey":"default" }

The account is derived from your credential and stamped (signed) into the record — you cannot send an account field (it is rejected).

2b · Shaped seals — the governance taxonomy

Prefer these over the generic seal. Witness is opinionated about evidentiary completeness — is a record reconstructable by an auditor? — but never about whether a decision was correct. Each shaped skill requires the fields that make its record self-reconstructable; the generic seal above is the compatibility escape (its records carry no record_type and report as unstructured). All three produce a standard vda.witness.record/1 — the structured block rides inside the signed decision.inputs, so it is tamper-evident today with no schema change, readable via get_record and surfaced as recordType in list_records.

⚠️ Witness seals the ASSERTED identity of the actor (and any agent_context) — it does not independently authenticate them. The record proves what was claimed, cryptographically bound and tamper-evident; it does not prove the claim of identity itself.

2b.i · seal_hitl_decision — a human decided

Captures the deciding human, the disposition + rationale, the policies cited as the basis, and content-addressed references (with sha256 hashes) to the system-of-record artifacts the decider saw at decision time — not the action produced. That is what makes the trail auditor-reconstructable.

curl -X POST https://witness.getvda.ai/api/witness/seal/hitl-decision -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "actor": {"id":"jane.ops@stay","type":"human","role":"duty-manager"},
  "decision": {"disposition":"approved","statement":"Waived late-cancel fee; guest showed a flight-cancellation notice."},
  "governing_clauses": [{"ref":"SOP.cancellations#3.2","text":"Duty managers MAY waive late-cancel fees on documented disruption.","hash":"sha256:..."}],
  "evidence": [
    {"ref":"pms://reservation/RES-88421","hash":"sha256:...","media_type":"application/json","captured_at":"2026-07-15T09:41:00Z"},
    {"ref":"upload://flight-cancel-notice.pdf","hash":"sha256:...","inline":{"encoding":"base64","bytes":"..."}}
  ],
  "basis_captured_at": "2026-07-15T09:41:00Z"
}'
# evidence is what the decider SAW (external, hashed). No basis? give "evidence_omitted_reason". inline <=100KB, hash-verified.

2b.ii · seal_agent_action — an agent acted autonomously

Records what the agent consumed, split by provenance: evidence is external material it saw (content-addressed + hashed); parameters is the computed arguments it was passed (self-contained, no hash). One-question test: does it exist outside this record?evidence (hash it); did you compute/pass it as an argument?parameters. At least one is required (or evidence_omitted_reason). Optional agent_context carries execution-substrate hints so an auditor can ask "was this at a known-buggy revision?"

curl -X POST https://witness.getvda.ai/api/witness/seal/agent-action -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "actor": {"id":"c2md-classifier","type":"agent"},
  "action": {"statement":"Classified the agent as high-risk under EU AI Act Annex III.","outcome":"high_risk"},
  "governing_rule": {"ref":"eu-ai-act#annex-III"},
  "evidence": [{"ref":"witness://record/rec_...","hash":"sha256:...","description":"whoami resolution for the credential"}],
  "parameters": {"jurisdictions":["EU"],"data_categories":["biometric"]},
  "agent_context": {"cloud_run_revision":"c2md-api-00042-abc","model_name":"gemini-2.5-flash","region":"europe-west1"}
}'

2b.iii · seal_attestation — assert a fact/state as of a time

For "model X passed eval Y", "card issued", "key rotated", "config was live" — not for decisions. Supporting evidence and a governing_basis framework are optional; without a basis it is sealed as attested-by-the-signing-party, not independently verified.

curl -X POST https://witness.getvda.ai/api/witness/seal/attestation -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "actor": {"id":"c2md-issuer","type":"system"},
  "claim": "Issued agent card did:web:example#key-3 to tenant acct_...",
  "as_of": "2026-07-15T10:00:00Z"
}'

A shaped seal that is missing a required field returns 400 with a message naming the gap — the schema is the guidance. Existing seal callers are unaffected; these are additive.

2c · Admission credentials (issue · verify · revoke)

An admission credential is a sealed attestation that admits an agent to a customer environment — the Witness record IS the credential, not a separate document. The issuing service (e.g. the Onboarding Agent) seals it; enforcers hold only the credential_id and check it by calling Witness. Records are immutable: validity is computed at verify time, and revocation is a new superseding record. Under the hood it stays record_type: attestation with attestation_type: "admission_credential" — no new envelope type.

⚠️ A credential id is a bearer handle. A valid credential is necessary, not sufficient: it proves the credential exists and names subject_did, not that the party presenting it is that subject. Enforcers MUST separately challenge the presenter to prove control of subject_did (a DID challenge-response), exactly as you would not accept a certificate serial number as proof of identity.

Issue (issuer key)

curl -X POST https://witness.getvda.ai/api/witness/credentials/issue -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "subject_did":"did:web:agent.example.com","issuer_did":"did:web:onboard.getvda.ai",
  "environment_id":"env_citizenm_prod","scope":["reservation.read","folio.settle"],
  "governance_files_hash":"sha256:...","sandbox_result":{"pass":true,"score":0.97,"evidence_seal_ref":"rec_..."},
  "impact_delta_ref":"rec_...","expires_at":"2027-07-16T00:00:00Z",
  "compliance_mappings":[{"framework":"eu_ai_act","article_ref":"Article 13","claim":"..."}]
}'
# -> { "credential_id":"rec_...", "record":{...signed...}, "stored":true }

check_valid (PUBLIC — no key)

curl "https://witness.getvda.ai/api/witness/credentials/<credential_id>"
# -> { "valid":true, "code":"valid", "subject":"did:web:agent...", "issuer":"did:web:onboard.getvda.ai",
#      "environment":"env_citizenm_prod", "scope":["reservation.read"], "expires_at":"...", "revoked":false }
# code is one of: valid | revoked | expired | not_found | not_credential   (always HTTP 200 — branch on code)
# revoked -> { "valid":false, "code":"revoked", "subject":"...", "environment":"...", "revoked":true, "revoked_at":"...", "reason_code":"compromised" }

Witness computes validity as a single canonical answer (issued ∧ signature verifies ∧ not revoked by the issuer ∧ not expired), so every enforcer is identically correct and the definition of "valid" evolves in one place — never drifting across independent enforcer implementations. Cache-Control: private, max-age=60 for credential-found states (revocations propagate within 60s); no-store for not_found / not_credential.

📓 Log every check_valid response in your own audit trail (timestamp, credential_id, response). Witness's cache and logs tell you what was returned; only your local record proves what you acted on and when — the answer a regulator needs when they ask "how did you know this credential was valid at time T?"

Revoke (issuing account only)

curl -X POST https://witness.getvda.ai/api/witness/credentials/<credential_id>/revoke -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "reason_code":"compromised","revoked_by":"did:web:onboard.getvda.ai","reason_text":"Out-of-band key rotation."
}'

Only the account that issued a credential may revoke it (enforced structurally, not by a DID string). Revocation is terminal — re-admission is a new credential. Customers who want a credential revoked call the issuing service (which owns revocation policy), not Witness directly.

2d · Customer-managed custody (customer-controlled signing)

By default the shaped skills seal custodially — Witness signs with its platform key. That is tamper-evident and anchored, but the signature is Witness's. For issuer-authenticity provable even against Witness — the property that matters for a credential-issuing authority — seal customer-managed: the record is signed by your own key, and Witness never holds it. You keep shape enforcement and get issuer-authenticity, because Witness stays the sole assembler (no client-side body-builder to drift).

Three steps — prepare → sign → submit:

# 1. PREPARE (stateless — Witness assembles, stores nothing; returns the bytes to sign)
curl -X POST https://witness.getvda.ai/api/witness/prepare -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "skill":"issue_admission_credential",
  "params":{ ...the admission_credential params... },
  "signingPublicKeyJwk":{"kty":"OKP","crv":"Ed25519","x":"<your record-signing PUBLIC key>"}
}'
# -> { "record":{...unsigned...}, "canonicalBytes":"<exact UTF-8 to sign>", "seq":N, "prevHash":"..." }

# 2. SIGN canonicalBytes with your record-signing PRIVATE key: raw Ed25519, signature base64url.

# 3. SUBMIT the signed record to the skill's normal endpoint
curl -X POST https://witness.getvda.ai/api/witness/credentials/issue -H "authorization: Bearer $WITNESS_API_KEY" -d '{
  "record":{ ...the prepared record..., "proof":{"algorithm":"Ed25519","signature":"<base64url>","created":"<record.issuedAt>"} }
}'
# custody = customer-managed. If the chain advanced between prepare and submit -> 409, re-prepare.

Same flow for revoke_admission_credential, seal_agent_action, seal_attestation, seal_hitl_decision (change skill + submit endpoint).

Issuer-authenticity: publish your signing key at your did:web

check_valid returns issuer_verified as a signal distinct from the lifecycle verdict. To get issuer_verified: true, publish your record-signing public key at your issuer's DID document (e.g. did:web:onboard.getvda.ai/.well-known/did.json). Then Witness resolves issuer_did and confirms the record's signing key is published there:

issuer_verificationmeaning
verifiedcustomer-managed + signing key is in the issuer's DID doc — provable even against Witness
key_not_in_did_doc⚠️ claims an issuer but signed by a key not in its DID doc — suspicious, do not ignore
custodialWitness-signed — issuer-authenticity not established (integrity/anchoring still hold)
did_unresolvablethe DID couldn't be resolved — the check did not complete (not verified, not forged)
🔑 Three distinct keys — don't conflate them. Your account controller key authorizes renewal (no-standing-secret rotation); your record-signing key signs records (custody) and its public half is what you publish at your did:web; a did:web card-signing key is separate again. Use a dedicated record-signing key. And once you go customer-managed for real credentials, seal customer-managed for all seals on that chain (intake, gates, credential, revocation) so issuer-authenticity is uniform across the whole admission trail.

Verify a record's issuer — verdict only, no record body (verify_record_issuer)

Publicly checkable issuer-authenticity for any record by id, without exposing it. Answers "is this record's signature by the issuer it claims?" and returns only a verdict, the issuer DID, and the key it resolved against — never decision.inputs, evidence, or body content. This is how an enforcer of a privilege-widening event — e.g. a service sealing its own genesis authority registration or a baseline promotion, customer-managed — confirms the issuer signed it, not merely that Witness recorded it, without holding the account's key.

curl "https://witness.getvda.ai/api/witness/records/<recordId>/issuer"   # PUBLIC — no key
# -> { "record_id":"rec_...", "signature_valid":true, "issuer_verified":true,
#      "issuer_verification":"verified", "issuer_did":"did:web:hitl.getvda.ai",
#      "signer_key":{"kty":"OKP","crv":"Ed25519","x":"..."} }

Same four states as check_valid's issuer_verified: verified / key_not_in_did_doc (loud) / custodial (not applicable — a different custody model, not a failure) / did_unresolvable (incomplete check). The issuer DID comes from the credential's issuer_did or, for a general record, the signer's did:web keyId. Also via MCP: verify_record_issuer.

3c · Revoke an API key — sealed as an event (revoke_api_key)

A revoked key stops authenticating at once, and the revocation is itself sealed as a key_revocation attestation on your chain. Two authorities, distinguished by revoked_by.type (the trust level, not a code path):

Controller-authorized (owner self-service, no operator) — prepare a key_revocation and sign it with your bound controller key, so the record is customer-managed and provable against Witness:

# 1. prepare  ->  2. Ed25519-sign canonicalBytes with your controller key  ->  3. submit:
curl -X POST "https://witness.getvda.ai/api/witness/keys/revoke" -H "authorization: Bearer $WITNESS_API_KEY" \
  -d '{"record":{ ...prepared key_revocation..., "proof":{"algorithm":"Ed25519","signature":"..."} }}'
# -> { "revoked": true, "key_id": "<keyId>", "revoked_by": { "type": "controller" } }

Admin break-glass (operator) — POST https://witness.getvda.ai/api/witness/keys/revoke {"key_id":"..."} with the x-witness-admin header; sealed custodially, revoked_by.type="operator". Revocation is total: a revoked key 401s and its account's records become unfetchable with it — there is no operator backdoor. Reflected in whoami (revoked key 401s; only a sibling's ≤60s cache is a lag).

3b · Read your own records

An operational query over your trail — account-scoped by your key (never an accountId param). Distinct from the Article-12 report, which is an evidence artefact.

List (summaries + your chainKeys, paginated)

curl "https://witness.getvda.ai/api/witness/records?limit=50&chainKey=default" -H "authorization: Bearer $WITNESS_API_KEY"
# -> { "account":"acct_...", "chainKeys":["default","..."],   // discover your own chains
#      "records":[ { "recordId":"...", "chainKey":"default", "seq":0, "verdict":"PASS",
#                    "agent":"Refund Agent", "ruleId":"refund.auto", "bodyHash":"sha256:...",
#                    "sealedAt":"...", "anchorState":"SIGNED_PENDING", "compliance":false } ],
#      "nextCursor":null }
# filters: chainKey, since/until (ISO-8601), limit (1-500), cursor.  ?view=full for full bodies.

Get one record — the FULL signed body, incl. your reasoning

curl "https://witness.getvda.ai/api/witness/records/<recordId>" -H "authorization: Bearer $WITNESS_API_KEY"
# -> { "record": { ...decision{agent,verdict,reasoning,inputs,actionProposed},
#                  governingRule{ruleId,ruleText}, seq, prevHash, proof... },
#      "chainKey":"default", "bodyHash":"sha256:...", "signatureValid":true,
#      "anchorState":"ANCHORED_VALID", "anchor": { "head":"sha256:...", "seq":0 } }

Everything needed to read back your reasoning and verify the record offline. A foreign or unknown recordId returns 404 not-found — indistinguishable from nonexistent (no existence leak). Also via MCP: list_records, get_record.

3d · Suite-service credential resolution (whoami)

For other services in the getvda.ai suite (C2MD first). When a sibling service accepts Authorization: Bearer wtn.<id>.<secret> from its caller, it validates that key by asking Witness — because Witness is the single source of truth for its own keys (no sibling replicates the key store).

curl "https://witness.getvda.ai/api/witness/whoami" -H "authorization: Bearer wtn.xxxx.yyyy"
# -> { "account_id":"acct_...", "tier":"SEALED"|"ANCHORED", "scopes":["seal","read"],
#      "compliance":false, "key_id":"xxxx", "revoked":false }
# 200: Cache-Control: private, max-age=60   ·   401 (generic, no-store) on any bad/unknown/revoked key

tier is resolved from account state (ANCHORED on the anchored tier, else SEALED) — the key identifies the credential; the account carries the tier. Any valid key may call whoami about itself (no elevated scope) and can only ever ask about the key it presents, so no other account is enumerable. There is no privileged service credential — a sibling acts on behalf of whichever end-user key its own caller presented. 401s (unknown / malformed / wrong-secret / revoked) return one generic message and are never cached, so a revocation propagates within the ~1-minute cache window. Permissively rate-limited per calling IP and per account.

3c · Pricing & usage

Three tiers, in the Sealed/Anchored vocabulary:

⚠️ Billing isn't live yet — anchoring is enabled manually today. Seal usage is measured and surfaced on every seal response (a usage block) and via GET /api/witness/usage, but never enforced: over the 5,000/month allowance sealing continues (warns, never blocks — an evidence trail is never silently dropped). Upgrading Sealed → Anchored preserves your account id and all chains (anchoringBeganSeq records where anchoring began); earlier records stay honestly sealed-not-anchored.
curl "https://witness.getvda.ai/api/witness/usage" -H "authorization: Bearer $WITNESS_API_KEY"
# -> { "account":"acct_...", "usage": { "seals_this_period":42, "included":5000, "remaining":4958,
#      "period":"YYYY-MM", "period_end":"...", "tier":"sealed", "over_limit":false,
#      "enforcement":"measured_not_billed" } }
# The same usage block rides on every /api/witness/seal response.

4 · MCP connectors (zero-code)

Endpoint: https://witness.getvda.ai/api/witness/mcp · Tools: get_test_key, renew_challenge, renew_key, seal_hitl_decision, seal_agent_action, seal_attestation, issue_admission_credential, check_valid, revoke_admission_credential, whoami, seal, list_records, get_record, verify, report · Auth: API key as a bearer header (no OAuth). verify, check_valid, get_test_key, and the renew_* tools are keyless.

Claude Desktop — claude_desktop_config.json

{
  "mcpServers": {
    "vda-witness": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://witness.getvda.ai/api/witness/mcp",
               "--header", "Authorization: Bearer ${WITNESS_API_KEY}"],
      "env": { "WITNESS_API_KEY": "wtn.xxxx.yyyy" }
    }
  }
}

Omit the --header/env to run read-only (verify only).

Claude.ai / Claude web — custom connector

Settings → Connectors → Add custom connector → URL https://witness.getvda.ai/api/witness/mcp. When prompted for a header, add Authorization: Bearer <your key>. Leave blank for keyless verify.

Cursor — ~/.cursor/mcp.json

{ "mcpServers": { "vda-witness": {
  "url": "https://witness.getvda.ai/api/witness/mcp",
  "headers": { "Authorization": "Bearer wtn.xxxx.yyyy" }
} } }

VS Code (MCP) — .vscode/mcp.json

{ "servers": { "vda-witness": {
  "type": "http", "url": "https://witness.getvda.ai/api/witness/mcp",
  "headers": { "Authorization": "Bearer wtn.xxxx.yyyy" }
} } }

5 · Schemas, limits & errors

decision

fieldtypenotes
agentstringrequired · which agent decided
inputsany JSONthe decision inputs (canonical ≤ 256KB)
verdictstringrequired
reasoningstringrequired
actionProposedstringoptional

governingRule

fieldtypenotes
ruleIdstringrequired
ruleTextstringrequired · the rule verbatim/ref
governanceRefstringoptional · e.g. AGENTS.md#refunds
governanceHashstringoptional · sha256:…

Options & limits

Grouping related records — use chainKey

There is no bundle_id / group_id / parent_id field, because chainKey already is one. Every seal endpoint takes an optional top-level chainKey; records sharing one are hash-linked in seal order into a set that is retrievable and provable as a set:

GET  https://witness.getvda.ai/api/witness/chains/<chainKey>/proof   # whole trail + anchor + did.json, one call
GET  https://witness.getvda.ai/api/witness/records?chainKey=<key>    # just that group (+ chainKeys[] to discover)
POST https://witness.getvda.ai/api/witness/report {"chainKey":"..."}  # Article 12 evidence scoped to the group

Give the seals of one workflow, compliance bundle, or case a stable shared key, namespaced to you — c2md:bundle:<id>, acp:run:<id>. Omit it and everything lands on default.

⚠️ Do not hand-roll a "manifest" record that lists sibling record IDs in evidence. An evidence hash is the content hash of external material — a record ID is not its content, so such a manifest either carries a hash an auditor cannot reproduce or fabricates one. The chain link is the grouping proof: cryptographic, Witness-verified, and free.

Chains are cheap and independent. Each (account, chainKey) anchors on its own cursor, so a finished group anchors once and then goes quiet rather than re-anchoring forever behind an ever-growing chain — many small chains cost less than one large one. A dedicated chain also has a single writer, avoiding the 409 a customer-managed prepare/submit hits when another writer advances the chain first. Chains are account-scoped: a foreign or unknown chainKey returns 404, never a leak.

⚠️ decisionId is not namespaced by chainKey. Dedupe is (account, decisionId), backed by a UNIQUE index that does not include chain_key — so the same decisionId on a different chain still collides and returns the first record with deduped:true. Never derive a decisionId from the chainKey alone for a record whose content may change; include a content hash or seq discriminator, or omit it.

Error codes

codemeaning
400bad request — e.g. you sent an account field (derived from your credential)
401missing / invalid / revoked API key
409seq conflict on the chain
413payload too large
429rate limited

6 · Article 12 Evidence Report

curl -X POST https://witness.getvda.ai/api/witness/report -H "authorization: Bearer $WITNESS_API_KEY"
# -> { "reportType":"vda.witness.art12-evidence/1", "lifecycle":"DEMO_DATA", "entries":[ ... one per sealed decision ... ] }

Also exposed as the MCP report tool. It is evidence, not a compliance certificate — Compliance Officer attestation is the human act that makes it a regulatory artefact.

SDKs: npm · PyPI (Apache-2.0; the PyPI sdist ships full source). Machine-readable: A2A agent card · OpenAPI · llms.txt.