Five-minute sandbox quickstart

Play both sides of a partner relationship in the sandbox: Northstar owns an Orders API, Harbor calls it. You finish with one verified allow, one verified deny, and a withdrawal that the gateway acknowledges.

Documentation menu

Prerequisites

A Mutual Trust account (sign in with a passkey, enterprise SSO or the labelled sandbox guest) and a macOS, Linux or Windows machine. The mt CLI is one native binary with the enforcer built in: no runtime or container is needed.

1. Create a space

Open app.mutualtrust.io, choose Start sandbox, then Try the Orders API. No card is needed. The portal calls POST /v1/sandbox/try-orders-api, which creates two clearly labelled sample organizations (Northstar (sample) as the API owner and Harbor (sample) as the partner), plus a sandbox trust space with its own private root and issuing CA, and an Orders API service with three route templates:

Orders API route templates
TemplateMethodsPath
Read ordersGET/orders/*
Submit ordersPOST/orders
Admin export (restricted)GET/admin/*

The portal shows the sandbox trust fingerprint (SHA-256 of the space’s root certificate). Sandbox roots carry SANDBOX TEST in their subject so they can never be mistaken for production.

2. Start the sample API and gateway

Install the mt CLI. The install script verifies the release signature and checksum before it installs anything (packages and other options):

Install on macOS or Linux
curl -fsSL https://mutualtrust.io/install.sh | sh
Install on Windows (PowerShell)
irm https://mutualtrust.io/install.ps1 | iex

Start the sample Orders API with the enforcer in front of it. mt gateway runs the built-in enforcer in proxy mode on https://localhost:8443:

Start the gateway
mt gateway --api https://api.mutualtrust.io

The gateway generates its own P-256 key, registers it and prints a pairing code:

Gateway output
Mutual Trust quickstart control plane https://api.mutualtrust.io sample Orders API on http://127.0.0.1:9001 (rejects calls that bypass the gateway) enforcer embedded (mt enforcer proxy) Pairing code: K7QM-4TXZ Approve it in the portal (Services > your service > Pair gateway), or open: https://app.mutualtrust.io/#/pair?code=K7QM-4TXZ gateway key thumbprint 3nUq0b2s9kTq7m1x…

In the portal, as Northstar, open Orders API → Pair gateway, enter the code and a name. The code is valid for 15 minutes. On approval the gateway collects its scoped credential once, pins the space’s policy-verification key, fetches its first signed policy manifest and acknowledges the revision it applied. The service readiness checklist now shows Gateway paired and Gateway acknowledged the current policy revision.

3. Authorize the caller

  1. As Northstar, open Orders API → Invite partner. Choose Harbor (sample) and the routes GET /orders/* and POST /orders. Leave Admin export out: that is the route you will be denied on.
  2. Switch to Harbor (sample) and accept the scoped invitation. Give the workload a name; in the sandbox it binds a sandbox identity. Accepting creates an active grant.
  3. Still as Harbor, choose Approve enrollment. The portal issues a single-use sandbox enrollment token (mtenr_…, valid 30 minutes) and shows a one-time command.
Enroll the workload
mt enroll --token mtenr_… --api https://api.mutualtrust.io
Enrollment output
Enrolling workload via https://api.mutualtrust.io 1. attestation accepted: challenge for enr_01jcqb5q2h4j6k8m0n2p4q6r8s 2. generated P-256 key locally (private key never leaves this machine) 3. certificate issued serial 5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9 principal urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g expires 2026-11-15T15:31:30Z renew after 2026-11-15T07:27:12Z files ~/.config/mutualtrust/workload/{cert,chain,key,root}.pem

The CLI generated the key locally, sent a CSR plus a signature over the server’s challenge, and wrote the certificate to the workload directory of its home (~/.config/mutualtrust on Linux, ~/Library/Application Support/MutualTrust on macOS, %ProgramData%\MutualTrust on Windows). The portal now shows the workload, its issuer, routes, owner and expiry. Details: Enrollment.

4. Send two requests

Allowed and denied calls
mt call https://localhost:8443/orders # 200 ALLOWED mt call https://localhost:8443/admin/export # 403 DENIED (route_not_permitted)
Call output
200 ALLOWED GET https://localhost:8443/orders request req_3f9a1c7e5b2d8a4c6e0f · policy revision 1.2 {"orders":[{"id":"ord_1001","status":"staged","lines":3}],"caller":"urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g"} 403 DENIED GET https://localhost:8443/admin/export request req_7b2e9d4a1f6c3e8b5a0d · policy revision 1.2 {"type":"https://mutualtrust.io/problems/route_not_permitted","title":"This workload is not permitted to call this route or method.","status":403,"code":"route_not_permitted","request_id":"req_7b2e9d4a1f6c3e8b5a0d","tls":"verified","policy_revision":2}

The gateway answers with x-mt-request-id and x-mt-policy-revision (epoch.revision) headers. Both requests produce signed receipts that appear in the portal with the gateway, grant, time and policy revision. TLS verification and authorization are separate verdicts on each receipt: the denied call above has tls: verified and authz: deny, which is what makes it a valid denied control for the service’s readiness checklist.

The origin refuses bypass

The sample Orders API on port 9001 answers 401 origin_requires_gateway to any request without the gateway-supplied x-mt-principal header. The gateway strips any x-mt-* headers the client sends before forwarding.

5. Change access

As Northstar, open the Harbor grant and choose Withdraw with a reason (for example contract_ended). The portal shows the effective control-plane time, the intended policy revision and how many gateways must acknowledge it. Repeat the first request:

Repeat after withdrawal
mt call https://localhost:8443/orders 403 DENIED GET https://localhost:8443/orders request req_9c4e1a7f3b5d2e8a6c0b · policy revision 1.3 {"type":"https://mutualtrust.io/problems/certificate_revoked","title":"The client certificate was revoked.","status":403,"code":"certificate_revoked",…}

Withdrawal revokes every live leaf for the grant, so the gateway reports certificate_revoked (the deny-set check runs before the grant check). The operation view lists each gateway as pending, delivered, applied or offline (pending); a disconnected gateway is never shown as applied. An online gateway refreshes policy every 15 seconds, so the acknowledgment typically arrives within seconds. Details: Withdraw access.

Keep it running: renewal

Renew automatically
mt renew --watch # in the foreground mt service install renew # or as a service: systemd, launchd or a Windows service

Leaves last 24 hours. The CLI renews at about two-thirds of the validity period (the server adds ±5% jitter), proves key continuity with the current key, and confirms the reload with the new key before the old credential is retired locally. Add --on-renew "systemctl reload nginx" (after -- for mt service install) when a server reads the files. See Renewal.

Troubleshooting

Common quickstart denials
Denial codeMeaning and fix
no_client_certificateNo usable client certificate was presented, or its chain did not verify against this service’s trust bundle (the response then shows "tls":"failed"). Use mt call, or pass ~/.mt/workload/chain.pem and key.pem to your client.
unknown_issuerThe chain verified but its issuing CA is not in this service’s signed trust bundle. Re-enroll against the right service.
route_not_permittedYour grant does not include this route and method. Ask the API owner to change the scope.
grant_revoked / certificate_revokedAccess was withdrawn. Contact the API owner shown in the portal.
manifest_expired / uninitializedThe gateway has no fresh signed policy and fails closed. Check that the gateway can reach api.mutualtrust.io.

Every error includes a request ID; quote it if you contact support. The full list is in Troubleshooting.

Next steps