Event envelope

State changes commit together with an outbox event in the same transaction. The same envelope (EventEnvelopeV1) is used on every hosting profile; each type has its own data schema.

Event envelope
{ "schema_version": 1, "event_id": "evt_01jcqm4x7g2h4j6k8m0n2p4q6r", "type": "grant.revoked", "tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n", "occurred_at": "2026-11-14T16:02:11Z", "data": { "grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "service_id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a", "revision": 4, "reason_code": "contract_ended" } }
Envelope fields
FieldTypeNotes
schema_versionconst 1
event_idstring^evt_[A-Za-z0-9_]+$
typestringOne of the types below.
tenant_idstringOrganization the event belongs to, ^org_[a-z0-9_]+$.
occurred_atdate-timeUTC.
dataobjectPer-type schema.

Event types

Event types
TypeData (required unless noted)Emitted when
grant.activatedgrant_id, service_id, revisionAn invitation is accepted.
grant.suspendedgrant_id, service_id, revision, reason_codeA grant is suspended.
grant.resumedgrant_id, service_id, revisionA grant is resumed.
grant.revokedgrant_id, service_id, revision, reason_codeA grant is withdrawn. The scheduler also emits it, with contract_ended, when a grant reaches its expires_at.
grant.scope_changedgrant_id, service_id, revisionA grant’s scope changes.
certificate.issuedcertificate_id, grant_id, service_id, serial (lowercase hex)A leaf is issued (enrollment or renewal).
certificate.revokedcertificate_id, service_id, serial; optional issuer_idA single leaf is revoked.
issuer.suspendedissuer_id, space_idSectigo trust operations suspends an issuer in an emergency.
policy.publish_requestedservice_id; optional causeA service is created or an issuer change requires republishing.
stripe.event_receivedinbox_idInternal: a verified Stripe webhook was stored.
billing.reconcile_requestedoptional tenant_idInternal: a billing reconciliation is requested.

Reason codes are contract_ended, security_incident and owner_request. Adding a reason code is a compatibility-reviewed change.

Delivery

Events drive the control plane’s own jobs (policy publishing, CRLs, billing). Delivery of events to customer endpoints is not available yet; use operations, receipts and the audit log.

Policy document v1

The immutable, content-addressed policy for one service revision. policy_hash is the lowercase hex SHA-256 of its canonical JSON.

PolicyDocumentV1
{ "schema": "mt.policy.v1", "tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n", "environment": "sandbox", "service_id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a", "epoch": 1, "revision": 4, "trust": [ { "issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b", "fingerprint": "4b8e2d7f1a9c3e6b0d5f8a2c7e1b4d9f6a3c8e0b5d2f7a1c4e9b6d3f8a0c5e27", "chain_pem": [ "-----BEGIN CERTIFICATE-----\n… issuing CA …\n-----END CERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\n… root …\n-----END CERTIFICATE-----\n" ] } ], "rules": [ { "grant_id": "gr_01jcqc8t2v4w6x8y0z2a4b6c8d", "consumer_org_id": "org_01jcqc8s0e2f4g6h8j0k2m4n6p", "workload_id": "wl_01jcqc8t1q3r5s7t9v1w3x5y7z", "principal": "urn:mutualtrust:workload:sandbox:org_01jcqc8s0e2f4g6h8j0k2m4n6p:wl_01jcqc8t1q3r5s7t9v1w3x5y7z", "issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b", "routes": [ { "methods": [ "GET" ], "path": "/orders/*" }, { "methods": [ "POST" ], "path": "/orders" } ], "expires_at": null } ], "deny": { "grants": [ { "grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g", "state": "revoked" } ], "certificates": [ { "issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b", "serial": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9" } ] } }
Policy fields
FieldMeaning
trust[]Active and retiring issuing CAs of the space: ID, SHA-256 fingerprint of the issuing CA DER, and PEM chain (issuing CA first, root last).
rules[]One rule per active grant and trusted issuer: the principal (workload URI), routes and optional expires_at.
deny.grants[]Suspended grants, and revoked or expired grants that still have unexpired leaves, with their state.
deny.certificates[]Revoked serials (lowercase hex) per issuer that have not expired.

Manifest JWS

Compact JWS. Header:

Protected header
{ "alg": "ES256", "kid": "kv_01jcqa7m4n6p8q0r2s4t6v8w0x", "typ": "mt.manifest.v1+jws" }
Claims (ManifestClaims)
{ "typ": "mt.manifest.v1", "iss": "https://api.mutualtrust.io", "tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n", "environment": "sandbox", "service_id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a", "gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "epoch": 1, "revision": 4, "policy_hash": "c7d2e9f4a1b6c3d8e5f0a7b2c9d4e1f6a3b8c5d0e7f2a9b4c1d6e3f8a5b0c7d2", "iat": 1763136140, "exp": 1763136440, "refresh_after": 15, "policy": "{ …PolicyDocumentV1… }" }
  • kid is the space’s manifest-signing key version, pinned by the gateway at pairing (policy_verification_key). It is a different key from the CA keys.
  • exp = iat + 300. refresh_after is the polling interval in seconds.
  • Gateways accept a manifest only if: alg is ES256 and kid is the pinned key; typ matches; the signature verifies; tenant, environment, service and gateway match the pinned target and the embedded policy; it is not expired (with a 30-second allowance) or issued in the future; the policy hashes to policy_hash; and (epoch, revision) is not lower than the applied high-water mark, nor equal with a different hash.

Acknowledgment JWS

Posted by the gateway to POST /v1/gateways/{id}/acks as {"ack":"<compact JWS>"}, signed with the gateway key registered at pairing.

Header
{ "alg": "ES256", "kid": "gw:gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "typ": "mt.ack.v1+jws" }
Payload
{ "typ": "mt.ack.v1", "gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "epoch": 1, "revision": 4, "policy_hash": "c7d2e9f4a1b6c3d8e5f0a7b2c9d4e1f6a3b8c5d0e7f2a9b4c1d6e3f8a5b0c7d2", "status": "applied", "observed_at": "2026-11-14T16:02:21.304Z", "connector_version": "mt-ref-gateway/0.9.0" }

status is applied or rejected (with error_code, up to 60 characters). The control plane checks the signature, the gateway ID, that the revision exists for the service and, when applied, that the hash matches. A gateway’s last-acknowledged revision only moves forward.

Receipt JWS

Batched by the gateway to POST /v1/gateways/{id}/receipts as {"receipts":[…]} (up to 100).

Header
{ "alg": "ES256", "kid": "gw:gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "typ": "mt.receipt.v1+jws" }
Payload
{ "typ": "mt.receipt.v1", "receipt_id": "rc_5d8e2a7c1f4b9e3a6c0d", "request_id": "req_7b2e9d4a1f6c3e8b5a0d", "gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "epoch": 1, "revision": 2, "tls": { "verdict": "verified" }, "authz": { "verdict": "deny", "reason": "route_not_permitted", "grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g" }, "issuer_fingerprint": "4b8e2d7f1a9c3e6b0d5f8a2c7e1b4d9f6a3c8e0b5d2f7a1c4e9b6d3f8a0c5e27", "serial": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9", "method": "GET", "route": "/admin/export", "observed_at": "2026-11-14T15:40:31.118Z" }

tls.verdict is verified, failed (with reason) or absent. authz.reason is one of the deny reasons or allowed. A grant ID that does not belong to the gateway’s service is discarded on receipt.

Workload proofs

Proof inputs
ProofSigned string (UTF-8)Key
Enrollmentmt-enroll-v1:{enrollment_id}:{challenge}The CSR key.
Renewalmt-renew-v1:{certificate_id}:{csr_sha256_hex}:{unix_seconds}The current certificate’s key.
Reloadmt-reload-v1:{certificate_id}:{unix_seconds}The new certificate’s key.

All proofs are ECDSA P-256 with SHA-256, encoded as base64url (no padding) of the raw 64-byte r‖s signature. Timestamps must be within ±300 seconds of the server.

JWS and canonical JSON

  • Signing input is base64url(JSON header) + "." + base64url(canonical JSON payload); the signature is raw r‖s, base64url.
  • Canonical JSON: object keys sorted lexicographically at every level, no insignificant whitespace, undefined members dropped, standard JSON string escaping. It is used for policy hashes, JWS payloads and audit exports.

Certificate profile mt.workload.clientauth.v1

Leaf certificate profile
Version and signatureX.509 v3, ecdsa-with-SHA256, signed by the space’s issuing CA.
KeyECDSA P-256, generated by the workload. The CSR must be signed by it (proof of possession) and is paired with a signed enrollment challenge.
SubjectCN=<workload id>, O=<consumer organization name, ≤64 chars>, informational only; never used for authorization.
Subject alternative nameExactly one URI, urn:mutualtrust:workload:<environment>:<consumer org id>:<workload id> (critical).
Key usagedigitalSignature (critical).
Extended key usageclientAuth only.
Basic constraintsCA:false (critical).
Key identifiersSubject and authority key identifiers.
ValiditynotBefore 30 s before issuance; 24 hours, bounded by the grant expiry and the issuing CA’s validity.
Serial16 random bytes with the top bit cleared (positive), unique per issuer.
Not includedCRL distribution points and authority information access. Revocation is distributed through the signed deny set and the issuer CRL.

The server derives every field from approved policy. CSR extensions are ignored (and listed in ignored_csr_extensions); CA requests and extra SANs cannot be obtained.

CA profiles

CA profiles
Root (mt.ca.root.v1)Issuing CA
Basic constraintsCA:true, pathLen 1 (critical)CA:true, pathLen 0 (critical)
Key usagekeyCertSign, cRLSign (critical)keyCertSign, cRLSign (critical)
SubjectCN=Mutual Trust SANDBOX TEST Root · <space>, O=<org>, OU=<space id>CN=Mutual Trust SANDBOX TEST Issuing CA · <space>, …
Sandbox validity90 days60 days

CRL profile

X.509 v2 CRL signed by the issuing CA with ecdsa-with-SHA256; extensions: authority key identifier and CRL number (incremented on each publication); nextUpdate = thisUpdate + 1 hour; lists revoked, unexpired serials with their revocation dates. Served at https://api.mutualtrust.io/pki/crl/{issuer_id}.crl.

Identifiers

IDs are opaque strings: a type prefix, an underscore, 10 characters of millisecond timestamp and 16 random characters (80 bits), all in lowercase Crockford base32 (0-9a-z without i l o u). They sort by creation time and match ^<prefix>_[a-z0-9_]+$ (at most 64 characters). Do not parse them.

ID prefixes
PrefixResourcePrefixResource
org_Organizationusr_User
spc_Trust spacesvc_Service
iss_Issuer (CA)kv_Key version
inv_Invitationgr_Grant
wl_Workloadwli_Workload issuer
enr_Enrollmentisr_Issuance request
crt_Certificategw_Gateway
pair_Pairing requestpol_Policy revision
op_Operationevt_Event
aud_Audit eventcred_API credential
sg_Support grantexg_Export grant
chk_Checkout operationreq_Request ID

Secrets and tokens

Token formats
PrefixWhatLifetime
mtinv_Invitation secret (shown once, stored hashed).Invitation expiry (default 7 days).
mtenr_Sandbox enrollment token (single use).30 minutes.
mtgw_Gateway credential (collected once at pairing).Until the gateway is disabled.
mtk_API credential; the first 12 characters are kept as a visible prefix.1 to 365 days (MCP: 30 days).
__Host-mt_sessionPortal session cookie.12 hours.

Problem details

application/problem+json
{ "type": "https://mutualtrust.io/problems/revision_mismatch", "title": "The resource changed since you last read it. Reload and retry.", "status": 412, "code": "revision_mismatch", "instance": "/v1/grants/gr_01jcqb3p1n3p5q7r9s1t3v5w7x/revoke", "request_id": "req_8e1f3a9c2b7d4e60" }

Additional members may appear: errors (validation), retry_after (rate limiting), from/to (invalid transitions). The type URI is a stable identifier; match on code. Catalogue: Errors.

Audit export

mt.audit_export.v1
{ "schema": "mt.audit_export.v1", "tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n", "generated_at": "2026-12-01T09:00:00Z", "range": { "from": "2026-11-01T00:00:00Z", "to": "2026-12-01T00:00:00Z" }, "row_count": 1, "rows": [ { "id": "aud_01jcqm4x7h3j5k7m9n1p3q5r7s", "actor_type": "user", "actor_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0", "action": "grant.revoke", "resource_type": "grant", "resource_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "resource_revision": 4, "reason": "contract_ended", "operation_id": "op_01jcqm4x7f9g1h3j5k7m9n1p3q", "request_id": "req_8e1f3a9c2b7d4e60", "environment": "sandbox", "detail": null, "created_at": "2026-11-14T16:02:11Z" } ] }

The body is canonical JSON; X-Content-SHA256 is the hex SHA-256 of the exact bytes. actor_type is user, workload, gateway, system, provider or staff.