Suspend, withdraw and revoke

Withdrawal writes authoritative deny state immediately, blocks re-enrollment, publishes a signed deny update and collects acknowledgments from each gateway, so you can see exactly where the change is enforced.

Documentation menu

Choose the control

Access controls
ActionEndpointReversibleEffect
Suspend a grantPOST /v1/grants/{id}/suspendYesDenies with grant_suspended; the capacity slot stays reserved; enrollment and renewal are blocked.
Resume a grantPOST /v1/grants/{id}/resumeNot applicableReturns a suspended grant to active.
Withdraw (revoke) a grantPOST /v1/grants/{id}/revokeNoTerminal. Revokes every live leaf for the grant, consumes outstanding sandbox enrollment tokens, blocks re-enrollment, publishes a CRL.
Revoke one certificatePOST /v1/certificates/{id}/revokeNoAdds the serial to the deny set and the issuer CRL. The grant stays active, so the workload can re-enroll.
Change scopePATCH /v1/grants/{id}/scopeYesNarrows or widens routes; see scope changes.

Only the API owner (sponsor) side can suspend, resume, withdraw or change scope, with the service_owner or trust_admin role. Either side can revoke an individual certificate: the sponsor with grant.manage, the partner with workload.manage (partner_admin).

Withdraw a grant

Send the current revision in If-Match (from the grant’s revision or its ETag) and a reason code: contract_ended, security_incident or owner_request. An optional note (up to 500 characters) may accompany it.

Withdraw a grant
curl -sS -X POST https://api.mutualtrust.io/v1/grants/gr_01jcqb3p1n3p5q7r9s1t3v5w7x/revoke \ -H 'Authorization: Bearer mtk_…' \ -H 'If-Match: "3"' \ -H 'Content-Type: application/json' \ -d '{"reason_code":"contract_ended"}'
200 OK (ETag: "4")
{ "grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "state": "revoked", "revision": 4, "operation_id": "op_01jcqm4x7f9g1h3j5k7m9n1p3q", "effective_control_plane_time": "2026-11-14T16:02:11Z", "intended_policy_revision": { "epoch": 1, "revision": 4 }, "pending_gateway_count": 1 }
  • effective_control_plane_time is when the deny became authoritative.
  • intended_policy_revision is the next revision the publisher is expected to produce; the operation records the actual target once published.
  • pending_gateway_count is the number of paired gateways that must acknowledge it.
  • Without If-Match the API returns 428 precondition_required; with a stale revision 412 revision_mismatch. Re-read the grant and retry.
  • Moving a terminal grant again fails with 409 invalid_transition.

Follow the operation

Poll the operation
GET https://api.mutualtrust.io/v1/operations/op_01jcqm4x7f9g1h3j5k7m9n1p3q
200 OK
{ "id": "op_01jcqm4x7f9g1h3j5k7m9n1p3q", "kind": "grant.revoke", "resource_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "effective_control_plane_time": "2026-11-14T16:02:11Z", "target_policy_revision": { "epoch": 1, "revision": 4 }, "state": "complete", "gateways": [ { "gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v", "name": "orders-edge-1", "status": "applied", "delivered_at": "2026-11-14T16:02:20Z", "applied_at": "2026-11-14T16:02:21Z", "apply_latency_seconds": 10 } ], "note": "A last applied revision is evidence about that gateway only, never evidence of enforcement everywhere." }
Operation and gateway states
FieldValues
statepublishing (no revision yet) → awaiting_acknowledgments → complete (every paired gateway applied).
gateways[].statuspending, delivered (manifest fetched at or above target), applied (signed ACK received), offline_pending (no check-in for 5 minutes).
kindgrant.revoke, grant.suspend, grant.resume, grant.scope_changed, certificate.revoke.

Revoke a single certificate

POST /v1/certificates/{id}/revoke
POST https://api.mutualtrust.io/v1/certificates/crt_01jcqb5r0d2e4f6g8h0j2k4m6n/revoke Content-Type: application/json { "reason_code": "key_compromise" }
200 OK
{ "certificate_id": "crt_01jcqb5r0d2e4f6g8h0j2k4m6n", "state": "revoked", "operation_id": "op_01jcqm6z2b4c6d8e0f2g4h6j8k", "effective_control_plane_time": "2026-11-14T16:10:42Z" }

Reason codes: key_compromise, superseded, cessation_of_operation, owner_request. Revoking an already revoked certificate returns 409 already_revoked. A revoked certificate can never be renewed; the workload re-enrolls with a new attestation while its grant is active.

What gateways enforce: the deny set

Each policy revision embeds a deny set alongside the allow rules:

policy.deny
{ "grants": [ { "grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x", "principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g", "state": "revoked" } ], "certificates": [ { "issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b", "serial": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9" } ] }
  • deny.grants lists suspended grants, and revoked or expired grants that still have unexpired leaves.
  • deny.certificates lists revoked serials per issuing CA that have not yet expired. Entries drop out once the leaf would have expired anyway, keeping manifests small.
  • The deny check runs on every request, including requests on resumed TLS sessions and keep-alive connections: a revoked serial is denied with certificate_revoked even if the connection was established earlier.

CRLs

Each issuing CA publishes a DER-encoded X.509 v2 CRL (with CRL number and authority key identifier) after a grant withdrawal or certificate revocation:

Fetch a CRL
curl -sS -o issuing.crl https://api.mutualtrust.io/pki/crl/iss_01jcqa7m4s8t0v2w4x6y8z0a2b.crl openssl crl -inform DER -in issuing.crl -noout -text
  • Content type application/pkix-crl, Cache-Control: public, max-age=300. The issuer ID is shown on the space (GET /v1/spaces/{id}) and in the policy trust list.
  • Returns 404 until the issuer has published its first CRL.
  • nextUpdate is one hour after thisUpdate. The CRL lists revoked serials that have not expired.
  • Leaves do not carry a CRL distribution point extension. The signed manifest deny set is the primary enforcement path; CRLs serve gateways and audits that need a standard artifact.

Timing and acknowledgments

  • Gateways poll for a new manifest every 15 seconds (plus up to 2 seconds of jitter), apply it atomically and send a signed acknowledgment.
  • The target is p95 ≤ 60 seconds from withdrawal to applied at an online gateway. Online gateways refresh policy every 15 seconds, so acknowledgments typically arrive within seconds.
  • When a revision changes, the gateway also closes open connections whose principal no longer has any allow rule.

Offline and stale gateways

A gateway that cannot reach the control plane keeps enforcing its last valid manifest until that manifest’s exp (300 seconds after issue) plus a 30-second clock allowance. After that every protected route is denied with manifest_expired; a gateway that never received a manifest denies with uninitialized. There is no fail-open mode.

  • Worst case for an offline gateway to stop honouring a withdrawn grant: 5 minutes 30 seconds after its last successful refresh.
  • Open connections are closed when policy goes stale, and every connection has a maximum age of 5 minutes, so long-lived streams cannot outlive the policy that admitted them.
  • The operation and service views list such a gateway as offline_pending. The overview raises an alert for gateways silent for 5 minutes.
  • Gateways persist their high-water mark (epoch, revision, hash), so a restart can never move them to an older policy.

Expiry is also enforced

Grants with an expires_at are moved to expired by a per-minute scheduler and published like a withdrawal. Rules also carry the grant’s expires_at, so gateways deny with grant_expired on time even before the next revision arrives.