{
  "openapi": "3.1.0",
  "info": {
    "title": "MopokeHQ public API",
    "version": "1.0.0",
    "summary": "Read your organisation's fix list, responses, exposures, components and evidence log, and act on them.",
    "description": "Authenticate with an API key created in MopokeHQ under Settings, Integrations: `Authorization: Bearer mpk_api_...`. A key acts as the person who created it, in their organisation, limited to its scopes: `read` for every GET, `write` (which includes read) for the POST endpoints. Writes go through the same rules as the web app and are recorded in the organisation's evidence log. Each key may make 120 requests a minute with bursts of 60; over that the API answers 429 with Retry-After. Lists are newest first and paged with `next_cursor`, except the evidence log, which is in chain order and paged with `after`."
  },
  "servers": [{ "url": "https://app.mopokehq.com/api/public/v1" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getOpenAPI",
        "summary": "This document",
        "security": [],
        "responses": { "200": { "description": "The OpenAPI document", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Who this key acts as",
        "x-scope": "read",
        "responses": {
          "200": {
            "description": "The key, its owner and organisation",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/now": {
      "get": {
        "operationId": "listNowActions",
        "summary": "The fix list, in the order to work it",
        "description": "Actions grouped by fix: one upgrade that closes every open advisory on one package across every component that has it. Tiers: now (P1 or due within 48 hours), week (P2 or due within 7 days), later.",
        "x-scope": "read",
        "parameters": [
          { "name": "tier", "in": "query", "schema": { "type": "string", "enum": ["now", "week", "later"] } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Actions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["actions", "total"],
                  "properties": {
                    "actions": { "type": "array", "items": { "$ref": "#/components/schemas/Action" } },
                    "total": { "type": "integer", "description": "Actions in the tier (or in all tiers) before the limit" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/responses": {
      "get": {
        "operationId": "listResponses",
        "summary": "Responses, newest first",
        "description": "A response is one advisory that affects the organisation, with its priority, due date and status.",
        "x-scope": "read",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["open", "confirm", "resolved", "dismissed"] } },
          { "name": "priority", "in": "query", "schema": { "type": "string", "enum": ["p1", "p2", "p3"] } },
          { "$ref": "#/components/parameters/Limit200" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of responses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["responses", "next_cursor"],
                  "properties": {
                    "responses": { "type": "array", "items": { "$ref": "#/components/schemas/Response" } },
                    "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/responses/{id}": {
      "get": {
        "operationId": "getResponse",
        "summary": "One response with its exposures",
        "x-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The response id, the advisory id (for example GHSA-xxxx-xxxx-xxxx) or one of its aliases (a CVE id)",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "The response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResponseDetail" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/exposures": {
      "get": {
        "operationId": "listExposures",
        "summary": "Exposures, newest first",
        "description": "An exposure is one advisory matched in one place: a deployed environment (env:production, confirmed live) or a branch (branch:main).",
        "x-scope": "read",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["open", "fixed"] } },
          { "name": "response_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
          { "name": "component_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
          { "name": "package", "in": "query", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/Limit500" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of exposures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["exposures", "next_cursor"],
                  "properties": {
                    "exposures": { "type": "array", "items": { "$ref": "#/components/schemas/Exposure" } },
                    "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/components": {
      "get": {
        "operationId": "listComponents",
        "summary": "Components, by name",
        "description": "A component is a repository, or a path in a monorepo, that uploads SBOMs.",
        "x-scope": "read",
        "parameters": [{ "$ref": "#/components/parameters/Limit500" }, { "$ref": "#/components/parameters/Cursor" }],
        "responses": {
          "200": {
            "description": "A page of components",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["components", "next_cursor"],
                  "properties": {
                    "components": { "type": "array", "items": { "$ref": "#/components/schemas/Component" } },
                    "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/evidence": {
      "get": {
        "operationId": "listEvidence",
        "summary": "The evidence log, in chain order",
        "description": "Every record carries its hash and the previous record's hash, so a copy can be checked: changing or removing a record breaks every hash after it.",
        "x-scope": "read",
        "parameters": [
          { "name": "after", "in": "query", "description": "Return records after this sequence number", "schema": { "type": "integer", "minimum": 0, "default": 0 } },
          { "name": "kind", "in": "query", "description": "Only kinds starting with this, for example response. or plugin.", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Evidence records",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["events", "next_after"],
                  "properties": {
                    "events": { "type": "array", "items": { "$ref": "#/components/schemas/EvidenceEvent" } },
                    "next_after": { "type": "integer", "description": "Pass as after for the next page; 0 when this was the last page" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/search": {
      "get": {
        "operationId": "search",
        "summary": "Search actions, advisories and components",
        "x-scope": "read",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "minLength": 2, "maxLength": 200 } },
          { "name": "limit", "in": "query", "description": "Per kind", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 } }
        ],
        "responses": {
          "200": { "description": "Matches", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/acknowledge": {
      "post": {
        "operationId": "acknowledge",
        "summary": "Acknowledge responses",
        "description": "Takes open responses on as the key's owner, which stops their P1 escalation. Responses already acknowledged or no longer open are left alone. Each acknowledgement is evidence.",
        "x-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["response_ids"],
                "properties": { "response_ids": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "type": "string", "format": "uuid" } } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The responses acknowledged by this call",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["acknowledged"],
                  "properties": { "acknowledged": { "type": "array", "items": { "type": "string", "format": "uuid" } } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/decisions": {
      "post": {
        "operationId": "decide",
        "summary": "Mark responses not affected, or accept the risk",
        "description": "not_affected needs a justification (OpenVEX codes, plus false_positive); accepted needs a reason and a review date within a year. Dismissing a P1 needs an owner or admin. Each decision is evidence and exports as a VEX statement.",
        "x-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["response_ids", "decision"],
                "properties": {
                  "response_ids": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "type": "string", "format": "uuid" } },
                  "decision": { "type": "string", "enum": ["not_affected", "accepted"] },
                  "justification": {
                    "type": "string",
                    "enum": [
                      "component_not_present",
                      "vulnerable_code_not_in_execute_path",
                      "vulnerable_code_cannot_be_controlled_by_adversary",
                      "inline_mitigations_already_exist",
                      "false_positive"
                    ]
                  },
                  "reason": { "type": "string", "maxLength": 1000 },
                  "review_on": { "type": "string", "format": "date", "description": "accepted only" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The responses decided",
            "content": {
              "application/json": {
                "schema": { "type": "object", "required": ["decided"], "properties": { "decided": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/fixes": {
      "post": {
        "operationId": "requestFix",
        "summary": "Request upgrade merge requests for one action",
        "description": "Queues one upgrade merge request per component in the action, at the action's fix version. MopokeHQ holds no repository credentials: the organisation's scheduled job claims the requests and opens the merge requests, usually within 15 minutes. Asking again re-queues only the ones that failed or found nothing to change.",
        "x-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["action_key"], "properties": { "action_key": { "type": "string", "description": "An action's key from /now" } } }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The action's fixes as they now stand",
            "content": {
              "application/json": {
                "schema": { "type": "object", "required": ["fixes"], "properties": { "fixes": { "type": "array", "items": { "$ref": "#/components/schemas/Fix" } } } }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "mpk_api_...", "description": "A MopokeHQ API key" }
    },
    "parameters": {
      "Cursor": { "name": "cursor", "in": "query", "description": "next_cursor from the previous page", "schema": { "type": "string" } },
      "Limit200": { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
      "Limit500": { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }
    },
    "responses": {
      "BadRequest": { "description": "The request is malformed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": {
        "description": "No key, or the key is unknown, revoked or expired (error: api_key_required, invalid_api_key, api_key_expired)",
        "headers": { "WWW-Authenticate": { "schema": { "type": "string" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "The key lacks the scope (insufficient_scope), its owner left the organisation (not_a_member), or the owner's role does not allow it",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": { "description": "Not found in this organisation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "Nothing to do in the current state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": {
        "description": "Over the key's rate limit",
        "headers": { "Retry-After": { "description": "Seconds to wait", "schema": { "type": "integer" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" }, "message": { "type": "string" } }
      },
      "NextCursor": { "type": "string", "description": "Pass as cursor for the next page; empty on the last page" },
      "Me": {
        "type": "object",
        "required": ["key", "user", "role", "organisation"],
        "properties": {
          "key": {
            "type": "object",
            "properties": { "id": { "type": "string" }, "scopes": { "type": "array", "items": { "type": "string", "enum": ["read", "write"] } } }
          },
          "user": { "type": "object", "properties": { "id": { "type": "string" }, "email": { "type": "string" }, "name": { "type": "string" } } },
          "role": { "type": "string", "enum": ["owner", "admin", "member"] },
          "organisation": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }
        }
      },
      "Action": {
        "type": "object",
        "required": ["key", "title", "ecosystem", "package", "fix_version", "tier", "priority", "due_at", "advisories", "components", "fixes"],
        "properties": {
          "key": { "type": "string", "description": "ecosystem:package:installed release line" },
          "title": { "type": "string", "examples": ["Upgrade axios to 1.8.2"] },
          "ecosystem": { "type": "string" },
          "package": { "type": "string" },
          "fix_version": { "type": "string", "description": "Empty when there is no fixed version yet" },
          "tier": { "type": "string", "enum": ["now", "week", "later"] },
          "priority": { "type": "string", "enum": ["p1", "p2", "p3"] },
          "severity": { "type": "string" },
          "due_at": { "type": "string", "format": "date-time" },
          "confidence": { "type": "string", "enum": ["verified", "likely", "possible"] },
          "why": { "type": "array", "items": { "type": "string" } },
          "projects": { "type": "integer" },
          "unacknowledged": { "type": "integer", "description": "P1 responses in this action nobody has acknowledged" },
          "advisories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "response_id": { "type": "string" },
                "id": { "type": "string" },
                "aliases": { "type": "array", "items": { "type": "string" } },
                "summary": { "type": "string" },
                "severity": { "type": "string" },
                "url": { "type": "string" },
                "priority": { "type": "string" },
                "due_at": { "type": "string", "format": "date-time" },
                "kev": { "type": "boolean" },
                "epss": { "type": "number" },
                "why": { "type": "array", "items": { "type": "string" } },
                "ai_note": { "type": ["object", "null"] },
                "acknowledged_at": { "type": ["string", "null"], "format": "date-time" },
                "acknowledged_by": { "type": "string" }
              }
            }
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "name": { "type": "string" },
                "project": { "type": "string" },
                "scopes": { "type": "array", "items": { "type": "string" } },
                "installed": { "type": "array", "items": { "type": "string" } },
                "confidence": { "type": "string" }
              }
            }
          },
          "fixes": { "type": "array", "items": { "$ref": "#/components/schemas/Fix" } }
        }
      },
      "Fix": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "component_id": { "type": "string" },
          "component": { "type": "string" },
          "status": { "type": "string", "enum": ["queued", "claimed", "opened", "unchanged", "failed"] },
          "mr_url": { "type": "string" },
          "branch": { "type": "string" },
          "message": { "type": "string" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Response": {
        "type": "object",
        "required": ["id", "advisory_id", "priority", "status", "opened_at", "due_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "advisory_id": { "type": "string" },
          "aliases": { "type": "array", "items": { "type": "string" } },
          "summary": { "type": "string" },
          "severity": { "type": "string" },
          "priority": { "type": "string", "enum": ["p1", "p2", "p3"] },
          "status": { "type": "string", "enum": ["open", "confirm", "resolved", "dismissed"] },
          "opened_at": { "type": "string", "format": "date-time" },
          "due_at": { "type": "string", "format": "date-time" },
          "resolved_at": { "type": ["string", "null"], "format": "date-time" },
          "acknowledged_at": { "type": ["string", "null"], "format": "date-time" },
          "open_exposures": { "type": "integer" },
          "components": { "type": "integer" }
        }
      },
      "ResponseDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/Response" },
          {
            "type": "object",
            "properties": {
              "details": { "type": "string" },
              "url": { "type": "string" },
              "cvss_score": { "type": ["number", "null"] },
              "cvss_vector": { "type": "string" },
              "kev": { "type": "boolean" },
              "epss": { "type": "number" },
              "why": { "type": "array", "items": { "type": "string" } },
              "ai_note": { "type": ["object", "null"], "description": "Lilly's note: a summary and, when available, the calibrated odds it applies" },
              "exposures": { "type": "array", "items": { "$ref": "#/components/schemas/Exposure" } }
            }
          }
        ]
      },
      "Exposure": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "response_id": { "type": "string", "format": "uuid" },
          "advisory_id": { "type": "string" },
          "component_id": { "type": "string", "format": "uuid" },
          "component": { "type": "string" },
          "project": { "type": "string" },
          "scope": { "type": "string", "examples": ["env:production", "branch:main"] },
          "confidence": { "type": "string", "enum": ["verified", "likely", "possible"] },
          "ecosystem": { "type": "string" },
          "package": { "type": "string" },
          "installed_version": { "type": "string" },
          "fixed_version": { "type": "string" },
          "status": { "type": "string", "enum": ["open", "fixed"] },
          "first_seen": { "type": "string", "format": "date-time" },
          "last_seen": { "type": "string", "format": "date-time" },
          "fixed_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Component": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "project_id": { "type": "string", "format": "uuid" },
          "project": { "type": "string" },
          "source": { "type": "string", "description": "Repository path, for example group/project" },
          "path": { "type": "string", "description": "Subdirectory in a monorepo" },
          "repo_url": { "type": "string" },
          "default_branch": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "last_upload_at": { "type": ["string", "null"], "format": "date-time" },
          "open_exposures": { "type": "integer" }
        }
      },
      "EvidenceEvent": {
        "type": "object",
        "properties": {
          "seq": { "type": "integer" },
          "occurred_at": { "type": "string", "format": "date-time" },
          "kind": { "type": "string", "examples": ["response.acknowledged", "plugin.evidence_note"] },
          "actor": { "type": "string", "examples": ["user:...", "mopoke", "plugin:acme.notes"] },
          "subject": { "type": "string" },
          "payload": { "type": "object" },
          "hash": { "type": "string", "description": "SHA-256, hex" },
          "prev_hash": { "type": "string", "description": "The previous record's hash, hex" }
        }
      },
      "SearchResult": {
        "type": "object",
        "required": ["actions", "advisories", "components"],
        "properties": {
          "actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "key": { "type": "string" }, "title": { "type": "string" }, "tier": { "type": "string" }, "priority": { "type": "string" } }
            }
          },
          "advisories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "response_id": { "type": "string" },
                "id": { "type": "string" },
                "aliases": { "type": "array", "items": { "type": "string" } },
                "summary": { "type": "string" },
                "priority": { "type": "string" },
                "status": { "type": "string" }
              }
            }
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "project": { "type": "string" }, "source": { "type": "string" } }
            }
          }
        }
      }
    }
  }
}
