API reference
Base URL https://api.mutualtrust.io. The machine-readable contract is OpenAPI 3.1 with JSON Schema 2020-12 components; the server validates every command body with those same schemas.
Hosts
| Host | Accepts | Use for |
|---|---|---|
https://api.mutualtrust.io | Bearer credentials (mtk_, mtgw_), workload proofs, Stripe signatures. Session cookies are ignored. | Automation, gateways, workloads, CRLs, webhooks. |
https://app.mutualtrust.io | Everything above, plus the portal session cookie and /auth/* sign-in routes. | The portal and browser-based flows. |
Both hosts serve the same /v1 routes. Public endpoints: GET /healthz, GET /openapi.json and GET /pki/crl/{issuer_id}.crl.
Authentication
Session and CSRF (portal)
Humans sign in at https://app.mutualtrust.io/auth/login (OIDC authorization code with PKCE; see Identity and access). The session is the host-only cookie __Host-mt_session (HttpOnly; Secure; SameSite=Lax, 12 hours), honoured only on app.mutualtrust.io. Every non-GET request on a cookie session must echo the session’s CSRF token, which GET /v1/me returns as csrf_token:
POST https://app.mutualtrust.io/v1/services
Cookie: __Host-mt_session=…
X-CSRF-Token: 3vQd0r1W…
Content-Type: application/jsonA missing or wrong token returns 403 forbidden (“Missing or invalid CSRF token.”).
API credentials: Bearer mtk_
Scoped API credentials authenticate automation as the user who created them, limited to one organization and a subset of that user’s roles. Effective roles are the credential’s roles intersected with the creator’s current roles. No CSRF token is needed.
GET https://api.mutualtrust.io/v1/grants?service_id=svc_01jcqa7m5r7s9t1v3w5x7y9z1a
Authorization: Bearer mtk_Jx4c…Create, list and revoke credentials under Organizations. Revoked or expired credentials return 401 unauthenticated.
Gateway credential: Bearer mtgw_
A gateway receives a scoped credential once, when its pairing is approved. It authenticates the gateway’s own manifest, acknowledgment and receipt endpoints only. Acknowledgments and receipts are additionally JWS-signed with the gateway’s P-256 key registered at pairing (kid: "gw:<gateway_id>"). A disabled gateway’s credential stops working.
Workload proofs
Enrollment, polling, renewal and reload confirmation carry no bearer token. They are authorized by what only the workload holds:
| Endpoint | Authorized by |
|---|---|
POST /v1/enrollments | A single-use sandbox token or an OIDC workload JWT. |
POST /v1/enrollments/{id}/complete | A signature over mt-enroll-v1:{enrollment_id}:{challenge} with the CSR key. |
GET /v1/issuance-requests/{id} | X-Enrollment-Challenge header equal to the enrollment challenge. |
POST /v1/certificates/{id}/renew | A signature over mt-renew-v1:{certificate_id}:{csr_sha256_hex}:{unix_seconds} with the current key. |
POST /v1/certificates/{id}/reloaded | A signature over mt-reload-v1:{certificate_id}:{unix_seconds} with the new key. |
Pairing (POST /v1/pairings) is unauthenticated and rate-limited; collecting the result needs the X-Poll-Secret returned at creation.
Conventions
| Content type | Send Content-Type: application/json with any body; other types return 415 unsupported_media_type. An empty body is treated as {}. |
|---|---|
| Errors | RFC 9457 application/problem+json with a stable code, a safe title, status, instance and request_id. See the error catalogue. |
| Closed schemas | Unknown fields or invalid enums return 422 validation_failed with errors: [{path, message}] (JSON pointers, up to 10). |
| Idempotency | Idempotency-Key is required on invitation create and accept and on checkout, and honoured on organization, space and service creation, Try the Orders API, and grant revoke, suspend and resume. The key is scoped to the caller, method and path and kept for 24 hours. Same key and body → the original status and body with Idempotent-Replayed: true; same key with a different body → 409 idempotency_key_reused; missing where required → 428 idempotency_key_required. |
| Concurrency | Responses that carry a numeric revision also send ETag: "<revision>". Grant changes need If-Match: "<revision>": missing → 428 precondition_required; stale → 412 revision_mismatch. |
| Visibility | Resources in organizations you are not a member of return 404, never 403. |
| Throttling | 429 rate_limited with Retry-After. Pairing is limited to 30 per hour and guest sign-in to 20 per hour per client IP. Transient dependency failures return 503 with Retry-After: 5. |
| IDs and times | Opaque, prefixed, sortable IDs such as gr_01jcqb3p… (see ID format). Times are UTC RFC 3339 without fractional seconds. Money is integer minor units with a lowercase ISO 4217 currency. |
| Lists | List endpoints return {"data":[…]}, newest first, capped (100 to 200 items). The audit log pages with before / next_cursor. |
| Headers | Every response carries X-Request-Id and security headers (HSTS, nosniff, X-Frame-Options: DENY). |
Session and overview
GET /v1/me
The signed-in user, CSRF token and memberships.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Success | 200 |
{
"user_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0",
"display_name": "Avery Chen",
"auth_method": "oidc",
"csrf_token": "3vQd0r1W…",
"memberships": [
{
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"role": "owner"
},
{
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"role": "service_owner"
}
],
"environment_notice": null
}GET /v1/me/recovery-codes
Recovery code status for a passkey account. See Recovery codes.
| Auth | Session cookie on app.mutualtrust.io |
|---|---|
| Success | 200 {"available":true,"remaining":9,"total":10,"generated_at":"…","last_used_at":"…"}; available: false for guest and single sign-on accounts |
POST /v1/me/recovery-codes
Generates ten new one-time recovery codes and invalidates every earlier code. The codes are returned once and stored only as hashes.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes); not API credentials |
|---|---|
| Success | 201 {"codes":["mtrc_…", …],"generated_at":"…"} |
| Errors | forbidden (sandbox guest, single sign-on account or recovery session) |
GET /v1/overview
Everything the user can see across organizations: spaces, services with gateway health, relationships with certificate expiry, and alerts (stale gateways, expiring certificates, and payment notices for owners and billing admins).
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Success | 200 {user, organizations[], spaces[], services[], relationships[], alerts[]} |
{
"user": {
"id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0",
"display_name": "Avery Chen",
"auth_method": "oidc"
},
"organizations": [
{
"id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Northstar",
"kind": "customer",
"domain_verification": "unverified",
"roles": [
"owner",
"trust_admin",
"service_owner",
"partner_admin"
]
}
],
"spaces": [
{
"id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Orders sandbox",
"environment": "sandbox",
"trust_fingerprint": "9f2c41d8e3b7a6051c4e8d2f7a9b3c6e1d5f0a8b2c7e4d9f3a6b1c8e5d2f7a40"
}
],
"services": [
{
"id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"space_id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"name": "Orders API",
"environment": "sandbox",
"active_grants": 1,
"gateways": 1,
"stale_gateways": 0,
"last_allow": "2026-11-14T15:40:12Z"
}
],
"relationships": [
{
"id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"state": "active",
"consumer_org_id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"sponsor_org_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"service_name": "Orders API",
"sponsor_name": "Northstar",
"consumer_name": "Harbor Logistics",
"workload_name": "harbor-order-sync",
"cert_expires_at": "2026-11-15T15:30:30Z",
"renew_after": "2026-11-15T07:26:41Z"
}
],
"alerts": []
}POST /v1/sandbox/try-orders-api
Creates the sample Northstar and Harbor organizations, a sandbox space and the Orders API service with three route templates.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Headers | Idempotency-Key (optional) |
| Success | 201 |
{
"sponsor": {
"id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Northstar (sample)",
"kind": "sample"
},
"partner": {
"id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"name": "Harbor (sample)",
"kind": "sample"
},
"space": {
"id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"org_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Orders sandbox",
"environment": "sandbox",
"trust_fingerprint": "9f2c41d8e3b7a6051c4e8d2f7a9b3c6e1d5f0a8b2c7e4d9f3a6b1c8e5d2f7a40",
"issuing_fingerprint": "4b8e2d7f1a9c3e6b0d5f8a2c7e1b4d9f6a3c8e0b5d2f7a1c4e9b6d3f8a0c5e27"
},
"service": {
"id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"space_id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"name": "Orders API",
"environment": "sandbox"
}
}Sign-in routes (app host only)
| Route | Behaviour |
|---|---|
GET /auth/config | {"oidc_configured":bool,"sandbox_guest":bool} |
GET /auth/login?return_to=/path | 302 to the identity provider (PKCE S256, scope=openid email profile). return_to must be a same-site path. 503 oidc_unconfigured if sign-in is not configured. |
GET /auth/callback | Exchanges the code, verifies the ID token and sets the session cookie, then 302 to return_to. |
POST /auth/sandbox | Creates a labelled sandbox guest and session: 201 {"user_id","csrf_token"}. 403 if disabled. |
POST /auth/logout | Revokes the session and clears the cookie: 204. |
POST /auth/passkey/register/verify | Creates a passkey account: 201 {"user_id","csrf_token","recovery_codes":[…10]}. The recovery codes appear only in this response. |
POST /auth/passkey/recovery | {"code":"mtrc_…"} redeems one recovery code: 201 {"user_id","csrf_token","purpose":"passkey_recovery","remaining_codes"} and a 15-minute session that can only call GET /v1/me, GET /v1/me/passkeys, POST /v1/me/passkeys/options and POST /v1/me/passkeys (everything else is 403 forbidden). Registering the passkey ends the session. Unknown or used codes: 401 unauthenticated. Rate limited per address. |
Organizations
POST /v1/organizations
Creates an organization and makes you its owner, trust admin, service owner and partner admin. Billing admin is not implied.
| Auth | Session or API credential (any signed-in user) |
|---|---|
| Headers | Idempotency-Key (optional) |
| Request body | name (string, 1 to 80, no control characters or <>) |
| Success | 201 {"id":"org_…","name":"Northstar","kind":"customer"} |
| Errors | validation_failed |
GET /v1/organizations/{id}/members
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 {"data":[{"user_id","display_name","email","roles":[…]}]} |
POST /v1/organizations/{id}/roles
Adds a role to an existing member. Additive and idempotent.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Request body | user_id (usr_…), role (owner | trust_admin | service_owner | partner_admin | billing_admin | auditor) |
| Success | 200 {"user_id":"usr_…","role":"billing_admin"} |
| Errors | not_found, forbidden, validation_failed |
DELETE /v1/organizations/{id}/members/{user_id}/roles/{role}
Removes one role from a member. Credentials the member created lose the role at once. Removing the last role ends the membership, which revokes the member’s API credentials and audit export rights.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"user_id":"usr_…","role":"auditor","removed":true,"member_removed":false,"credentials_revoked":0} |
| Errors | last_owner (the organization’s only owner), not_found (no such membership), forbidden, validation_failed (unknown role) |
DELETE /v1/organizations/{id}/members/{user_id}
Removes a member entirely and revokes every API credential they created in the organization, in one batch with the audit entry.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"user_id":"usr_…","removed":true,"roles":["service_owner"],"credentials_revoked":2} |
| Errors | last_owner (including removing yourself as the only owner), not_found, forbidden |
GET /v1/organizations/{id}/partner-handle
The handle API owners use to invite this organization. See Invite an existing partner by handle.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 {"org_id":"org_…","handle":"harbor-logistics-7kq2m9xd","rotated_at":null} |
POST /v1/organizations/{id}/partner-handle/rotate
Replaces the handle. The old one stops resolving immediately; invitations already sent stay bound to the organization.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"org_id":"org_…","handle":"harbor-logistics-q3w8p2rt","rotated_at":"…"} |
| Errors | not_found, forbidden, concurrent_change |
GET /v1/partner-handles/{handle}
Resolves a partner handle to the organization’s display name and nothing else. Only organizations someone has claimed resolve.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner or trust_admin in any organization |
| Success | 200 {"handle":"harbor-logistics-7kq2m9xd","organization_name":"Harbor Logistics"} |
| Errors | not_found, forbidden, rate_limited (30 lookups per hour per principal) |
GET /v1/organizations/{id}/workload-issuers
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 {"data":[{"id","issuer_url","jwks_uri","state","created_at"}]} |
POST /v1/organizations/{id}/workload-issuers
Approves an OIDC workload issuer for this organization’s workloads.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | partner_admin |
| Request body | issuer_url (https URI), jwks_uri (https URI) |
| Success | 201 {"id":"wli_01jcqc2e4f6g8h0j2k4m6n8p0q","issuer_url","jwks_uri","state":"approved"} |
GET /v1/organizations/{id}/audit
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner, trust_admin, auditor |
| Query | before: the next_cursor of the previous page |
| Success | 200 {"data":[audit events],"next_cursor":string|null}, 100 per page |
{
"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"
}GET /v1/organizations/{id}/audit-export
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | audit.read plus an active export grant |
| Query | from, to (RFC 3339, optional; to exclusive) |
| Success | 200 attachment, mt.audit_export.v1 canonical JSON, header X-Content-SHA256 |
| Errors | forbidden |
GET /v1/organizations/{id}/support-grants
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"data":[{"id","staff_email","action","reason_code","expires_at","created_at","approved_by","active"}]} (active is 1 or 0) |
POST /v1/organizations/{id}/support-grants
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Request body | staff_email, action (audit.detail | invitation.resend), reason_code (customer_request | incident | diagnostics), expires_in_hours (1 to 72) |
| Success | 201 {"id":"sg_01jcqd3f5g7h9j1k3m5n7p9q1r","staff_email","action","reason_code","expires_at"} |
POST /v1/organizations/{id}/support-grants/{gid}/revoke
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"id","expires_at","active":false} |
| Errors | not_found |
GET /v1/organizations/{id}/export-grants
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"data":[{"id","user_id","display_name","expires_at","revoked_at","created_at","active"}]} |
POST /v1/organizations/{id}/export-grants
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Request body | user_id (a member holding auditor, owner or trust_admin), expires_in_hours (1 to 168) |
| Success | 201 {"id":"exg_01jcqd4g6h8j0k2m4n6p8q0r2s","user_id","expires_at"} |
| Errors | validation_failed |
POST /v1/organizations/{id}/export-grants/{gid}/revoke
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner |
| Success | 200 {"id","active":false} |
GET /v1/organizations/{id}/api-credentials
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 {"data":[{"id","name","roles","prefix","purpose","expires_at","revoked_at","last_used_at","created_at","created_by","active"}]} |
POST /v1/organizations/{id}/api-credentials
| Auth | Signed-in session only (a credential cannot create credentials) |
|---|---|
| Role | Any member; requested roles must be ones you hold |
| Request body | name (1 to 80), roles (non-empty, unique, subset of yours), expires_in_days (1 to 365, default 90) |
| Success | 201 |
| Errors | forbidden, validation_failed |
{
"id": "cred_01jcqd5h7j9k1m3n5p7q9r1s3t",
"secret": "mtk_Jx4c…",
"expires_at": "2027-02-12T16:20:00Z",
"roles": [
"service_owner"
],
"name": "terraform-ci",
"note": "Shown once. Send as \"Authorization: Bearer <secret>\" to https://api.mutualtrust.io."
}POST /v1/organizations/{id}/api-credentials/{cid}/revoke
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | The credential’s creator or an owner |
| Success | 200 {"id":"cred_…","active":false} |
| Errors | not_found, forbidden |
POST /v1/mcp/authorizations
Portal consent for an MCP client: mints a 30-day purpose: "mcp" credential and hands it once to the MCP server. See MCP server.
| Auth | Signed-in session only |
|---|---|
| Role | Requested roles must be ones you hold in org_id |
| Request body | request_id (16 to 128 URL-safe characters, from the MCP authorization request), org_id, roles, client_name (optional, ≤120) |
| Success | 201 {"redirect_url":"https://mcp.mutualtrust.io/oauth/complete?req=…&code=…","credential_id","expires_at"} |
| Errors | mcp_request_used, mcp_unconfigured, forbidden |
GET /v1/organizations/{id}/billing
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | billing_admin, owner, auditor |
| Success | 200, see billing summary |
Spaces
POST /v1/spaces
Creates a trust space and provisions its private root, issuing CA and manifest key through the signer. Production spaces cannot be created by a sandbox guest and are provisioned with Sectigo PKI Operations on hardware-protected keys; request production access from the portal (see Environments).
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner, trust_admin |
| Headers | Idempotency-Key (optional) |
| Request body | org_id, name (1 to 80), environment (sandbox | production) |
| Success | 201 |
| Errors | space_exists, forbidden, not_found |
{
"id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"org_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Orders sandbox",
"environment": "sandbox",
"trust_fingerprint": "9f2c41d8e3b7a6051c4e8d2f7a9b3c6e1d5f0a8b2c7e4d9f3a6b1c8e5d2f7a40",
"issuing_fingerprint": "4b8e2d7f1a9c3e6b0d5f8a2c7e1b4d9f6a3c8e0b5d2f7a1c4e9b6d3f8a0c5e27"
}GET /v1/spaces/{id}
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 |
{
"id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Orders sandbox",
"environment": "sandbox",
"root_issuer_id": "iss_01jcqa7m4c1d3e5f7g9h1j3k5m",
"issuing_issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b",
"manifest_key_version_id": "kv_01jcqa7m4n6p8q0r2s4t6v8w0x",
"created_at": "2026-11-14T15:20:04Z",
"issuers": [
{
"id": "iss_01jcqa7m4c1d3e5f7g9h1j3k5m",
"kind": "root",
"subject_dn": "CN=Mutual Trust SANDBOX TEST Root · Orders sandbox, O=Northstar, OU=spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"cert_fingerprint": "9f2c41d8e3b7a6051c4e8d2f7a9b3c6e1d5f0a8b2c7e4d9f3a6b1c8e5d2f7a40",
"not_before": "2026-11-14T15:19:04Z",
"not_after": "2027-02-12T15:20:04Z",
"state": "active",
"signer_mode": "simulator",
"profile": "mt.ca.root.v1"
},
{
"id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b",
"kind": "intermediate",
"subject_dn": "CN=Mutual Trust SANDBOX TEST Issuing CA · Orders sandbox, O=Northstar, OU=spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"cert_fingerprint": "4b8e2d7f1a9c3e6b0d5f8a2c7e1b4d9f6a3c8e0b5d2f7a1c4e9b6d3f8a0c5e27",
"not_before": "2026-11-14T15:19:04Z",
"not_after": "2027-01-13T15:20:04Z",
"state": "active",
"signer_mode": "simulator",
"profile": "mt.workload.clientauth.v1"
}
]
}GET /v1/spaces/{id}/trust-bundle.pem
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member |
| Success | 200 application/x-pem-file attachment: each issuer preceded by # <kind> <issuer_id> state=<state> sha256=<fingerprint> |
Services
POST /v1/services
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | owner, trust_admin, service_owner |
| Headers | Idempotency-Key (optional) |
| Request body | space_id, name (1 to 80), contact (optional, ≤200, shown to partners), route_templates (1 to 50 of {name, methods[], path, restricted?}) |
| Success | 201 {"id":"svc_…","space_id","name","environment"} |
| Errors | space_not_provisioned, service_exists, validation_failed |
{
"space_id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"name": "Orders API",
"contact": "api-platform@northstar.example",
"route_templates": [
{
"name": "Read orders",
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"name": "Submit orders",
"methods": [
"POST"
],
"path": "/orders"
},
{
"name": "Admin export (restricted)",
"methods": [
"GET"
],
"path": "/admin/*",
"restricted": true
}
]
}GET /v1/services/{id}
Service detail with gateways, current policy revision and the readiness checklist.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member of the owning organization |
| Success | 200 |
{
"id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"space_id": "spc_01jcqa7m4f6g8h2j4k6m8n0p2q",
"environment": "sandbox",
"name": "Orders API",
"contact": "api-platform@northstar.example",
"owner_user_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0",
"policy_epoch": 1,
"created_at": "2026-11-14T15:20:05Z",
"sample_partner_org_id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"route_templates": [
{
"name": "Read orders",
"methods": [
"GET"
],
"path": "/orders/*"
}
],
"space_name": "Orders sandbox",
"trust_fingerprint": "9f2c41d8e3b7a6051c4e8d2f7a9b3c6e1d5f0a8b2c7e4d9f3a6b1c8e5d2f7a40",
"current_policy": {
"epoch": 1,
"revision": 2,
"policy_hash": "c7d2e9f4a1b6c3d8e5f0a7b2c9d4e1f6a3b8c5d0e7f2a9b4c1d6e3f8a5b0c7d2",
"created_at": "2026-11-14T15:29:41Z",
"cause": "grant.activated"
},
"gateways": [
{
"id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v",
"name": "orders-edge-1",
"state": "paired",
"connector_version": "mt-ref-gateway/0.9.0",
"last_ack_epoch": 1,
"last_ack_revision": 2,
"last_ack_hash": "c7d2e9f4a1b6c3d8e5f0a7b2c9d4e1f6a3b8c5d0e7f2a9b4c1d6e3f8a5b0c7d2",
"last_ack_at": "2026-11-14T15:29:55Z",
"last_seen_at": "2026-11-14T15:41:02Z",
"gateway_key_thumbprint": "3nUq0b2s9kTq7m1x…",
"created_at": "2026-11-14T15:24:10Z",
"status": "current",
"policy_age_seconds": 667
}
],
"readiness": [
{
"step": "trust_anchor",
"label": "Private trust anchor provisioned",
"ok": true
},
{
"step": "gateway_paired",
"label": "Gateway paired with a pinned policy key",
"ok": true
},
{
"step": "policy_applied",
"label": "Gateway acknowledged the current policy revision",
"ok": true
},
{
"step": "allow_receipt",
"label": "Allowed request observed",
"ok": true
},
{
"step": "deny_receipt",
"label": "Denied control request observed",
"ok": true
}
],
"verified": true
}PATCH /v1/services/{id}
Changes service settings. grant_approval: "sponsor_review" makes every accepted invitation wait for a service owner’s approval. See Sponsor review.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin |
| Request body | At least one of grant_approval (automatic | sponsor_review) and contact (string up to 200, or null) |
| Success | 200 {"id":"svc_…","grant_approval":"sponsor_review","contact":"…"} |
| Errors | validation_failed, not_found, forbidden |
GET /v1/services/{id}/receipts
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | grant.read (owner, trust admin, service owner, partner admin, auditor) in the owning organization |
| Success | 200 latest 100 receipts |
{
"data": [
{
"receipt_id": "rc_5d8e2a7c1f4b9e3a6c0d",
"request_id": "req_7b2e9d4a1f6c3e8b5a0d",
"gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v",
"gateway_name": "orders-edge-1",
"epoch": 1,
"revision": 2,
"tls_verdict": "verified",
"tls_reason": null,
"authz_verdict": "deny",
"authz_reason": "route_not_permitted",
"grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"cert_serial": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9",
"method": "GET",
"route": "/admin/export",
"observed_at": "2026-11-14T15:40:31Z"
}
]
}GET /v1/services/{id}/invitations
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | grant.read |
| Success | 200 {"data":[{"id","consumer_org_id","consumer_name","scope","state","expires_at","created_at"}]} (latest 100) |
POST /v1/services/{id}/invitations
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin |
| Headers | Idempotency-Key (required) |
| Request body | consumer ({org_handle}, {new_org_name} or {org_id}), scope, expires_in_hours (1 to 720, default 168), grant_expires_at (optional date-time) |
| Success | 201, includes the one-time token and accept_url |
| Errors | idempotency_key_required, idempotency_key_reused, validation_failed, not_found (unknown or unclaimed partner handle) |
{
"id": "inv_01jcqb2n8c4d6e8f0g2h4j6k8m",
"service_id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"consumer_org_id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"consumer_binding": "existing_org",
"state": "pending",
"expires_at": "2026-11-21T15:40:00Z",
"scope": {
"v": 1,
"routes": [
{
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"methods": [
"POST"
],
"path": "/orders"
}
]
},
"token": "mtinv_V9n2…",
"accept_url": "https://app.mutualtrust.io/#/invitations/inv_01jcqb2n8c4d6e8f0g2h4j6k8m?token=mtinv_V9n2…"
}POST /v1/services/{id}/gateways
Approves a pairing code shown by a gateway and binds the gateway to this service.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin |
| Request body | code (XXXX-XXXX, case-insensitive), name (1 to 80) |
| Success | 201 {"gateway_id":"gw_01jcqa9b6k8m0n2p4q6r8s0t2v","service_id","name","key_thumbprint"} |
| Errors | not_found (unknown, used or expired code) |
Invitations
POST /v1/invitations/{id}/preview
| Auth | The invitation secret in the body (no sign-in needed) |
|---|---|
| Request body | token |
| Success | 200 recipient view, see Partners |
| Errors | not_found |
POST /v1/invitations/{id}/accept
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential, plus the invitation secret |
|---|---|
| Role | partner_admin or owner in the consumer organization, or none when claiming an unclaimed organization |
| Headers | Idempotency-Key (required) |
| Request body | token, consumer_org_id, workload (name, attestation_kind = sandbox_token | oidc_jwt, and for OIDC issuer, audience, subject and optional subject_uid) |
| Success | 201; state is pending with awaiting_approval: true when the service uses sponsor review |
| Errors | invitation_not_pending, invitation_expired, capacity_exceeded, concurrent_change (a new organization was claimed first), forbidden, not_found, validation_failed |
{
"grant_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"state": "active",
"service": "Orders API",
"environment": "sandbox",
"workload": {
"id": "wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"name": "harbor-order-sync",
"principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g"
},
"scope": {
"v": 1,
"routes": [
{
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"methods": [
"POST"
],
"path": "/orders"
}
]
}
}POST /v1/invitations/{id}/decline
| Auth | The invitation secret in the body |
|---|---|
| Request body | token |
| Success | 200 {"id","state":"declined"} |
| Errors | invitation_not_pending, not_found |
POST /v1/invitations/{id}/cancel
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin (sponsor) |
| Success | 200 {"id","state":"canceled"} |
| Errors | invitation_not_pending |
Grants and operations
GET /v1/grants
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | grant.read |
| Query | service_id (sponsor view) or org_id (partner view); one is required |
| Success | 200 {"data":[…]} (latest 200) |
| Errors | validation_failed when neither is given |
{
"id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"state": "active",
"revision": 2,
"scope": {
"v": 1,
"routes": [
{
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"methods": [
"POST"
],
"path": "/orders"
}
]
},
"expires_at": null,
"environment": "sandbox",
"consumer_org_id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"consumer_name": "Harbor Logistics",
"workload_id": "wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"workload_name": "harbor-order-sync",
"updated_at": "2026-11-14T15:29:40Z",
"cert_expires_at": "2026-11-15T15:30:30Z"
}{
"id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"state": "active",
"revision": 2,
"scope": {
"v": 1,
"routes": [
{
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"methods": [
"POST"
],
"path": "/orders"
}
]
},
"expires_at": null,
"environment": "sandbox",
"service_name": "Orders API",
"contact": "api-platform@northstar.example",
"sponsor_name": "Northstar",
"workload_id": "wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"workload_name": "harbor-order-sync",
"cert_expires_at": "2026-11-15T15:30:30Z",
"renew_after": "2026-11-15T07:26:41Z"
}GET /v1/grants/{id}
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | grant.read on either side |
| Success | 200 with ETag |
{
"id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"side": "sponsor",
"sides": [
"sponsor"
],
"state": "active",
"revision": 2,
"environment": "sandbox",
"scope": {
"v": 1,
"routes": [
{
"methods": [
"GET"
],
"path": "/orders/*"
},
{
"methods": [
"POST"
],
"path": "/orders"
}
]
},
"expires_at": null,
"reason_code": null,
"created_at": "2026-11-14T15:29:40Z",
"updated_at": "2026-11-14T15:29:40Z",
"service": {
"id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"name": "Orders API",
"contact": "api-platform@northstar.example"
},
"sponsor": {
"id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"name": "Northstar"
},
"consumer": {
"id": "org_01jcqa7m3a2b7c9d4e6f8g1h3j",
"name": "Harbor Logistics"
},
"workload": {
"id": "wl_01jcqb3p1y2z4a6b8c0d2e4f6g",
"name": "harbor-order-sync",
"attestation_kind": "sandbox_token",
"attestation_issuer": "urn:mutualtrust:sandbox",
"attestation_subject": "sbx_01jcqb3p2a4b6c8d0e2f4g6h8j",
"principal": "urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g"
},
"certificates": [
{
"id": "crt_01jcqb5r0d2e4f6g8h0j2k4m6n",
"serial_hex": "5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9",
"fingerprint": "2e7a…",
"not_before": "2026-11-14T15:30:30Z",
"not_after": "2026-11-15T15:30:30Z",
"renew_after": "2026-11-15T07:26:41Z",
"state": "issued",
"revoked_at": null,
"revocation_reason": null,
"predecessor_id": null,
"reload_confirmed_at": null,
"issuer_id": "iss_01jcqa7m4s8t0v2w4x6y8z0a2b",
"created_at": "2026-11-14T15:31:01Z"
}
],
"last_successful_call": "2026-11-14T15:40:12Z"
}POST /v1/grants/{id}/approve
Approves a pending grant (sponsor review): pending to active, emits grant.activated and publishes a policy revision. In production, connection capacity is checked atomically.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin (sponsor) |
| Headers | If-Match: "<revision>" (required), Idempotency-Key (optional) |
| Request body | note (optional, up to 500) |
| Success | 200 GrantTransition |
| Errors | precondition_required, revision_mismatch, invalid_transition (not pending), capacity_exceeded (the grant stays pending), forbidden, not_found |
POST /v1/grants/{id}/reject
Rejects a pending grant: pending to revoked with a reason the partner can see.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin (sponsor) |
| Headers | If-Match: "<revision>" (required), Idempotency-Key (optional) |
| Request body | reason_code (contract_ended | security_incident | owner_request), note (optional) |
| Success | 200 GrantTransition |
| Errors | precondition_required, revision_mismatch, invalid_transition, validation_failed, forbidden, not_found |
POST /v1/grants/{id}/suspend
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin (sponsor) |
| Headers | If-Match (required), Idempotency-Key (optional) |
| Request body | reason_code (contract_ended | security_incident | owner_request), note (optional, ≤500) |
| Success | 200 transition result (below) |
| Errors | precondition_required, revision_mismatch, invalid_transition, forbidden |
POST /v1/grants/{id}/resume
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Sponsor service_owner, trust_admin |
| Headers | If-Match (required), Idempotency-Key (optional) |
| Request body | note (optional) |
| Success | 200 transition result |
| Errors | precondition_required, revision_mismatch, invalid_transition |
POST /v1/grants/{id}/revoke
Withdraws the grant permanently and revokes its live certificates.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Sponsor service_owner, trust_admin |
| Headers | If-Match (required), Idempotency-Key (optional) |
| Request body | reason_code, note (optional) |
| Success | 200 with ETag |
| Errors | precondition_required, revision_mismatch, invalid_transition, forbidden |
{
"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
}PATCH /v1/grants/{id}/scope
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Sponsor service_owner, trust_admin |
| Headers | If-Match (required unless preview) |
| Request body | scope, preview (boolean), note (optional) |
| Success | 200 preview {"preview":true,"diff":{added,removed,unchanged},"current_revision"} or result {"grant_id","revision","diff","operation_id"} |
| Errors | revision_mismatch, invalid_transition, validation_failed |
GET /v1/operations/{id}
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Any member of the organization that owns the operation (the sponsor) |
| Success | 200, see Follow the operation |
Enrollment and certificates
PATCH /v1/workloads/{id}/binding
Binds or rebinds an OIDC workload to a Kubernetes service account UID (the token claim kubernetes.io.serviceaccount.uid). Audited for the partner and for every API owner with a live grant to the workload. See Bind the service account UID.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | partner_admin of the workload’s organization |
| Request body | subject_uid (string, or null to remove the binding), note (optional) |
| Success | 200 {"workload_id":"wl_…","attestation_subject":"system:serviceaccount:orders:order-sync","attestation_subject_uid":"0a0b…","previous_subject_uid":"6f1a…","binding_updated_at":"…"} |
| Errors | validation_failed (sandbox workload or malformed UID), concurrent_change, forbidden, not_found |
POST /v1/workloads/{id}/enrollment-tokens
Sandbox only: approves enrollment and returns a single-use token valid for 30 minutes.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | partner_admin of the workload’s organization |
| Request body | grant_id |
| Success | 201 {"token":"mtenr_…","expires_at","grant_id","workload_id","label"} |
| Errors | grant_not_active, validation_failed, not_found |
POST /v1/enrollments
| Auth | Attestation in the body |
|---|---|
| Request body | attestation ({kind: sandbox_token | oidc_jwt, token}); for OIDC also workload_id and grant_id |
| Success | 201 {"enrollment_id","challenge","expires_at","proof_input","workload_id","grant_id"} |
| Errors | attestation_invalid, attestation_replayed, unauthenticated, grant_not_active, jwks_unavailable |
POST /v1/enrollments/{id}/complete
| Auth | Proof of possession |
|---|---|
| Request body | csr (base64 DER or PEM, P-256, ECDSA-SHA256), proof (base64url raw signature over proof_input) |
| Success | 200 issued certificate, or 202 {"state":"pending","issuance_request_id","poll_url","retry_after_seconds":5} |
| Errors | enrollment_used, challenge_expired, validation_failed, grant_not_active, grant_expiring, issuance_refused, issuer_unavailable |
Response body: see Enrollment.
GET /v1/issuance-requests/{id}
| Auth | X-Enrollment-Challenge header |
|---|---|
| Success | 200 issued certificate, {"state":"pending",…} while signing is retried, or {"state":"failed","issuance_request_id"} if the signer refused it |
| Errors | not_found (unknown request or wrong challenge) |
POST /v1/certificates/{id}/renew
| Auth | Key-continuity proof |
|---|---|
| Request body | csr, proof, timestamp (unix seconds, ±300 s) |
| Success | 200 issued certificate (or state: "pending") |
| Errors | not_found, certificate_revoked, certificate_expired, grant_not_active, validation_failed |
POST /v1/certificates/{id}/reloaded
| Auth | Proof with the new key |
|---|---|
| Request body | proof, timestamp |
| Success | 200 {"certificate_id","reload_confirmed":true,"predecessor_retirable"} |
| Errors | not_found, validation_failed |
POST /v1/certificates/{id}/revoke
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | Sponsor service_owner/trust_admin, or the workload’s partner_admin |
| Request body | reason_code (key_compromise | superseded | cessation_of_operation | owner_request) |
| Success | 200 {"certificate_id","state":"revoked","operation_id","effective_control_plane_time"} |
| Errors | already_revoked, not_found |
GET /v1/certificates/{id}/chain.pem
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | certificate.read on either side |
| Success | 200 application/x-pem-file: leaf, issuing CA, root |
Gateways
POST /v1/pairings
A gateway registers its public key and receives a code to show its operator. The code expires after 15 minutes.
| Auth | None (rate-limited: 30 per hour per client IP) |
|---|---|
| Request body | gateway_public_jwk ({kty:"EC", crv:"P-256", x, y}), connector_version (optional, ≤40) |
| Success | 201 |
| Errors | validation_failed, rate_limited |
{
"pairing_id": "pair_01jcqa9a4w6x8y0z2a4b6c8d0e",
"code": "K7QM-4TXZ",
"poll_secret": "bW9…",
"expires_at": "2026-11-14T15:39:10Z",
"key_thumbprint": "3nUq0b2s9kTq7m1x…",
"approve_url": "https://app.mutualtrust.io/#/pair?code=K7QM-4TXZ"
}GET /v1/pairings/{id}
Poll until approved. The approved payload (including the credential) is returned exactly once; later polls return {"state":"collected"}.
| Auth | X-Poll-Secret header |
|---|---|
| Success | 200 {"state":"pending"}, {"state":"expired"} or the approved payload |
{
"state": "approved",
"gateway_id": "gw_01jcqa9b6k8m0n2p4q6r8s0t2v",
"tenant_id": "org_01jcqa7m2k9x4v8r3t6p5w2h7n",
"service_id": "svc_01jcqa7m5r7s9t1v3w5x7y9z1a",
"environment": "sandbox",
"credential": "mtgw_…",
"manifest_url": "https://api.mutualtrust.io/v1/gateways/gw_01jcqa9b6k8m0n2p4q6r8s0t2v/manifest",
"policy_verification_key": {
"kid": "kv_01jcqa7m4n6p8q0r2s4t6v8w0x",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
},
"refresh_seconds": 15
}POST /v1/gateways/{id}/disable
Disables a gateway; its credential stops working immediately.
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | service_owner, trust_admin |
| Success | 200 {"gateway_id","state":"disabled"} |
GET /v1/gateways/{id}/manifest
| Auth | Gateway credential (its own ID only) |
|---|---|
| Success | 200 {"manifest":"<compact JWS>","epoch","revision"} |
| Errors | policy_not_published, not_found |
Format: Manifest JWS.
POST /v1/gateways/{id}/acks
| Auth | Gateway credential + JWS signed by the gateway key |
|---|---|
| Request body | ack (compact JWS, typ: mt.ack.v1+jws) |
| Success | 202 {"accepted":true,"received_at"} |
| Errors | validation_failed (bad signature, unknown revision, hash mismatch) |
POST /v1/gateways/{id}/receipts
| Auth | Gateway credential + JWS signed by the gateway key |
|---|---|
| Request body | receipts (≤100 compact JWS, typ: mt.receipt.v1+jws) |
| Success | 202 {"accepted":n,"rejected":n} |
Billing
POST /v1/billing/checkout-sessions
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | billing_admin |
| Headers | Idempotency-Key (required) |
| Request body | org_id, sku (team_10 | team_50) |
| Success | 201 {"operation_id":"chk_…","url","mode"} |
| Errors | subscription_exists, plan_unavailable, billing_unconfigured, stripe_error, stripe_unavailable |
POST /v1/billing/portal-sessions
| Auth | Session cookie on app.mutualtrust.io (+ X-CSRF-Token on writes) or API credential |
|---|---|
| Role | billing_admin |
| Request body | org_id |
| Success | 201 {"url"} |
| Errors | billing_unconfigured, stripe_unavailable |
POST /webhooks/stripe
For Stripe only.
| Auth | Stripe-Signature over the raw body (5-minute tolerance) |
|---|---|
| Success | 200 {"received":true,"duplicate":bool}; 400 {"code":"invalid_signature"} |
Instrumentation and public endpoints
POST /v1/adoption-events
| Auth | None |
|---|---|
| Request body | milestone (demo_started | paired | enrolled | first_allow | first_deny | unattended_renewal), elapsed_ms, connector_version, error_code, cohort_id (all optional except milestone). No personal data or tokens. |
| Success | 202 {"recorded":true} |
GET /healthz
| Auth | None |
|---|---|
| Success | 200 {"ok":true,"deployment":"sandbox"} |
GET /openapi.json
| Auth | None |
|---|---|
| Success | 200 OpenAPI 3.1.1 document generated from the command schemas |
GET /pki/crl/{issuer_id}.crl
| Auth | None |
|---|---|
| Success | 200 DER application/pkix-crl, Cache-Control: public, max-age=300; 404 until first published |
OpenAPI coverage