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.
Choose the control
| Action | Endpoint | Reversible | Effect |
|---|---|---|---|
| Suspend a grant | POST /v1/grants/{id}/suspend | Yes | Denies with grant_suspended; the capacity slot stays reserved; enrollment and renewal are blocked. |
| Resume a grant | POST /v1/grants/{id}/resume | Not applicable | Returns a suspended grant to active. |
| Withdraw (revoke) a grant | POST /v1/grants/{id}/revoke | No | Terminal. Revokes every live leaf for the grant, consumes outstanding sandbox enrollment tokens, blocks re-enrollment, publishes a CRL. |
| Revoke one certificate | POST /v1/certificates/{id}/revoke | No | Adds the serial to the deny set and the issuer CRL. The grant stays active, so the workload can re-enroll. |
| Change scope | PATCH /v1/grants/{id}/scope | Yes | Narrows 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.
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"}'{
"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_timeis when the deny became authoritative.intended_policy_revisionis the next revision the publisher is expected to produce; the operation records the actual target once published.pending_gateway_countis the number of paired gateways that must acknowledge it.- Without
If-Matchthe API returns428 precondition_required; with a stale revision412 revision_mismatch. Re-read the grant and retry. - Moving a terminal grant again fails with
409 invalid_transition.
Follow the operation
GET https://api.mutualtrust.io/v1/operations/op_01jcqm4x7f9g1h3j5k7m9n1p3q{
"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."
}| Field | Values |
|---|---|
state | publishing (no revision yet) → awaiting_acknowledgments → complete (every paired gateway applied). |
gateways[].status | pending, delivered (manifest fetched at or above target), applied (signed ACK received), offline_pending (no check-in for 5 minutes). |
kind | grant.revoke, grant.suspend, grant.resume, grant.scope_changed, certificate.revoke. |
Revoke a single certificate
POST https://api.mutualtrust.io/v1/certificates/crt_01jcqb5r0d2e4f6g8h0j2k4m6n/revoke
Content-Type: application/json
{ "reason_code": "key_compromise" }{
"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:
{
"grants": [
{
"grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"state": "revoked"
}
],
"certificates": [
{
"issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b",
"serial": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9"
}
]
}deny.grantslists suspended grants, and revoked or expired grants that still have unexpired leaves.deny.certificateslists 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_revokedeven 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:
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 policytrustlist. - Returns
404until the issuer has published its first CRL. nextUpdateis one hour afterthisUpdate. 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