{
  "openapi": "3.1.0",
  "info": {
    "title": "NESSA Protocol API",
    "version": "1.0.0",
    "description": "Reference API for the NESSA + qFold zero-knowledge identity protocol. All endpoints are conceptual — this document describes the interface a Verifier, Issuer, or Registry would implement against the NESSA spec.\n\n**Key concepts:**\n- `RegistryCheckpoint` — a signed snapshot of the accumulated credential set at a given epoch\n- `ProofBundle` — the output of a qFold proof: `{proof, session_pubkey, nullifier, root_id}`\n- `Challenge` — a one-time verifier-issued nonce bound to `{aud, exp, root_id, action}`\n- Nullifiers are globally unique per (cred_handle, action_domain, index) — enforcing replay prevention across all verifiers",
    "contact": {
      "name": "NESSA Protocol",
      "url": "https://nessa.sh"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    { "url": "https://registry.nessa.sh", "description": "Registry (public read) — example" },
    { "url": "https://verifier.nessa.sh", "description": "Verifier (relying party) — example" },
    { "url": "https://issuer.nessa.sh", "description": "Issuer — example" }
  ],
  "tags": [
    { "name": "Registry", "description": "Public read endpoints for checkpoint and witness data" },
    { "name": "Challenge", "description": "Verifier challenge issuance" },
    { "name": "Proof", "description": "Proof submission and verification" },
    { "name": "Issuer", "description": "Credential issuance and revocation" },
    { "name": "Session", "description": "Session management after proof verification" }
  ],
  "paths": {
    "/v1/checkpoint/latest": {
      "get": {
        "tags": ["Registry"],
        "summary": "Get latest RegistryCheckpoint",
        "description": "Returns the most recent signed checkpoint. Verifiers must fetch this before issuing a challenge. Wallets fetch it before generating a proof.",
        "operationId": "getLatestCheckpoint",
        "responses": {
          "200": {
            "description": "Latest checkpoint",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RegistryCheckpoint" },
                "example": {
                  "root_id": "chk_7f3a2b",
                  "epoch": 142,
                  "accumulated_at": "2026-03-03T10:00:00Z",
                  "sig": "ed25519:7f3a2b..."
                }
              }
            }
          }
        }
      }
    },
    "/v1/checkpoint/{root_id}": {
      "get": {
        "tags": ["Registry"],
        "summary": "Get checkpoint by root_id",
        "description": "Fetch a specific historical checkpoint by its root_id. Used by verifiers to validate that a proof was generated against a known checkpoint.",
        "operationId": "getCheckpointById",
        "parameters": [
          {
            "name": "root_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "example": "chk_7f3a2b"
          }
        ],
        "responses": {
          "200": {
            "description": "Checkpoint found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RegistryCheckpoint" }
              }
            }
          },
          "404": { "description": "Checkpoint not found" }
        }
      }
    },
    "/v1/witness/{root_id}/{cred_handle}": {
      "get": {
        "tags": ["Registry"],
        "summary": "Get witness bundle for credential at checkpoint",
        "description": "Returns the accumulator membership witness and revocation non-membership witness for a credential at a specific checkpoint root. Wallets call this before generating a proof.",
        "operationId": "getWitnessBundle",
        "parameters": [
          {
            "name": "root_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "cred_handle",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Witness bundle",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WitnessBundle" }
              }
            }
          },
          "404": { "description": "Credential or checkpoint not found" }
        }
      }
    },
    "/v1/challenge": {
      "post": {
        "tags": ["Challenge"],
        "summary": "Issue a login challenge",
        "description": "Called by the relying party backend. Returns a signed challenge that the wallet must bind its proof to. The verifier fetches the latest checkpoint internally and embeds `root_id` in the challenge.",
        "operationId": "createChallenge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ChallengeRequest" },
              "example": {
                "aud": "acme-corp.com",
                "action": "login",
                "exp_seconds": 300
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge issued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Challenge" },
                "example": {
                  "nonce": "nonce_a1b2c3",
                  "aud": "acme-corp.com",
                  "exp": "2026-03-03T10:05:00Z",
                  "root_id": "chk_7f3a2b",
                  "action": "login"
                }
              }
            }
          }
        }
      }
    },
    "/v1/proof/verify": {
      "post": {
        "tags": ["Proof"],
        "summary": "Submit and verify a proof bundle",
        "description": "The relying party submits the proof bundle received from the wallet. The verifier checks: (1) qFold proof validity, (2) challenge binding (nonce, aud, exp, root_id, action), (3) checkpoint freshness, (4) nullifier uniqueness. Returns a session token on success.",
        "operationId": "submitProof",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ProofSubmission" },
              "example": {
                "proof": "qfold:v1:abcdef...",
                "session_pubkey": "ed25519:pub_1a2b3c...",
                "nullifier": "null_7f3a2b...",
                "root_id": "chk_7f3a2b",
                "challenge_nonce": "nonce_a1b2c3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proof accepted — session issued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ProofAccepted" }
              }
            }
          },
          "400": {
            "description": "Proof rejected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ProofRejected" },
                "examples": {
                  "INVALID_PROOF": {
                    "value": { "valid": false, "reason_code": "INVALID_PROOF", "reason_message": "Proof does not verify against the circuit verification key." }
                  },
                  "REPLAY": {
                    "value": { "valid": false, "reason_code": "REPLAY", "reason_message": "Nullifier already present in the spent set." }
                  },
                  "ROOT_STALE": {
                    "value": { "valid": false, "reason_code": "ROOT_STALE", "reason_message": "Proof was generated against a checkpoint that is outside the freshness window." }
                  },
                  "REVOKED": {
                    "value": { "valid": false, "reason_code": "REVOKED", "reason_message": "Credential has been revoked at the current checkpoint." }
                  },
                  "CHALLENGE_EXPIRED": {
                    "value": { "valid": false, "reason_code": "CHALLENGE_EXPIRED", "reason_message": "Challenge nonce has expired." }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/session/verify": {
      "post": {
        "tags": ["Session"],
        "summary": "Verify a session-signed request",
        "description": "After login, the wallet signs subsequent API requests with its ephemeral session private key. The relying party calls this endpoint to verify a session signature without requiring another proof.",
        "operationId": "verifySessionSignature",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SessionVerifyRequest" }
            }
          }
        },
        "responses": {
          "200": { "description": "Signature valid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionVerifyResult" } } } },
          "401": { "description": "Invalid or expired session signature" }
        }
      }
    },
    "/v1/credential/issue": {
      "post": {
        "tags": ["Issuer"],
        "summary": "Issue a credential",
        "description": "Issuer creates a credential bound to the user's `IdentityCommit`. Publishes the credential commitment (`cred_handle`) to the registry and delivers the full credential to the wallet.",
        "operationId": "issueCredential",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CredentialIssueRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Credential issued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IssuedCredential" }
              }
            }
          }
        }
      }
    },
    "/v1/credential/{cred_handle}/revoke": {
      "post": {
        "tags": ["Issuer"],
        "summary": "Revoke a credential",
        "description": "Issuer revokes a credential. The registry increments its epoch and updates the revocation accumulator. The next checkpoint will reflect the revocation. Any proof generated before revocation will fail with `REVOKED` at the next epoch boundary.",
        "operationId": "revokeCredential",
        "parameters": [
          {
            "name": "cred_handle",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Credential revoked, new epoch published" },
          "404": { "description": "Credential not found" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "RegistryCheckpoint": {
        "type": "object",
        "required": ["root_id", "epoch", "accumulated_at", "sig"],
        "properties": {
          "root_id": { "type": "string", "description": "Unique ID for this checkpoint (hash of accumulator state)", "example": "chk_7f3a2b" },
          "epoch": { "type": "integer", "description": "Monotonically increasing epoch counter", "example": 142 },
          "accumulated_at": { "type": "string", "format": "date-time", "description": "UTC timestamp when this checkpoint was published" },
          "sig": { "type": "string", "description": "Registry signature over (root_id, epoch, accumulated_at)", "example": "ed25519:7f3a2b..." }
        }
      },
      "WitnessBundle": {
        "type": "object",
        "required": ["cred_handle", "root_id", "membership_witness", "revocation_witness"],
        "properties": {
          "cred_handle": { "type": "string" },
          "root_id": { "type": "string" },
          "membership_witness": { "type": "string", "description": "Accumulator membership proof (Merkle path or RSA accumulator witness)" },
          "revocation_witness": { "type": "string", "description": "Non-membership witness proving credential is not in revoked set" }
        }
      },
      "ChallengeRequest": {
        "type": "object",
        "required": ["aud", "action"],
        "properties": {
          "aud": { "type": "string", "description": "Audience — the verifier's domain", "example": "acme-corp.com" },
          "action": { "type": "string", "description": "Action being authorized. Use 'login' for authentication, or a structured string for step-up (e.g. 'transfer:5000:alice')", "example": "login" },
          "exp_seconds": { "type": "integer", "description": "Challenge TTL in seconds. Defaults to 300.", "example": 300 }
        }
      },
      "Challenge": {
        "type": "object",
        "required": ["nonce", "aud", "exp", "root_id", "action"],
        "properties": {
          "nonce": { "type": "string", "description": "One-time random nonce", "example": "nonce_a1b2c3" },
          "aud": { "type": "string", "example": "acme-corp.com" },
          "exp": { "type": "string", "format": "date-time", "description": "Challenge expiry" },
          "root_id": { "type": "string", "description": "Checkpoint root_id at time of challenge issuance" },
          "action": { "type": "string", "example": "login" }
        }
      },
      "ProofSubmission": {
        "type": "object",
        "required": ["proof", "session_pubkey", "nullifier", "root_id", "challenge_nonce"],
        "properties": {
          "proof": { "type": "string", "description": "Serialised qFold proof (base64url)", "example": "qfold:v1:abcdef..." },
          "session_pubkey": { "type": "string", "description": "Ephemeral Ed25519 public key generated by the wallet for this session", "example": "ed25519:pub_1a2b3c..." },
          "nullifier": { "type": "string", "description": "H(\"login\" | cred_handle | nonce | aud) — prevents replay", "example": "null_7f3a2b..." },
          "root_id": { "type": "string", "description": "Checkpoint root_id the proof was generated against" },
          "challenge_nonce": { "type": "string", "description": "Nonce from the challenge that initiated this proof" }
        }
      },
      "ProofAccepted": {
        "type": "object",
        "properties": {
          "valid": { "type": "boolean", "example": true },
          "session_token": { "type": "string", "description": "Opaque session token bound to session_pubkey. Expires per verifier policy." },
          "session_pubkey": { "type": "string", "description": "Echo of the wallet's session public key" }
        }
      },
      "ProofRejected": {
        "type": "object",
        "required": ["valid", "reason_code", "reason_message"],
        "properties": {
          "valid": { "type": "boolean", "example": false },
          "reason_code": {
            "type": "string",
            "enum": ["INVALID_PROOF", "REPLAY", "ROOT_STALE", "REVOKED", "CHALLENGE_EXPIRED", "SCOPE_EXCEEDED"],
            "description": "Machine-readable denial code"
          },
          "reason_message": { "type": "string", "description": "Human-readable explanation" }
        }
      },
      "SessionVerifyRequest": {
        "type": "object",
        "required": ["session_pubkey", "message", "signature"],
        "properties": {
          "session_pubkey": { "type": "string" },
          "message": { "type": "string", "description": "The message that was signed (typically the request payload hash)" },
          "signature": { "type": "string", "description": "Ed25519 signature over message using session private key" }
        }
      },
      "SessionVerifyResult": {
        "type": "object",
        "properties": {
          "valid": { "type": "boolean" },
          "session_pubkey": { "type": "string" }
        }
      },
      "CredentialIssueRequest": {
        "type": "object",
        "required": ["identity_commit", "credential_type"],
        "properties": {
          "identity_commit": { "type": "string", "description": "Poseidon(master_secret) — the user's public identity commitment", "example": "ic_3f7a2b..." },
          "credential_type": { "type": "string", "example": "membership" },
          "claims": {
            "type": "object",
            "description": "Arbitrary issuer-defined claims to embed in the credential (encrypted to identity_commit in production)",
            "additionalProperties": true
          },
          "expiry": { "type": "string", "format": "date-time" }
        }
      },
      "IssuedCredential": {
        "type": "object",
        "properties": {
          "cred_handle": { "type": "string", "description": "Public handle (commitment hash) published to registry" },
          "credential_type": { "type": "string" },
          "expiry": { "type": "string", "format": "date-time" },
          "epoch_issued": { "type": "integer" },
          "root_id_at_issue": { "type": "string" }
        }
      }
    }
  }
}
