MeshQu is currently available for research, evaluation and design-partner deployments. Production availability and support commitments are agreed per engagement. This changelog covers changes to the publicly documented API and SDK contracts. Releases with no change to those contracts have no entry here.
v1 — the /v1 URL prefix, which is stable and only changes on a major release. The current release is 1.31.0.
Versioning policy
- Major (
v2.x): Breaking changes to request/response contracts or authentication. Announced with a migration guide and deprecation period. - Minor (
v1.2): New endpoints, fields, or features. Backwards-compatible. - Patch (
v1.1.1): Bug fixes and performance improvements. No contract changes.
Stability guarantees
- Existing response fields are never removed or renamed within a major version.
- New optional fields may be added to responses at any time — clients should ignore unknown fields.
- Webhook payload schemas follow the same policy.
API version header
All endpoints are prefixed with/v1. When a new major version is released, the previous version will remain available for a documented deprecation period.
v1.31.0
The auditaction list gains a value, connection_permission_outcome. The
three audit-log reads can return it, and GET /v1/audit-log accepts it as a
filter. No operation is added or removed and no response status changes. The
only schema change is the new value in the action list.
A client that treats the list of audit actions as closed, for example by
validating action against the values the reference listed before, will reject
an event that carries the new value.
Audit actions
connection_permission_outcomeis a new auditactionvalue. No operation in this reference records it, so the reference does not say when it appears.- Where it can appear. As
actionon the events returned byGET /v1/audit-log,GET /v1/admin/audit-logandGET /v1/audit-log/entity/{type}/{id}. - As a filter.
GET /v1/audit-logaccepts it as theactionquery parameter. Before, it was refused like any other value that is not a listed audit action: 400FST_ERR_VALIDATION.
v1.30.0
Breaking for API keys: inviting a member and changing a member’s role are now human acts.POST /v1/admin/members/invite and
PATCH /v1/admin/members/{user_id} answer 403 HUMAN_PRINCIPAL_REQUIRED to
any credential that is not a verified human session, which includes every API
key. Before, an API key that held members:write and the admin role could do
both. No operation is added or removed, no request schema changes, and no
other response changes.
Callers that relied on the old behaviour. These requests used to succeed
and are now refused (403):
POST /v1/admin/members/invitemade with an API key. Before, it answered 201.PATCH /v1/admin/members/{user_id}made with an API key. Before, it answered 200.
- Two refusals, in order. An API key without
members:writeis still refused 403FORBIDDENfirst. A credential that holdsmembers:writebut is not a human session is then refused 403HUMAN_PRINCIPAL_REQUIRED, and nothing is written. - A human session is unaffected. Only the admin role holds
members:write, so a verified admin session reaches both operations as before, and anyone else was already refused 403FORBIDDEN. - What did not change.
DELETE /v1/admin/members/{user_id}, which removes a member, andGET /v1/admin/memberscan still be called with an API key. This release makes only inviting and role changes human acts.
v1.29.0
Breaking: a request body with a key the reference does not declare is now refused.POST /v1/decisions/evaluate, POST /v1/decisions/record and
POST /v1/policies/{id}/versions/{version}/ratify answer
400 FST_ERR_VALIDATION to a body that carries an unknown top-level key.
Before, such a key was accepted and ignored. Callers that send extra keys now
get 400. Ratify also adds review-state checks where they apply, and the audit
actions gain a value. No operation is added or removed.
Callers that relied on the old behaviour. These requests used to succeed
and are now refused:
- A body to
POST /v1/decisions/evaluateorPOST /v1/decisions/recordwith a top-level key other than the ones listed under “Decisions” below (400). - A body to
POST /v1/policies/{id}/versions/{version}/ratifywith a key other thanexpected_revision,approval_authorityandratifier_role(400). - A ratify whose reviewed active rules are stale, or whose latest applicable review is adverse (409).
action against the values the reference listed before,
will reject an event that carries the new value.
Decisions: evaluate and record
- Unknown top-level keys are refused.
POST /v1/decisions/evaluateacceptscontext(required),options,actionandsource.POST /v1/decisions/recordacceptscontext(required),options,action,actor,evidence_manifest_digest,evidence_manifestandsource. Any other top-level key is refused 400FST_ERR_VALIDATION, including a key that only the other route takes, such asactoron evaluate. The refused body reaches neither the policy read nor the decision write. Before, an unknown top-level key was accepted and ignored. sourceis accepted and not read. Both bodies now declare a top-levelsource, described as “Accepted and not read.” It accepts any value, and nothing reads it. Before, it was accepted and ignored as an unknown key.- Only the top level is closed. An unknown key inside
contextoroptionsis still accepted on both routes. The reference declares noadditionalPropertiesforcontext,options,actionoractor, andcontext.fields,context.metadataandaction.metadataaccept any key. - The form API contract. In
GET /v1/forms/{formId}/api-contract, the evaluate request schema inopenapi_snippetis now closed (additionalProperties: false) and declaresoptionsandactionbeside the requiredcontext. It does not listsource, which the endpoint accepts. Before, it declaredcontextonly and did not close the schema.
- The body is closed. A body with any key other than
expected_revision,approval_authorityandratifier_roleis refused 400FST_ERR_VALIDATION, and nothing is ratified. Before, an unknown key was accepted and ignored.expected_revisionis still required. A request authenticated by an API key is still refused 403HUMAN_PRINCIPAL_REQUIREDbefore the body is checked, and a key withoutpolicies:writestill gets 403FORBIDDENfirst. - Reviewed rules must still match. When ratification relies on an accepted
review, a change to the active evaluated rules is refused with
409
REVIEWED_SUBJECT_STALE. Nothing is ratified; review the current rules again or restore the accepted rules. This check covers the active evaluation fields, not descriptive metadata or inactive rules. - An adverse review blocks ratification. A latest applicable review of
CORRECT_AND_RESUBMITorSTOPis refused with 409RATIFY_REVIEW_ADVERSEor, where the review gate requires an acceptance, 409RATIFY_NO_ACCEPT_RECORD. Nothing is ratified. - A stale
expected_revisionremains 409STALE_DRAFT_REVISION, and a version that cannot transition remains 409INVALID_TRANSITION. - When an accepted review supports ratification, the
policy_ratifiedaudit entry includes the review record and submission identifiers.
connection_consentedis a new auditactionvalue. No operation in this reference records it, so the reference does not say when it appears.- Where it can appear. As
actionon the events returned byGET /v1/audit-log,GET /v1/admin/audit-logandGET /v1/audit-log/entity/{type}/{id}. - As a filter.
GET /v1/audit-logaccepts it as theactionquery parameter. Before, it was refused like any other value that is not a listed audit action: 400FST_ERR_VALIDATION.
v1.26.0
Clearer refusals across key, licence, discard, ratify and codification writes. Most changes below replace a status a caller already got on the same input, whether a success, a bare 500 or a misleading 404, with a named refusal. One widens who may discard a draft. The set of routes and their request schemas are unchanged. Callers that relied on the old behaviour. Six requests that used to succeed are now refused: revoking a key that is already revoked (409), creating a key that asks for more than the calling API key holds (403), discarding a draft with an API key (403), ratifying a version of an archived policy (409), issuing a licence with a future start while the key’s governing licence is in force (409), and acodification block that repeats a unit_id
(422).
API keys
- Revoking a key that is already revoked is refused.
DELETE /v1/api-keys/{id}andDELETE /v1/admin/api-keys/{id}answer 409API_KEY_ALREADY_REVOKED, withdetails.revoked_atgiving the revocation that stands. Nothing is written: the original revocation time and actor are kept and no second audit entry is appended. Before, the second call answered 200 or 204 and overwrote the revocation time and actor. An unknown key is still 404NOT_FOUND. - Creating a key: an API key cannot ask for more than it holds.
POST /v1/api-keysandPOST /v1/admin/api-keys, when the caller is an API key:- Every requested scope must be one the calling key holds, otherwise
403
API_KEY_SCOPE_ESCALATION, with the scopes it lacks indetails.exceeding_scopes. - The requested
rolemust be one the calling key’s role may create, otherwise 403API_KEY_ROLE_ESCALATION, withdetails.requested_roleanddetails.minter_role.adminmay create any role; every role may create itself andviewer;operatorandauditormay not create each other. - An omitted
scopesorroleis checked as the value it would default to. A request whose default exceeds the calling key is refused, not narrowed. - What this does not cover. A request from a verified human session is not bounded this way. The expiry of the new key is not bounded by the calling key’s. And the check applies to these two routes only: it does not change what any other route lets the holder of a key grant.
- Every requested scope must be one the calling key holds, otherwise
403
POST /v1/agent-licences and
POST /v1/agent-licences/{id}/revoke.
POST /v1/agent-licences: a version that is not in force. When the named version exists, matchespolicy_idandpolicy_version_number, and is not ratified and in force, or its policy is archived, the answer is 409AGENT_LICENCE_POLICY_VERSION_NOT_IN_FORCE. It used to be a 500. Nothing is issued. Apolicy_version_idthat does not exist, or does not match the policy and number named, still answers 500.POST /v1/agent-licences: a future start while a licence is live. Avalid_fromafter now, for a key whose governing licence is in force now, is refused 409AGENT_LICENCE_FUTURE_START_WHILE_LIVE, withdetails.live_licence_id,details.live_valid_untilanddetails.requested_valid_from. It used to be a 201 that would govern at once and leave the key with no valid licence until its start. Nothing is issued. The governing licence is the most recently issued one for the key. A licence that starts now is not refused for this reason, and neither is a future start for a key whose governing licence is not in force.- A committed write whose read-back failed. If the read after an issue or
revoke commits but fails, the response is 500
AGENT_LICENCE_READBACK_FAILEDwithdetails.committed: trueand an identifier for the committed record. Check that record before retrying; repeating an issue could replace the governing licence. A 404 means nothing was written. - UUID letter case. Licence issue and revoke now treat upper-case UUIDs as the same identifiers as their lower-case spellings.
- Discarding a draft is a human act.
DELETE /v1/policies/{id}/versions/{version}now refuses a request authenticated by an API key with 403HUMAN_PRINCIPAL_REQUIRED, before the route looks at the draft. That includes a key that authored the draft, which could discard it before. The scope check runs first: a key withoutpolicies:writestill gets 403FORBIDDEN. A verified human session reaches the route as before.HUMAN_PRINCIPAL_REQUIREDis the refusal ratify, reject and activation already give an API key. - A tenant admin may discard an API-key-created draft. A verified tenant
admin may discard an eligible draft created by one of the tenant’s API keys,
even if that key remains active. The request must include a
reason, or it is refused with 400DISCARD_REASON_REQUIRED. The reason is recorded in the audit trail. GET /v1/policies/{id}no longer offers a key a discard it would be refused. For a request authenticated by an API key,permissions.canDiscardisfalseandactionsdoes not includediscard.- Ratify refuses a version whose policy is archived.
POST /v1/policies/{id}/versions/{version}/ratifyanswers 409INVALID_TRANSITIONwithdetails.reasonPOLICY_ARCHIVED, the code and reason activation already gives for an archived policy. The version stays as it was and nothing is recorded. Before, the ratify succeeded, leaving a ratified version that activation then refuses. - A
codificationblock that declares oneunit_idon two clauses is refused. OnPOST /v1/policies,POST /v1/policies/{id}/versions,POST /v1/policies/{id}/versions/{version}/supersedeand acodification.clauseslist with the sameunit_idon more than one clause answers 422CODIFICATION_REFUSEDwithdetails.codeDUPLICATE_UNIT_ID. Nothing is stored. Before, the later clause silently replaced the earlier in the lookup, so one outcome accounted for both.unit_idvalues are compared exactly, sou-1andU-1are two clauses. A version already stored with a duplicate is not rewritten.
v1.23.0
A server-side bound on policy draft writes.- Affected public route:
PATCH /v1/policies/{id}/versions/{version}. - The response. A write that exceeds the bound is rolled back — nothing is
written — and the request fails with 503
AUTHORING_WRITE_CONTENDED, includingdetails.committed: false, a reason, and aRetry-Afterheader. This response is a definite non-commit and is safe to retry. - Client timeouts remain ambiguous. A client-side timeout alone does not tell the caller whether the write committed; read the draft before retrying.
v1.22.0
Active-rules hash on the policy version read. An additive field: every response that carries a policy version now includes a hash of that version’s current active rules.active_rules_hashis added toGET /v1/policies/{id}/versions/{version}, the versions list, and every other response that embeds a policy version (create, patch, reopen, set-aside, supersede, submit, reject, recall and ratify). It identifies the version’s active evaluated rules.- What it covers. The hash is derived from each active rule’s code, condition, severity and when-clause, in stored order. Editing a rule’s name, description or other non-evaluated metadata does not change it, and adding a new inactive rule does not change it either — only a change to an active rule’s code, condition, severity or when-clause, or a change to which rules are active, moves the hash.
- Always present on 1.22.0 responses. Every policy version has this field from this release on. Servers older than 1.22.0 omit it — clients should treat a missing field as unknown, not as a specific value.
v1.20.0
Audit and policy response compatibility. Audit log responses may contain the newauthoring_capability_carried action. Clients that treat audit action
values as a closed enum should handle unknown values. Policy version responses
may return a key’s display identity in closed_by when that key superseded a
draft.
v1.19.0
Ratification review gate. Where a policy version is subject to the review gate, ratification requires an accepted review of the current draft. A request without one is refused with 409RATIFY_NO_ACCEPT_RECORD. Clients should
use details.reason to distinguish an absent seal, absent acceptance, or
unresolved lineage.
v1.18.0
One pending version per policy- A policy now has its version in force plus at most one pending version: a draft, a version submitted for review, or a ratified version that is not yet in force.
POST /v1/policies/{id}/versionsrefuses a second pending version with409 DRAFT_CONFLICT.details.reasonisOPEN_DRAFT,RATIFIED_AWAITING_ACTIVATIONorPENDING_VERSION_EXISTS, anddetails.pending_versionnames the pending version when it is known.
403 HUMAN_PRINCIPAL_REQUIRED.
POST /v1/policies/{id}/versions/{version}/reopen: a ratified version that is not in force continues as a new draft, which must be ratified again. The ratified version is closed asreopenedand can no longer be activated.POST /v1/policies/{id}/versions/{version}/set-aside: the pending version is closed asset_asidewith a requiredreason, so that a new draft can start from the version in force.POST /v1/policies/{id}/versions/{version}/supersede: a correction to a draft. The draft is closed asreplacedand kept unchanged, including its codification record, and the corrected content becomes a new draft. It takesexpected_revision,rulesand an optionalcodificationblock processed exactly as on version creation.
- Version
statusmay now also bereopened,set_asideorreplaced. A closed version cannot be ratified or activated.supersededkeeps its existing meaning: a version that was in force and has been replaced. - Closed versions carry
closed_at,closed_byandclosed_reason. - New audit actions:
policy_version_reopened,policy_version_set_aside,policy_version_replaced.
v1.16.0
The codification lint’s findings, on the wire- Policy version responses carry an optional
codification_record.lint. Present only on a version whose write ran the model-assisted codification lint; absent — not null — on every other version, including one written before the lint existed and one written with the lint disabled. - Each linted clause carries its
findings(a code, a message, and diagnostic evidence) and aneeds_contextflag.status: "ran"with an emptyfindingsarray for a clause is a real statement — the lint ran and found nothing — distinct from an absentlint, which means the lint never ran at all. - The response contains derived findings and the diagnostic evidence listed in the API schema.
- It is advisory, produced by an automated, model-assisted check. It is unsigned and unhashed — not covered by
policy_rules_hashorpolicy_snapshot_digest, and it enters no receipt, digest, snapshot, export or evidence manifest. - It is not part of review or ratification. A finding is a prompt to whoever is codifying or ratifying the version, never a disposition — it does not change
codification_record.ledgerorcodification_record.counts.
v1.15.0
Policy version activation- New
POST /v1/policies/{id}/versions/{version}/activate— human-only. Puts this exact ratified version in force: the version currently in force (if any) is superseded, this version governs evaluations from now on, and a signed immutablepolicy_version_activationreceipt is returned. - A policy’s first activation starts it in shadow mode. A later activation keeps the policy’s current mode — moving a policy from shadow to enforcing stays its own act,
POST /v1/policies/{id}/activate. - Refusals:
404 NOT_FOUNDwhen the policy or version does not exist in this tenant;409 INVALID_TRANSITIONwithdetails.reasonofPOLICY_ARCHIVED,VERSION_ALREADY_IN_FORCE,VERSION_NOT_RATIFIED, orVERSION_NOT_PENDING;500 RATIFIER_KEY_REQUIREDwhen no ratifier key is configured. Nothing is written on a refusal. - New audit action
policy_version_activated.
POST /v1/policies/{id}/versions/{version}/ratify’s description is corrected: ratification records that a human approved the exact draft version, and returns a signedPolicyApprovalReceiptwhen a ratifier key is configured, but it does not put the version in force. The version currently in force keeps governing evaluations until the ratified version is activated with the new endpoint above.
v1.14.0
The codification record: what a version’s clauses were accounted for- Policy version responses carry an optional
codification_record. It is present only on versions written through the codification path introduced in 1.13.0, and absent — not null — on every other version. - It records one disposition per declared clause, the clause text a reviewer needs in order to judge it, the counts, and any proposal the rule grammar refused.
- The declines are the point. Before this, a clause the codifier read and chose not to express as a rule left no trace once the write completed, so a reviewer could see the rules that were produced but not the judgements behind the ones that were not.
ledger,countsandrefusalsare the write boundary’s own output, not values the caller supplies. Acodification_recordsent in a request body is ignored.
- It is unsigned and unhashed. It is not covered by
policy_rules_hashorpolicy_snapshot_digest, and it enters no receipt, snapshot, export or evidence manifest. - It does not establish that the declared clauses are the complete source document or that each rule expresses its cited clause correctly.
v1.13.0
Codification: rule citations are derived, not assertedPOST /v1/policiesandPOST /v1/policies/{id}/versionsaccept an optionalcodificationblock: the clauses the caller read, a map from rule code to clause, and a disposition (ambiguous,unsupported,non_normative) with a reason for every clause that produced no rule.- When it is supplied, each stored rule’s
source_refis derived from the clause it is mapped to, and anysource_refsent in the request is ignored. A rule that names no clause cannot be stored through this path. - Refusals return
422 CODIFICATION_REFUSEDwith the specific reason indetails.code— a clause with no outcome, a disposition with no reason, or a rule whose condition the evaluator cannot execute. - Omitting the block leaves behaviour exactly as before. Existing callers are unaffected.
- That every clause the caller declared has exactly one accounted-for outcome, and that every stored rule is executable and attributed to a declared clause with a citation the API derived rather than accepted.
- It does not establish that the declared clauses are the whole source document. Nothing server-side reads the document, so a caller that under-reports both its clauses and their outcomes cannot be distinguished from one that does not. That remains a question for human review before ratification.
v1.12.0
Evaluation results say where a rule came fromPOST /v1/decisions/evaluateandPOST /v1/decisions/recordmay now returnrule_sources: a map from rule code to the source reference its author recorded (document,section,url). Present only when at least one rule in the governing snapshot carries one.- It sits beside
result, not inside it.resultis the signed object and itsintegrity_hashcovers the violations exactly as the evaluator produced them, so a caller joinsrule_sourcestoviolations[].rule_codeorna_rules[].rule_codeitself. Existing clients that ignore the field see unchanged bodies. - What it is evidence of, stated precisely:
source_refis author-supplied and is excluded frompolicy_rules_hash, so ratification does not attest to it. It is covered bypolicy_snapshot_digest, which hashes the snapshot rules verbatim — so a v2 receipt binds that this snapshot carried this reference. That is a narrower claim than “the rule faithfully encodes that clause”, which no digest can make.
- The OpenAPI specification now defines the recursive
whengate union. Earlier published versions had an unresolved schema reference that could break validators and generated clients for policy operations.
v1.11.0
Ungoverned evaluations are distinguishable from a governed passPOST /v1/decisions/evaluateandPOST /v1/decisions/recordmay now return an optionalgovernance_coverageobject when no applicable rule was evaluated. It carriespermission_established: false, saying explicitly that anALLOWover zero evaluated rules records the absence of an applicable ratified rule rather than a determination that the action is permitted.- The object is unsigned and is not a receipt field. No hash payload, signing envelope, canonical profile or
receipt_schema_versionchanged, and stripping the object does not affect the signed payload. Existing receipts verify exactly as before. - It is absent on a governed pass, so a client that ignores it sees byte-identical response bodies to v1.10.0. Treating its absence as permission is a client error.
coveragedistinguishesNO_ACTIVE_RULESfromNO_APPLICABLE_RULES.cryptographically_establishedistrueonly forNO_ACTIVE_RULES, and only whenevaluated_rules_hashequals the empty-active-set hash and the result is signed — it means the claim is bound into a signed payload, not that the signature has been verified.- Note
POST /v1/decisions/evaluatecontinues to answer404 NO_ACTIVE_RULESwhen a decision type resolves to no active rules at all; it refuses the evaluation rather than returning an advisory. The advisory’s reachable case is active rules that are all excluded by theirwhengates.
- A rule whose condition the evaluator cannot execute is now rejected with
VALIDATION_ERRORon draft update and ratify, and withUNKNOWN_CONDITION_SHAPE(400) at evaluation time. Previously, an invalid condition could be skipped and reported as a passing rule. - A boolean temporal operator set to
false(not_expired,not_future,must_be_before,must_be_after) andmin_length: 0are likewise refused: the evaluator reads none of them, so they asked nothing. Numeric bounds of0are unaffected and remain executable —{ field, max: 0 }still refuses any positive value. - Rules already stored are unaffected until they are next edited or ratified.
PATCH /v1/policies/{id}/versions/{version}now answers409 NOT_DRAFTfor a submitted or ratified version even when the submitted rules are also malformed. It previously answered422in that case, reporting a rule problem where the version was not editable at all.- The temporal rule condition schema documents
min_days_after,must_be_beforeandmust_be_after. All three were already executed by the evaluator and missing from the published schema. - Policy list endpoints document the
qsearch parameter, and policy version responses documentdraft_revision. Both shipped earlier and had not been regenerated into the spec.
v1.10.0
Case-insensitive policywhen gates
whengates withequalsorinaccept an opt-incase_sensitive: false, which folds string-vs-string comparisons only. Booleans and numbers always compare strictly, and gates never coerce types — a booleantrueis never matched by the string"true".- An absent key means strict comparison, exactly as before: every previously authored rule and every ratified policy version evaluates byte-identically. There is no schema default — absence is the canonical strict spelling.
- Note the default is deliberately the opposite of the list-condition
case_sensitive(which defaults tofalse): gates match ratified domains, list conditions match free text.
- The published spec now also reflects earlier shipped work it had lagged behind:
boolean members in
when.inlists and admin member invitations by email withactor_emailin audit views.
v1.9.0
Tenant Governance Settings- New
GET /v1/settingsandPATCH /v1/settingsendpoints for tenant-level governance configuration. - Includes maker-checker toggle controlling whether policy versions require an independent ratifier before becoming active.
v1.8.0
Human Decision Capture (Forms)- New Forms surface for capturing structured human decisions that produce receipts identical in shape to programmatic decisions.
- Endpoints:
POST /v1/forms,GET /v1/forms,GET /v1/forms/:formId,PATCH /v1/forms/:formId,DELETE /v1/forms/:formId. - Form schemas and API contracts are introspectable:
GET /v1/forms/:formId/schema,GET /v1/forms/:formId/api-contract. - Submissions:
POST /v1/forms/:formId/submitreturns a decision receipt; retrieve later viaGET /v1/forms/:formId/receipts/:decisionId.
v1.7.0
Policy v2 Lifecycle (Submit / Reject / Recall)- Draft policy versions now move through an explicit lifecycle:
POST /v1/policies/:id/versions/:version/submit— submit a draft for ratification.POST /v1/policies/:id/versions/:version/reject— reject a submitted draft.POST /v1/policies/:id/versions/:version/recall— recall a submitted draft back to draft state.
- Additional draft-version endpoints:
GET /v1/policies/:id/versions/:version,PATCH /v1/policies/:id/versions/:version,DELETE /v1/policies/:id/versions/:version. - New
GET /v1/policies/:id/decisions— list all decisions evaluated against a given policy.
v1.6.0
Receipt Schema v2 + Approval Lineage- New top-level fields on decision receipts:
receipt_schema_version— explicit schema version marker.policy_snapshot_digest— canonical SHA-256 of the bound policy snapshot.evidence_manifest_digest— canonical SHA-256 of the evidence manifest, when present.
- Policy Approval Receipts — cryptographic ratifier signatures on the policy version that governed a decision. The offline verifier now accepts ratifier and custodian public-key sets as additional trust roots.
- Two new bundle verifier sub-claims —
evidence(bundled evidence manifest matchesevidence_manifest_digest) andapproval_lineage(per-version approval-receipt digest, ratifier signature andpolicy_rules_hash) — taking the sub-claim set from eight to ten. - All v1 receipts remain verifiable indefinitely — schema v2 fields are additive.
v1.5.0
Public Verification Surface- New JWKS endpoint:
GET /v1/.well-known/signing-keys— versioned public signing keys for offline receipt verification. - New public, unauthenticated endpoints (callers do not need an API key):
GET /v1/receipts/:decisionId— fetch a signed decision receipt.GET /v1/receipts/:decisionId/bundle— download the full Verification Bundle.GET /v1/chains/:chainId/verify— verify a chain proof.GET /v1/chains/:chainId/seal— read a chain seal.GET /v1/chains/:chainId/bundle— download a chain Verification Bundle.
- Verification Bundles — self-contained, offline-verifiable archives under a digest-of-digests manifest, with a sub-claim reported per artefact (receipt, policy snapshot, chain proofs, transparency proof, trust roots, canonicalisation). Two further sub-claims (
evidence,approval_lineage) arrived in v1.6.0, bringing the set to ten.
v1.4.0
Actor AttributionPOST /v1/decisions/recordnow accepts an optional top-levelactorobject:id,type,role,authority,display_name.actor.idis included in the integrity hash and cryptographically bound to every receipt.- Other actor fields are stored for audit display.
- Backward compatible:
actoris optional. If omitted andcontext.metadata.actor_idis present, the API falls back to that value. Whenactor.idis supplied, it takes precedence. - New
actoroptional parameter onclient.record()in the TypeScript SDK. - Decision chain steps can now carry per-step actor attribution, enabling multi-actor chain audit trails.
v1.3.0
Structured Violation Reason Codes- Every violation now includes a
reason_codefield — a structured code for programmatic aggregation without text parsing. - 17 reason codes across all rule types: presence (
FIELD_MISSING,FIELD_FORBIDDEN,FIELD_TOO_SHORT), temporal (DATE_EXPIRED,DATE_INVALID, etc.), threshold (VALUE_ABOVE_MAX,VALUE_BELOW_MIN, etc.), and list (VALUE_NOT_ALLOWED,VALUE_FORBIDDEN). - New
ReasonCodetype exported from@meshqu/typesand re-exported from@meshqu/client. - Backwards compatible:
reason_codeis a new required field on violations. Existingreasonstrings are unchanged. - Replay note: New receipts have different integrity hashes than pre-
reason_codereceipts due to the additional field. Pre-migration receipts remain verifiable against their stored hash.
- New
POST /v1/decisions/:id/outcomeendpoint to capture what happened after a verdict. - Five outcome statuses:
accepted,overridden,escalated,expired,abandoned. - One outcome per decision (409 on duplicate). Immutable after insert.
- Outcomes included on
GET /v1/decisions/:idresponse. - New
recordOutcome()method in the TypeScript SDK.
GET /v1/decisionsnow acceptscorrelation_idquery parameter.- Policy rules accept optional
replaces_rule_codefor advisory lineage tracking.
v1.2.0
Conditional Rule Applicability (When Gating)- Added optional
whenclause on policy rules — rules are only evaluated when the clause matches the decision context. - Non-matching rules return NA (not applicable) with a human-readable reason on the decision receipt.
- New optional fields on
EvaluationResult:rules_na(count) andna_rules(details). - Supported
whenoperators:equals,in,exists, withall/anycombinators for nested logic. - Snapshot hash includes
whenclauses (backward compatible — existing hashes unchanged).
- Decision receipts are now signed with Ed25519 when a signing key is configured.
- Receipts include
signature,signature_kid, andsignature_algorithmfields. - Auditors can verify receipts using the public key alone — no shared secret needed.
- Encryption key versioning with rotation support and legacy fallback.
- Rate limiting upgraded to shared state across API replicas.
v1.1.0
- Added policy groups (CRUD + listing policies within a group).
- Added alert webhook subscriptions with HMAC-signed delivery.
- Added webhook delivery history and statistics endpoints.
- Added decision SSE stream (
GET /v1/decisions/stream). - Added audit log verification (
POST /v1/audit-log/verify). - Added API key naming (
key_name) for audit attribution. - Added advisory mode for policies.
- Added two-tier rate limiting (pre-auth + post-auth).
v1.0.1
Production hardening release. All fixes are backwards-compatible at the HTTP layer.- SDK
apiKeyconfiguration is now required (throws a clear error if missing). idempotency_keyis now required when recording decisions (no longer auto-generated).- Added
dryRun()as a semantic alias forevaluate(). - Recorded decisions now capture audit attribution (
api_key_id,api_key_name). - Backend infrastructure failures now return
503 Service Unavailablewithretry_afterinstead of500. - Production authentication hardening improvements.
v1.0.0
- Initial release.
- Policy CRUD with immutable versioning.
- Decision evaluation (dry-run) and recording (persisted).
- Decision replay for audit verification.
- Alert listing and acknowledgement.
- API key management with scoped access.
- Audit event listing.
- OpenAPI 3.0 spec and Swagger UI.