{
  "schema": "ecz.msp.partner_api.descriptor.v1",
  "title": "ECZ-ID TrustOps Partner API v1 (MSP / MSSP)",
  "description": "The API an MSP / MSSP partner of record uses to operate its managed clients' ECZ-ID estates within each client's explicit, revocable consent. The client always owns its ECZ-ID, Passports, evidence and entitlements; the partner acts under delegated authority only.",
  "base_url": "https://trustops.ecocitizenz.com",
  "documentation": "https://trustops.ecocitizenz.com/console/partner",
  "authentication": {
    "session": "A signed-in TrustOps account (one-time email code). Required for every write that changes a relationship, for key management, for every client-side act and for delegated Receipts reads.",
    "machine_credential": {
      "scheme": "Authorization: Bearer eczmsp_<24 hex key id>_<64 hex secret>",
      "issued_at": "/console/partner (API & integrations), signed in; the secret is shown once",
      "authenticates": "the partner account only; every client read still requires the relationship, the partner's MSP plan in force and the client's own consent",
      "scopes": ["partner.read", "clients.read", "client.entitlements.read", "client.passports.read", "client.watch.read", "client.receipts.read", "events.read", "webhooks.manage", "client.commerce.read", "client.commerce.purchase"],
      "rate_limits": "120 requests per minute per key and 300 per partner, per instance; 429 with retry-after",
      "note": "client.receipts.read is listed for completeness but a machine credential never reads Receipts: Receipts require a human sign-in (403 RECEIPTS_REQUIRE_SESSION)."
    }
  },
  "client_consents": {
    "accepted_management": ["estate.read", "entitlements.read", "evidence.read"],
    "delegated_scopes": {
      "client.watch.read": "the client's whole Watch, shared by the granting account",
      "client.receipts.read": "the client's private Receipts, through ECZ-ID Core's delegated-reader authority (available only once Core's partner delegation is switched on)",
      "client.commerce.purchase": "the partner may pay for eligible products whose entitlements belong to the client"
    },
    "granted_by": "a sign-in that governs the client organisation, at POST /api/v1/partner-of-record/scopes; withdrawn at DELETE; every ending of the relationship withdraws them all"
  },
  "endpoints": [
    { "method": "GET", "path": "/api/v1/partner", "key_scope": "partner.read" },
    { "method": "POST", "path": "/api/v1/partner/organisation", "auth": "session" },
    { "method": "GET", "path": "/api/v1/partner/clients", "key_scope": "clients.read" },
    { "method": "POST", "path": "/api/v1/partner/clients", "auth": "session" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}", "key_scope": "clients.read" },
    { "method": "DELETE", "path": "/api/v1/partner/clients/{eczId}", "auth": "session" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/watch", "key_scope": "client.watch.read", "consent": "client.watch.read" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/receipts", "auth": "session", "consent": "client.receipts.read", "availability": "when delegated Receipts is switched on" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/receipts/{receiptId}", "auth": "session", "consent": "client.receipts.read", "availability": "when delegated Receipts is switched on" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/receipts/history", "auth": "session", "consent": "client.receipts.read", "availability": "when delegated Receipts is switched on" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/commerce", "key_scope": "client.commerce.read" },
    { "method": "POST", "path": "/api/v1/partner/clients/{eczId}/commerce/checkout", "key_scope": "client.commerce.purchase", "consent": "client.commerce.purchase" },
    { "method": "GET", "path": "/api/v1/partner/clients/{eczId}/commerce/orders", "key_scope": "client.commerce.read" },
    { "method": "GET", "path": "/api/v1/partner/events", "key_scope": "events.read" },
    { "method": "GET", "path": "/api/v1/partner/credentials", "key_scope": "partner.read" },
    { "method": "POST", "path": "/api/v1/partner/credentials", "auth": "session" },
    { "method": "GET", "path": "/api/v1/partner/credentials/{keyId}", "key_scope": "partner.read" },
    { "method": "PATCH", "path": "/api/v1/partner/credentials/{keyId}", "auth": "session" },
    { "method": "DELETE", "path": "/api/v1/partner/credentials/{keyId}", "auth": "session" },
    { "method": "GET", "path": "/api/v1/partner/webhooks", "key_scope": "webhooks.manage" },
    { "method": "POST", "path": "/api/v1/partner/webhooks", "key_scope": "webhooks.manage" },
    { "method": "GET", "path": "/api/v1/partner/webhooks/{endpointId}", "key_scope": "webhooks.manage" },
    { "method": "PATCH", "path": "/api/v1/partner/webhooks/{endpointId}", "key_scope": "webhooks.manage" },
    { "method": "DELETE", "path": "/api/v1/partner/webhooks/{endpointId}", "key_scope": "webhooks.manage" },
    { "method": "GET", "path": "/api/v1/partner/webhooks/{endpointId}/deliveries", "key_scope": "webhooks.manage" },
    { "method": "POST", "path": "/api/v1/partner/webhooks/{endpointId}/deliveries", "key_scope": "webhooks.manage" },
    { "method": "GET", "path": "/api/v1/partner-of-record", "auth": "session (the client)" },
    { "method": "POST", "path": "/api/v1/partner-of-record", "auth": "session (the client)" },
    { "method": "DELETE", "path": "/api/v1/partner-of-record", "auth": "session (the client)" },
    { "method": "POST", "path": "/api/v1/partner-of-record/scopes", "auth": "session (the client)" },
    { "method": "DELETE", "path": "/api/v1/partner-of-record/scopes", "auth": "session (the client)" }
  ],
  "webhooks": {
    "envelope_schema": "ecz.msp.partner.webhook.v1",
    "signature": "X-TrustOps-Signature = hex HMAC-SHA256(endpoint secret, X-TrustOps-Timestamp + '.' + raw body); verify within 5 minutes",
    "headers": ["X-TrustOps-Timestamp", "X-TrustOps-Signature", "X-ECZ-Webhook-Event-Id", "X-ECZ-Webhook-Event-Type"],
    "delivery": "at least once; the same event_id on every retry; bounded exponential backoff, at most 10 attempts, then dead letter; HTTPS public endpoints only; at most 5 endpoints per partner; payloads at most 64 KB",
    "source_of_truth": "GET /api/v1/partner/events (the pull feed); a webhook is a signed notification of an event already recorded"
  },
  "events": ["msp.partner.organisation_attached", "msp.partner.plan_activated", "msp.partner.plan_renewed", "msp.partner.plan_withdrawn", "msp.partner.plan_restored", "msp.partner.plan_cancel_scheduled", "msp.partner.audit_paid", "msp.partner.audit_cancelled", "msp.client.invited", "msp.client.invitation_withdrawn", "msp.client.declined", "msp.client.linked", "msp.client.unlinked", "msp.client.transferred", "msp.client.scope_granted", "msp.client.scope_withdrawn", "msp.client.purchase_started", "msp.client.purchase_applied", "msp.client.purchase_renewed", "msp.client.purchase_reversed", "msp.client.purchase_review"],
  "errors": "JSON {error} with HTTP 400 named malformed requests (CURSOR_INVALID for a pull-feed cursor the feed did not hand out), 401 NOT_AUTHENTICATED, 403 (ACCOUNT_PRINCIPAL_REQUIRED, SESSION_REQUIRED, SCOPE_REQUIRED, SCOPE_NOT_GRANTED, PARTNER_PLAN_NOT_IN_FORCE), 404 NOT_MANAGED (identical for another partner's client, an unknown client and a malformed id), 409 named states, 422 validation, 429 rate limited, 503 never a partial answer. Every answer is no-store and carries no other tenant's identifiers, no email and no provider id.",
  "policy": {
    "client_owns_identity_and_evidence": true,
    "partner_is_never_owner": true,
    "payer_and_entitlement_holder_are_distinct": true,
    "payment_is_proof": false
  }
}
