Wire formats
Every format here is versioned. Consumers must ignore unknown output and event fields; command bodies are closed and reject unknown fields.
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.
{
"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"
}
}| Field | Type | Notes |
|---|---|---|
schema_version | const 1 | |
event_id | string | ^evt_[A-Za-z0-9_]+$ |
type | string | One of the types below. |
tenant_id | string | Organization the event belongs to, ^org_[a-z0-9_]+$. |
occurred_at | date-time | UTC. |
data | object | Per-type schema. |
Event types
| Type | Data (required unless noted) | Emitted when |
|---|---|---|
grant.activated | grant_id, service_id, revision | An invitation is accepted. |
grant.suspended | grant_id, service_id, revision, reason_code | A grant is suspended. |
grant.resumed | grant_id, service_id, revision | A grant is resumed. |
grant.revoked | grant_id, service_id, revision, reason_code | A grant is withdrawn. The scheduler also emits it, with contract_ended, when a grant reaches its expires_at. |
grant.scope_changed | grant_id, service_id, revision | A grant’s scope changes. |
certificate.issued | certificate_id, grant_id, service_id, serial (lowercase hex) | A leaf is issued (enrollment or renewal). |
certificate.revoked | certificate_id, service_id, serial; optional issuer_id | A single leaf is revoked. |
issuer.suspended | issuer_id, space_id | Sectigo trust operations suspends an issuer in an emergency. |
policy.publish_requested | service_id; optional cause | A service is created or an issuer change requires republishing. |
stripe.event_received | inbox_id | Internal: a verified Stripe webhook was stored. |
billing.reconcile_requested | optional tenant_id | Internal: 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
Policy document v1
The immutable, content-addressed policy for one service revision. policy_hash is the lowercase hex SHA-256 of its canonical JSON.
{
"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"
}
]
}
}| Field | Meaning |
|---|---|
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:
{
"alg": "ES256",
"kid": "kv_01jcqa7m4n6p8q0r2s4t6v8w0x",
"typ": "mt.manifest.v1+jws"
}{
"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… }"
}kidis 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_afteris the polling interval in seconds.- Gateways accept a manifest only if:
algis ES256 andkidis the pinned key;typmatches; 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 topolicy_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.
{
"alg": "ES256",
"kid": "gw:gw_01jcqa9b6k8m0n2p4q6r8s0t2v",
"typ": "mt.ack.v1+jws"
}{
"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).
{
"alg": "ES256",
"kid": "gw:gw_01jcqa9b6k8m0n2p4q6r8s0t2v",
"typ": "mt.receipt.v1+jws"
}{
"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 | Signed string (UTF-8) | Key |
|---|---|---|
| Enrollment | mt-enroll-v1:{enrollment_id}:{challenge} | The CSR key. |
| Renewal | mt-renew-v1:{certificate_id}:{csr_sha256_hex}:{unix_seconds} | The current certificate’s key. |
| Reload | mt-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 rawr‖s, base64url. - Canonical JSON: object keys sorted lexicographically at every level, no insignificant whitespace,
undefinedmembers dropped, standard JSON string escaping. It is used for policy hashes, JWS payloads and audit exports.
Certificate profile mt.workload.clientauth.v1
| Version and signature | X.509 v3, ecdsa-with-SHA256, signed by the space’s issuing CA. |
|---|---|
| Key | ECDSA P-256, generated by the workload. The CSR must be signed by it (proof of possession) and is paired with a signed enrollment challenge. |
| Subject | CN=<workload id>, O=<consumer organization name, ≤64 chars>, informational only; never used for authorization. |
| Subject alternative name | Exactly one URI, urn:mutualtrust:workload:<environment>:<consumer org id>:<workload id> (critical). |
| Key usage | digitalSignature (critical). |
| Extended key usage | clientAuth only. |
| Basic constraints | CA:false (critical). |
| Key identifiers | Subject and authority key identifiers. |
| Validity | notBefore 30 s before issuance; 24 hours, bounded by the grant expiry and the issuing CA’s validity. |
| Serial | 16 random bytes with the top bit cleared (positive), unique per issuer. |
| Not included | CRL 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
| Root (mt.ca.root.v1) | Issuing CA | |
|---|---|---|
| Basic constraints | CA:true, pathLen 1 (critical) | CA:true, pathLen 0 (critical) |
| Key usage | keyCertSign, cRLSign (critical) | keyCertSign, cRLSign (critical) |
| Subject | CN=Mutual Trust SANDBOX TEST Root · <space>, O=<org>, OU=<space id> | CN=Mutual Trust SANDBOX TEST Issuing CA · <space>, … |
| Sandbox validity | 90 days | 60 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.
| Prefix | Resource | Prefix | Resource |
|---|---|---|---|
org_ | Organization | usr_ | User |
spc_ | Trust space | svc_ | Service |
iss_ | Issuer (CA) | kv_ | Key version |
inv_ | Invitation | gr_ | Grant |
wl_ | Workload | wli_ | Workload issuer |
enr_ | Enrollment | isr_ | Issuance request |
crt_ | Certificate | gw_ | Gateway |
pair_ | Pairing request | pol_ | Policy revision |
op_ | Operation | evt_ | Event |
aud_ | Audit event | cred_ | API credential |
sg_ | Support grant | exg_ | Export grant |
chk_ | Checkout operation | req_ | Request ID |
Secrets and tokens
| Prefix | What | Lifetime |
|---|---|---|
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_session | Portal session cookie. | 12 hours. |
Problem details
{
"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
{
"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.