openapi: 3.0.3 info: title: Keploy Public API version: 1.0.0 description: | Programmatic access to the Keploy platform for CI/CD pipelines, scripts, and AI agents. See the full guide at . **Scopes and 403 errors:** Every endpoint requires a minimum scope (`read`, `write`, or `admin`). If the API key lacks the required scope the server returns `403 Forbidden` with error code `INSUFFICIENT_SCOPE`. contact: email: support@keploy.io termsOfService: https://keploy.io/terms servers: - url: https://api.keploy.io/client/v1 description: Production - url: https://api.staging.keploy.io/client/v1 description: Staging security: - apiKeyAuth: [] tags: - name: Apps - name: Clusters - name: Recordings - name: Test Reports - name: Test Suites - name: Test Runs - name: Jobs - name: Load Tests - name: Generation History - name: Company - name: Users - name: API Keys paths: # ── Apps ────────────────────────────────────────────────────────────── /apps: get: operationId: listApps x-required-scope: read summary: List apps description: "Returns the tenant's apps. Use the optional `q` query parameter to name-filter (case-insensitive substring, e.g. `?q=orderflow` → apps whose name contains 'orderflow'); without it the full paginated list is returned. Callers that know the app's folder / repo name should pass it as `q` to avoid paginating through hundreds of apps. Requires scope: `read`." tags: [Apps] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" - in: query name: q description: "Case-insensitive substring to filter app names by. Omit to list all apps." required: false schema: type: string responses: "200": description: App list content: application/json: schema: $ref: "#/components/schemas/EnvelopeAppList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } post: operationId: createApp x-required-scope: write summary: Create an app description: "Requires scope: `write`." tags: [Apps] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateAppRequest" responses: "201": description: App created content: application/json: schema: $ref: "#/components/schemas/EnvelopeApp" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": description: App already exists content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}: parameters: - $ref: "#/components/parameters/appId" get: operationId: getApp x-required-scope: read summary: Get an app description: | Requires scope: `read`. Optional `fields` query parameter projects the response to a subset of properties — useful for MCP / AI callers that only need a few identity fields (e.g. `["name","namespace","deployment","origin.clusterName"]`) and don't want the full ~16k-token embedded schema in their context. Supports dotted paths for nested objects. Omitting `fields` returns the full envelope as before. tags: [Apps] parameters: - name: fields in: query required: false description: | Optional comma-separated list of response field paths to keep. Each path is dotted (e.g. `origin.clusterName`). When set, the response is projected to just those paths inside `data`; the envelope shape (`{data, meta?}`) is preserved. schema: type: array items: { type: string } style: form explode: false responses: "200": description: App detail content: application/json: schema: $ref: "#/components/schemas/EnvelopeApp" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } put: operationId: updateApp x-required-scope: write summary: Update an app description: "Requires scope: `write`." tags: [Apps] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateAppRequest" responses: "200": description: App updated content: application/json: schema: $ref: "#/components/schemas/EnvelopeApp" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } delete: operationId: deleteApp x-required-scope: admin summary: Delete an app description: "Requires scope: `admin`." tags: [Apps] responses: "200": description: App deleted content: application/json: schema: $ref: "#/components/schemas/EnvelopeDeleted" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Schema Coverage ─────────────────────────────────────────────────── /apps/{appId}/schema-coverage: parameters: - $ref: "#/components/parameters/appId" get: operationId: getSchemaCoverage x-required-scope: read summary: Get schema coverage description: "Requires scope: `read`." tags: [Apps] responses: "200": description: Schema coverage data content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Clusters ───────────────────────────────────────────────────────── /clusters: get: operationId: listClusters summary: List clusters description: "Returns all clusters for the authenticated company. Requires scope: `read`." x-required-scope: read tags: [Clusters] responses: "200": description: Cluster list content: application/json: schema: $ref: "#/components/schemas/EnvelopeClusterList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } post: operationId: createCluster summary: Create a cluster description: "Provisions a new cluster in the authenticated company and returns its access key. The access key is shown only once. Requires scope: `admin`." x-required-scope: admin tags: [Clusters] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateClusterRequest" responses: "201": description: Cluster created (includes access key, shown only once) content: application/json: schema: $ref: "#/components/schemas/EnvelopeClusterCreated" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": { $ref: "#/components/responses/Conflict" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/by-cluster/{clusterId}: parameters: - $ref: "#/components/parameters/clusterId" get: operationId: listAppsByCluster summary: List apps in a cluster description: "Returns apps belonging to a specific cluster. More efficient than iterating all apps. Requires scope: `read`." x-required-scope: read tags: [Clusters] responses: "200": description: Apps in cluster content: application/json: schema: $ref: "#/components/schemas/EnvelopeAppByClusterList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Recordings (integration test recordings from k8s-proxy) ────────── /apps/with-recordings: get: operationId: listAppsWithRecordings summary: List proxy apps with network recordings description: "Returns all k8s-proxy apps (origin.type=PROXY). These apps are auto-created by the Keploy k8s-proxy agent on first recording and contain network recordings of ingress HTTP traffic (as Keploy test cases) and egress dependency calls — database queries, external API calls, message queues — captured as Keploy mocks. Use listRecordings and getRecording to access the recorded request/response pairs and dependency mocks from live environments. Requires scope: `read`." x-required-scope: read tags: [Recordings] responses: "200": description: Apps with recordings content: application/json: schema: $ref: "#/components/schemas/EnvelopeAppWithRecordingsList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings: parameters: - $ref: "#/components/parameters/appId" get: operationId: listRecordings summary: List recording sessions description: "Returns test sets (recording sessions) for an app. Requires scope: `read`." x-required-scope: read tags: [Recordings] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" responses: "200": description: Recording sessions content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestSetList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" get: operationId: getRecording summary: Get recorded test cases description: "Returns individual recorded test cases within a test set, including HTTP request/response data. Requires scope: `read`." x-required-scope: read tags: [Recordings] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" responses: "200": description: Recorded test cases content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestCaseList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/generated-schema: parameters: - $ref: "#/components/parameters/appId" get: operationId: getGeneratedSchema summary: Get auto-generated OpenAPI schema description: "Returns the OpenAPI schema auto-generated from recorded traffic. Requires scope: `read`." x-required-scope: read tags: [Recordings] responses: "200": description: Generated schema content: application/json: schema: $ref: "#/components/schemas/EnvelopeGeneratedSchema" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/mocks: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" get: operationId: listMocks summary: List mocks for a recording description: "Returns mock reference metadata and optionally parsed mock specs for a test set. Use ?include_specs=true to download and parse the actual mock YAML from object storage. Pass `branch_id` to scope the read to a branch overlay (bundle-uploaded recordings on a branch are invisible to main reads). Requires scope: `read`." x-required-scope: read tags: [Recordings] parameters: - name: include_specs in: query schema: type: boolean default: false description: "When true, download and parse the actual mock YAML specs from object storage." - $ref: "#/components/parameters/branchId" responses: "200": description: Mock reference metadata and optionally parsed specs content: application/json: schema: $ref: "#/components/schemas/EnvelopeMockList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } post: operationId: createMock summary: Author one mock under a recording description: | Insert a single mock into the given test set. When `branch_id` is supplied, the mock lands on that branch's overlay (`branch_sandbox_ops`) and only surfaces to main on merge. Without `branch_id` the mock writes straight to main — same behaviour as the recording-driven agent path. Authoring shape — pick ONE: - **`mock_yaml`** (PREFERRED) — paste the canonical mock YAML envelope (`version` / `kind` / `name` / `spec` with the per-kind payload, exactly as it lives in `mocks.yaml` on disk). The server decodes via OSS DecodeMocks so kind- specific Spec contents (`req`, `resp`, `metadata`, …) round-trip without field-name loss. This is the only path that preserves payloads pasted from existing mocks. - **`mock`** — typed OSS Mock JSON object. Brittle: the OSS struct uses PascalCase JSON tags (`Metadata`, `Req`, `Res`), so lowercase canonical keys are silently dropped. Use only when authoring programmatically from typed Go shapes. When both are sent, `mock_yaml` wins. Requires scope: `write`. x-required-scope: write tags: [Recordings] requestBody: required: true content: application/json: schema: type: object # mock is no longer required at the schema level — mock_yaml # is the preferred path and either one suffices. The service # rejects requests that supply neither. properties: mock: type: object description: OSS Mock — see schema in keploy.io/server/v3 pkg/models/mock.go. Use only when authoring from typed Go shapes. Lowercase YAML keys are dropped; prefer mock_yaml. mock_yaml: type: string description: Canonical single-doc mock YAML (version/kind/name/spec). Preferred over `mock`. Round-trips kind-specific contents losslessly. branch_id: type: string description: Optional branch overlay id. Absent → write lands on main. responses: "200": description: Inserted mock content: application/json: schema: $ref: "#/components/schemas/EnvelopeMockSingle" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/mocks/{mockId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" - name: mockId in: path required: true schema: { type: string } description: Mock id (UUID assigned at insert) OR human-readable mock name. The server resolves names within the test set, so the UI can pass either form. get: operationId: getMock summary: Read one mock's canonical YAML description: | Returns the canonical mock YAML doc (version/kind/name/spec) for the named mock in the given test set. Branch-aware: when `branch_id` is supplied, a branch-only upsert or tombstone takes precedence over main. Authoring workflow for AI agents: call this BEFORE updateMock to fetch the existing payload, edit fields locally, then round-trip the result through `mock_yaml` on updateMock. Requires scope: `read`. x-required-scope: read tags: [Recordings] parameters: - name: branch_id in: query schema: { type: string } description: Optional branch overlay id. Absent → reads from main. responses: "200": description: Canonical mock YAML content: application/json: schema: type: object required: [success, metadata, mockName] properties: success: { type: boolean } metadata: type: string description: Canonical single-doc mock YAML. mockName: type: string description: Resolved mock name (mirrors the path param when caller passed an id). "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } put: operationId: updateMock summary: Replace one mock's stored data description: | Updates the mock identified by `{mockId}` in the path. The body carries the full replacement. Two shapes — pick ONE: - **`mock_yaml`** (PREFERRED) — canonical mock YAML envelope. See createMock for the field-loss rationale. - **`mock`** — typed OSS Mock JSON. Brittle for lowercase keys. When both are sent, `mock_yaml` wins. Branch-aware via the optional `branch_id` body field — same semantics as createMock. Accepts both the mock's UUID `_id` and its human-readable Name as `mockId` — the server resolves names within the test set, so UI callers (which don't have access to the mock's `_id`) can pass the Name directly. Requires scope: `write`. x-required-scope: write tags: [Recordings] requestBody: required: true content: application/json: schema: type: object properties: mock: type: object description: OSS Mock — typed full replacement payload. Prefer mock_yaml. mock_yaml: type: string description: Canonical single-doc mock YAML. Preferred path. branch_id: type: string responses: "200": description: Updated mock content: application/json: schema: $ref: "#/components/schemas/EnvelopeMockSingle" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } delete: operationId: deleteMock summary: Drop one mock description: | Idempotent — returns 200 even if the mock is already gone. Path `{mockId}` accepts both the UUID `_id` and the human-readable Name (resolved within the test set). Branch-aware via optional `branch_id` query param. Requires scope: `write`. x-required-scope: write tags: [Recordings] parameters: - name: branch_id in: query schema: { type: string } description: | Optional branch overlay id. When set, the delete writes a tombstone op onto the branch's overlay (main untouched until the branch merges). When absent, the delete applies to main directly — no tombstone is involved. responses: "200": description: Deleted (or already absent) content: application/json: schema: $ref: "#/components/schemas/EnvelopeSuccess" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/test-cases/{testCaseId}/mock-mapping: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" - name: testCaseId in: path required: true schema: { type: string } description: Test case name (the mapping doc keys cases by name, not _id). get: operationId: getMockMapping summary: Read the mocks currently linked to a test case description: | Returns the mock entries in the mapping doc for the named test case. Branch-aware via `branch_id` — when set, the branch overlay's mapping wins over main's. Workflow: AI agents should call this BEFORE editMockMapping to inspect what's linked, then issue targeted add / remove ops with confidence. Empty result (mocks: []) is normal — means no mocks linked yet. Requires scope: `read`. x-required-scope: read tags: [Recordings] parameters: - name: branch_id in: query schema: { type: string } description: Optional branch overlay id. Absent → reads from main. responses: "200": description: Linked mock entries for this test case content: application/json: schema: type: object required: [success, mocks] properties: success: { type: boolean } mocks: type: array description: Mock entries — empty array when nothing is linked. items: type: object properties: name: { type: string } kind: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } post: operationId: editMockMapping summary: Link or unlink a mock from a test case description: | Targeted mutation of the test case's entry in the mapping doc. Add appends a mock entry if not already present; remove drops the entry by name. Both idempotent — safe to retry on a network blip. The MCP layer exposes this as TWO tools (`link_mock` / `unlink_mock`) — they both call this endpoint with the appropriate `action`. Splitting at the MCP layer keeps each tool's description tighter and avoids the LLM having to remember the enum spelling. Requires scope: `write`. x-required-scope: write tags: [Recordings] requestBody: required: true content: application/json: schema: type: object required: [action, mock_name] properties: action: type: string enum: [add, remove] mock_name: type: string mock_kind: type: string description: Optional. Stamped on the mapping entry; useful when callers want the kind preserved on the mapping doc for downstream readers. branch_id: type: string responses: "200": description: Updated mapping content: application/json: schema: $ref: "#/components/schemas/EnvelopeMappingEdit" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/bundle: parameters: - $ref: "#/components/parameters/appId" post: operationId: uploadRecordingBundle summary: Atomic test set + cases + mocks + mappings ingest description: | Bundle ingest — creates the test set, every test case, every mock, and the mapping doc in a single call. Each step is its own DB write; partial failure leaves earlier rows in place, callers can replay safely. Branch-aware via optional `branch_id` — when set, every row lands on the overlay until merge. Use this when authoring a recording from scratch (LLM workflows, CLI imports). For incremental edits, prefer the per-resource endpoints (`createMock`, `createTestCase`, etc.). Requires scope: `write`. x-required-scope: write tags: [Recordings] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string description: Test set name (unique within the app/branch). image_name: type: string image_tag: type: string branch_id: type: string smart_set: type: boolean description: > Route the bundle into the smart test set as a branch import (new schema_refs only) instead of creating a legacy test set. Requires branch_id. The CLI sets this only when --smartTestSet is passed and the app has EnableSmartTestSet. test_cases: type: array items: type: object required: [name, http_req, http_resp] properties: name: { type: string } http_req: { type: object } http_resp: { type: object } noise: type: object additionalProperties: type: array items: { type: string } mock_names: type: array items: { type: string } mocks: type: array description: | Per-mock authoring entries. For each entry, prefer `mock_yaml` (canonical envelope as it lives in mocks.yaml on disk) over `spec`. The typed `spec` object hits the same lowercase-key-drop bug documented on createMock when the input came from on-disk YAML. When both are set on an entry, `mock_yaml` wins. items: type: object # name + kind remain authoritative for the index even # if `mock_yaml` carries its own — the server stamps # these onto the canonical doc post-decode so the # bundle entry is unambiguous. required: [name, kind] properties: name: { type: string } kind: { type: string } spec: type: object description: Typed OSS Mock spec. Prefer mock_yaml. mock_yaml: type: string description: Canonical single-doc mock YAML for this entry. responses: "200": description: Bundle ingest result content: application/json: schema: $ref: "#/components/schemas/EnvelopeBundleUpload" "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" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/test-cases/{testCaseId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" - name: testCaseId in: path required: true schema: { type: string } get: operationId: getTestCase summary: Get a single test case description: "Returns a single recorded test case identified by its friendly **name** (e.g. `test-4` — the name in the recording yaml) within a recording session. Within `(testSetId, branchId)` the name is unambiguous; this is the same identifier callers see in the on-disk recording bundle. Pass `branch_id` to scope the read to a branch overlay (bundle-uploaded test cases on a branch are invisible to main reads). Requires scope: `read`." x-required-scope: read tags: [Recordings] parameters: - $ref: "#/components/parameters/branchId" responses: "200": description: Test case details content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestCase" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } put: operationId: updateTestCase summary: Update a test case description: "Update mutable fields of a recorded test case identified by its friendly **name** (e.g. `test-4` — the name in the recording yaml) within `(testSetId, branchId)`. The body can carry `name` / `http_req` / `http_resp` (response-edit sub-action) AND/OR `noise` (noise sub-action — a replace-style `path → match-substrings` map for non-deterministic fields). Both Case-2a sub-actions documented in the LLM workflow are handled by this single endpoint. Pass `branch_id` to scope the edit to a branch overlay (bundle-uploaded test cases on a branch are invisible to main writes). Requires scope: `write`." x-required-scope: write tags: [Recordings] parameters: - $ref: "#/components/parameters/branchId" requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } http_req: { type: object } http_resp: { type: object } noise: type: object nullable: true description: "Replace-style noise map. Keys are JSON paths (`body.foo.bar` / `header.X-Trace`), values are arrays of match-substrings (empty array means \"ignore this field entirely on diff\"). To merge with existing noise, do GET → modify → PUT. Nullability semantics: **omit the field** (or send JSON `null`) to preserve the existing noise unchanged; **send `{}`** to clear all noise; **send a populated map** to replace. Sending `null` and omitting are equivalent at the server." additionalProperties: type: array items: { type: string } example: body.trace_id: [] header.X-Request-Id: [] branch_id: type: string description: Optional branch overlay id (body alternative to the query param). Either form is accepted; query wins when both are set. responses: "200": description: Updated test case content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestCase" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/export: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" get: operationId: exportRecording summary: Export a recording bundle description: "Export a complete recording bundle: test set metadata, all test cases, mocks, and test-to-mock mappings as a single JSON response. Use ?include_mocks=false to exclude mocks. Requires scope: `read`." x-required-scope: read tags: [Recordings] parameters: - name: include_mocks in: query schema: type: boolean default: true description: "Include dependency mocks in the export (default true)." responses: "200": description: Recording bundle content: application/json: schema: $ref: "#/components/schemas/EnvelopeRecordingExport" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/recordings/{testSetId}/import: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/testSetId" post: operationId: importRecording summary: Import test case changes into a recording description: "Bulk import test case changes: update existing test cases (by ID), insert new ones (without ID), and delete specified test cases. Pass `branch_id` (query or body) to scope the import to a branch overlay. Requires scope: `write`." x-required-scope: write tags: [Recordings] parameters: - $ref: "#/components/parameters/branchId" requestBody: required: true content: application/json: schema: type: object properties: test_cases: type: array items: type: object properties: id: { type: string, description: "Existing test case ID for update; omit for insert" } name: { type: string } http_req: { type: object } http_resp: { type: object } delete_test_case_ids: type: array items: { type: string } description: "IDs of test cases to delete" branch_id: type: string description: Optional branch overlay id (body alternative to the query param). Either form is accepted; query wins. responses: "200": description: Import result content: application/json: schema: $ref: "#/components/schemas/EnvelopeRecordingImportResult" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } # ── Test Reports (integration test run results from k8s-proxy) ─────── /apps/{appId}/test-reports: parameters: - $ref: "#/components/parameters/appId" get: operationId: listTestReports summary: List test run reports description: | Browse legacy /tr test runs for an app. Combine `status` + `source` to scope to specific run kinds — e.g. `status=FAILED` + `source=ci` shows broken CI runs only. Use `branch_id` to scope to a Keploy branch overlay, or `all_branches=true` to list runs from every branch alongside main's (without it, a call with no `branch_id` returns main's runs only). Use `getTestReportFull` for the inflated single-call view with per-test-case diffs + mock mismatches. CANONICAL ONE-SHOT CALL for "find the latest failed local cloud replay" (Phase A1 of the skill): listTestReports({appId, branch_id, status: "FAILED", limit: 5}) Note: `status` is CASE-SENSITIVE. Use the exact enum values (`FAILED`, `PASSED`, `RUNNING`, `PENDING`, `IGNORED`, `OBSOLETE`). Results are sorted newest-first by `created_at` — the most recent run is `data[0]`. ONE call is enough; if `data` is empty DO NOT retry with different status / source / branch_id permutations — the run genuinely doesn't exist on this branch and the dev needs to re-run `keploy cloud replay` first. Requires scope: `read`. x-required-scope: read tags: [Test Reports] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" - name: branch_id in: query required: false schema: { type: string } - name: all_branches in: query required: false schema: { type: boolean } - name: source in: query required: false schema: type: string enum: [ci, manual] - name: status in: query required: false schema: type: string enum: [PASSED, FAILED, RUNNING, PENDING, IGNORED, OBSOLETE] - name: since in: query required: false schema: { type: integer, format: int64 } - name: until in: query required: false schema: { type: integer, format: int64 } responses: "200": description: Test run reports content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestRunReportList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-reports/{reportId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/reportId" get: operationId: getTestReport summary: Get a test run report description: | Returns the rollup view of a single test run by ID — counts, CI metadata, normalize/coverage data. Cheap and lightweight. For per-test-case diffs and mock mismatches use `getTestReportFull`. Requires scope: `read`. x-required-scope: read tags: [Test Reports] responses: "200": description: Test run report content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestRunReport" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-reports/{reportId}/full: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/reportId" get: operationId: getTestReportFull summary: Get a fully-inflated test run report description: | Returns everything about a single legacy /tr test run in one call: the rollup (counts, CI metadata, normalize/coverage data), every test-set report, and — when `include_oss_report=true` (default) — every per-test-case result with request/response diff and `mock_mismatches`. Designed for AI / MCP analysis so the model can ground answers in actual diffs without chaining listTestSetReports + listTestCaseReports calls. Use `mock_mismatches_only=true` to restrict per-test-case rows to those with non-empty `mock_mismatches` OR the legacy `failure_info.mock_mismatch` fallback (for reports produced by replayers older than v3.5.49 that pre-date the top-level `mock_mismatches` field). Use `failed_only=true` to restrict per-test-case rows to those whose `status` is `FAILED` — covers all failure modes (response divergence, schema delta, mock mismatch). `failed_only` and `mock_mismatches_only` AND together: passing both keeps only tests that are both FAILED and have mock mismatches. Use `max_test_cases_per_set` to cap response size on large suites; `truncated.test_cases_dropped` reports how many were elided. Requires scope: `read`. x-required-scope: read tags: [Test Reports] parameters: - name: include_oss_report in: query required: false schema: { type: boolean, default: true } - name: mock_mismatches_only in: query required: false schema: { type: boolean, default: false } - name: failed_only in: query required: false description: | When `true`, drops every test case whose `status` is not `FAILED` (passed/skipped/pending/running are elided). Use this to slim the report down to just the failures the model needs to analyze — typically 1–5 cases out of 50. Combine with `fields` for the smallest possible response. Combines with `mock_mismatches_only` via AND. schema: { type: boolean, default: false } - name: max_test_cases_per_set in: query required: false schema: { type: integer, minimum: 1, maximum: 1000, default: 100 } - name: fields in: query required: false description: | Optional comma-separated list of response field paths to keep inside `data`. Supports dotted nested paths and `[].` for array wildcards (e.g. `failed_steps[].diff`, `mock_mismatches`). Typical AI use: `?fields=failed_steps[].diff,mock_mismatches` — projects the ~34k-token full report down to ~5k while preserving the diff content the model needs to ground its analysis. Omitting `fields` returns the full report as before. schema: type: array items: { type: string } style: form explode: false responses: "200": description: Fully inflated test run report content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestRunReportFull" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-reports/{reportId}/test-set-reports: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/reportId" get: operationId: listTestSetReports summary: List test set reports within a run description: "Returns per-test-set results within a test run report. Requires scope: `read`." x-required-scope: read tags: [Test Reports] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" responses: "200": description: Test set reports content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestSetReportList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-reports/{reportId}/test-set-reports/{testSetReportId}/test-cases: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/reportId" - $ref: "#/components/parameters/testSetReportId" get: operationId: listTestCaseReports summary: List test case reports description: "Returns individual test case results with expected/actual diffs within a test set report. Requires scope: `read`." x-required-scope: read tags: [Test Reports] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" responses: "200": description: Test case reports content: application/json: schema: $ref: "#/components/schemas/EnvelopeTestCaseReportList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Test Suites ─────────────────────────────────────────────────────── /apps/{appId}/test-suites: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/branchId" get: operationId: listTestSuites x-required-scope: read summary: List test suites description: "List test suites for an app. Optional `has_sandbox_test` query param filters by sandbox-test linkage: `true` returns only suites that have a sandbox test (linked=true / test_set_id populated); `false` returns only suites without one. Omit to return every suite. Requires scope: `read`. Supports cursor-based pagination." tags: [Test Suites] parameters: - name: page_size in: query schema: type: integer minimum: 1 description: Number of items per page - name: after in: query schema: { type: string } description: Cursor for forward pagination (mutually exclusive with `before`) - name: before in: query schema: { type: string } description: Cursor for backward pagination (mutually exclusive with `after`) - name: has_sandbox_test in: query required: false schema: type: string enum: ["true", "false"] description: "Filter by sandbox-test linkage. Omit to return every suite." - name: q in: query required: false schema: { type: string } description: "Substring / regex match on suite name (server-side regex filter). Use for bounded duplicate-checks on large apps so MCP doesn't have to paginate the whole list." responses: "200": description: Paginated test suites content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } post: operationId: createTestSuite x-required-scope: write summary: Create a test suite description: "Requires scope: `write`." tags: [Test Suites] parameters: - name: X-Keploy-Validator-Version in: header required: false description: | Optional. The enterprise binary stamps the rule-set version it pre-validated the suite against. The api-server compares this against its own rule set and rejects with 426 if they disagree so the user gets an explicit upgrade message instead of a silently-accepted suite that fails newer rules at run time. schema: type: string requestBody: required: true content: application/json: schema: type: object responses: "201": description: Test suite created content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": { $ref: "#/components/responses/Conflict" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "426": { $ref: "#/components/responses/UpgradeRequired" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-suites/generate: parameters: - $ref: "#/components/parameters/appId" post: operationId: generateTestSuites x-required-scope: write summary: Generate test suites via AI description: "Requires scope: `write`." tags: [Test Suites] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/GenerateTestSuitesRequest" responses: "202": description: Generation job accepted content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-suites/run: parameters: - $ref: "#/components/parameters/appId" post: operationId: runTestSuites x-required-scope: write summary: Run test suites description: "Run test suites against a PUBLIC target URL. DO NOT use for local-app / localhost runs — base_url must be reachable from the SaaS backend (rejects loopback / private IPs as 400 'invalid baseURL'). For localhost runs use the MCP tool record_sandbox_test (keploy agent). Optional sandbox_mode field: \"\"|\"rerecord\"|\"integration_test\" — the sandbox modes are primarily used through MCP's record_sandbox_test / replay_sandbox_test tools. Requires scope: `write`." tags: [Test Suites] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RunTestSuitesRequest" responses: "202": description: Test run started content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-suites/bulk-delete: parameters: - $ref: "#/components/parameters/appId" post: operationId: bulkDeleteTestSuites x-required-scope: write summary: Bulk-delete test suites description: "Requires scope: `write`." tags: [Test Suites] requestBody: required: true content: application/json: schema: type: object properties: test_suite_ids: type: array items: { type: string } required: [test_suite_ids] responses: "200": description: Suites deleted content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-suites/{suiteId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/suiteId" - $ref: "#/components/parameters/branchId" get: operationId: getTestSuite x-required-scope: read summary: Get a test suite description: "Requires scope: `read`." tags: [Test Suites] responses: "200": description: Test suite detail content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } put: operationId: updateTestSuite x-required-scope: write summary: Update a test suite description: "Requires scope: `write`." tags: [Test Suites] requestBody: required: true content: application/json: schema: type: object responses: "200": description: Test suite updated content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } delete: operationId: deleteTestSuite x-required-scope: write summary: Delete a test suite description: "Requires scope: `write`." tags: [Test Suites] responses: "200": description: Test suite deleted content: application/json: schema: $ref: "#/components/schemas/EnvelopeDeleted" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-suites/{suiteId}/validate: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/suiteId" post: operationId: validateTestSuite x-required-scope: write summary: Validate a test suite description: "Run the suite against a public, non-loopback base URL to capture responses and run assertions. DO NOT use for local-app / localhost validation — the SaaS backend rejects private IPs with 500. For local apps, curl endpoints yourself (Bash) and pass the captured responses into create_test_suite directly. Requires scope: `write`." tags: [Test Suites] responses: "202": description: Validation job accepted content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Test Runs ───────────────────────────────────────────────────────── /apps/{appId}/test-runs: parameters: - $ref: "#/components/parameters/appId" get: operationId: listTestRuns x-required-scope: read summary: List test runs description: "List test runs for an app. Optional `kind` query param filters by run kind: `rerecord` (record_sandbox_test runs), `sandbox_run` (replay_sandbox_test runs), or `test_suite_run` (replay_test_suite live runs). Omit to return runs of every kind. Requires scope: `read`." tags: [Test Runs] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" - name: kind in: query required: false schema: type: string enum: [rerecord, sandbox_run, test_suite_run] description: "Filter by run kind. Omit to return runs of every kind." responses: "200": description: Paginated test runs content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-runs/{runId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" get: operationId: getTestRun x-required-scope: read summary: Get a test run description: "Requires scope: `read`." tags: [Test Runs] responses: "200": description: Test run detail content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-runs/{runId}/normalize: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" post: operationId: normalizeTestRun x-required-scope: write summary: Normalize a test run description: "Requires scope: `write`." tags: [Test Runs] responses: "200": description: Normalization applied content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-runs/{runId}/suite-reports: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" get: operationId: listSuiteReports x-required-scope: read summary: List suite reports for a test run description: "Requires scope: `read`. Supports cursor-based pagination." tags: [Test Runs] parameters: - name: page_size in: query schema: type: integer description: Number of items per page - name: after in: query schema: { type: string } description: Cursor for forward pagination - name: before in: query schema: { type: string } description: Cursor for backward pagination responses: "200": description: Suite reports content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-runs/{runId}/suite-reports/{reportId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" - $ref: "#/components/parameters/reportId" get: operationId: getSuiteReport x-required-scope: read summary: Get a suite report description: "Requires scope: `read`." tags: [Test Runs] responses: "200": description: Suite report detail content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/test-runs/{runId}/suite-reports/{reportId}/normalize: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" - $ref: "#/components/parameters/reportId" post: operationId: normalizeSuiteReport x-required-scope: write summary: Normalize a suite report description: "Requires scope: `write`." tags: [Test Runs] responses: "200": description: Normalization applied content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Load Tests ──────────────────────────────────────────────────────── /apps/{appId}/load-tests: parameters: - $ref: "#/components/parameters/appId" post: operationId: startLoadTest x-required-scope: write summary: Start a load test description: "Requires scope: `write`." tags: [Load Tests] requestBody: required: true content: application/json: schema: type: object responses: "202": description: Load test started content: application/json: schema: $ref: "#/components/schemas/Envelope" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } get: operationId: listLoadTestRuns x-required-scope: read summary: List load test runs description: "Requires scope: `read`. Returns all runs (no pagination)." tags: [Load Tests] responses: "200": description: Load test run list content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/load-tests/{runId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" get: operationId: getLoadTestReport x-required-scope: read summary: Get a load test report description: "Requires scope: `read`." tags: [Load Tests] responses: "200": description: Load test report content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/load-tests/{runId}/stop: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" post: operationId: stopLoadTest x-required-scope: write summary: Stop a running load test description: "Requires scope: `write`." tags: [Load Tests] responses: "200": description: Load test stopped content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/load-tests/{runId}/events: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/runId" get: operationId: streamLoadTestEvents x-required-scope: read summary: Stream load test events (SSE) description: "Requires scope: `read`. Returns a `text/event-stream`." tags: [Load Tests] responses: "200": description: SSE event stream content: text/event-stream: schema: type: string "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Generation History ──────────────────────────────────────────────── /apps/{appId}/generation-history: parameters: - $ref: "#/components/parameters/appId" get: operationId: listGenerationHistory x-required-scope: read summary: List generation history description: "Requires scope: `read`. Returns all entries (no pagination)." tags: [Generation History] responses: "200": description: Generation history list content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/generation-history/{jobId}: parameters: - $ref: "#/components/parameters/appId" - $ref: "#/components/parameters/jobId" get: operationId: getGenerationHistoryDetails x-required-scope: read summary: Get generation history details description: "Requires scope: `read`." tags: [Generation History] responses: "200": description: Generation history detail content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Branches (Keploy branch lifecycle for CI / MCP) ────────────────── /apps/{appId}/branches: parameters: - $ref: "#/components/parameters/appId" get: operationId: listBranches x-required-scope: read summary: List Keploy branches on an app description: "Returns every Keploy branch on an app, optionally filtered by status. Used by MCP write tools and CI integrations to enumerate branches without shelling out to the CLI. Requires scope: `read`." tags: [Branches] parameters: - name: status in: query required: false schema: type: string enum: [open, review_requested, approved, changes_requested, merged, closed, conflict] description: "Optional status filter. Omit to return all branches." responses: "200": description: All branches for the app (no pagination — typical apps have <50 branches; the dashboard renders the full list). content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/branches/ci: parameters: - $ref: "#/components/parameters/appId" post: operationId: createCIBranch x-required-scope: write summary: Find-or-create a CI branch description: "Creates a new Keploy branch (or returns the existing one) for a CI pipeline run. Idempotent on the (appId, name) pair. Requires scope: `write`." tags: [Branches] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, description: "Branch name (e.g., `pr-123`)" } git_ref: type: object description: "Optional Git provider context (PR, repo, etc.)" properties: provider: { type: string } repo_full_name: { type: string } pr_number: { type: integer } source_branch: { type: string } target_branch: { type: string } installation_id: { type: integer, format: int64 } responses: "200": description: Branch resolved (created or existing) content: application/json: schema: $ref: "#/components/schemas/Envelope" "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" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } # ── Jobs ────────────────────────────────────────────────────────────── /jobs: get: operationId: listJobs x-required-scope: read summary: List jobs description: "Requires scope: `read`." tags: [Jobs] parameters: - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/limit" responses: "200": description: Job list content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /jobs/{jobId}: parameters: - $ref: "#/components/parameters/jobId" get: operationId: getJob x-required-scope: read summary: Get a job description: "Requires scope: `read`." tags: [Jobs] responses: "200": description: Job detail content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /jobs/{jobId}/stop: parameters: - $ref: "#/components/parameters/jobId" post: operationId: stopJob x-required-scope: write summary: Stop a running job description: "Requires scope: `write`." tags: [Jobs] responses: "200": description: Job stopped content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /jobs/{jobId}/events: parameters: - $ref: "#/components/parameters/jobId" get: operationId: streamJobEvents x-required-scope: read summary: Stream job events (SSE) description: "Requires scope: `read`. Returns a `text/event-stream`." tags: [Jobs] responses: "200": description: SSE event stream content: text/event-stream: schema: type: string "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /jobs/{jobId}/validation-result: parameters: - $ref: "#/components/parameters/jobId" get: operationId: getValidationResult x-required-scope: read summary: Get job validation result description: "Requires scope: `read`." tags: [Jobs] responses: "200": description: Validation result content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Company ─────────────────────────────────────────────────────────── /company/subscription: get: operationId: getSubscription x-required-scope: read summary: Get company subscription description: "Requires scope: `read`." tags: [Company] responses: "200": description: Subscription info content: application/json: schema: $ref: "#/components/schemas/EnvelopeSubscription" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /company/usage: get: operationId: getUsage x-required-scope: read summary: Get company usage description: "Requires scope: `read`." tags: [Company] responses: "200": description: Usage data content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── Users ───────────────────────────────────────────────────────────── /users/me: get: operationId: getCurrentUser x-required-scope: read summary: Get current user description: "Requires scope: `read`. Returns the user associated with the API key." tags: [Users] responses: "200": description: Current user content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ── API Keys ────────────────────────────────────────────────────────── /api-keys: post: operationId: createAPIKey x-required-scope: admin summary: Create an API key description: "Requires scope: `admin`. The raw key is returned only once in the response." tags: [API Keys] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateAPIKeyRequest" responses: "201": description: API key created (includes raw key) content: application/json: schema: $ref: "#/components/schemas/EnvelopeAPIKeyCreated" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "500": { $ref: "#/components/responses/InternalError" } get: operationId: listAPIKeys x-required-scope: admin summary: List API keys description: "Requires scope: `admin`." tags: [API Keys] responses: "200": description: API key list content: application/json: schema: $ref: "#/components/schemas/EnvelopeAPIKeyList" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /api-keys/{keyId}: parameters: - name: keyId in: path required: true schema: { type: string } delete: operationId: revokeAPIKey x-required-scope: admin summary: Revoke an API key description: "Requires scope: `admin`." tags: [API Keys] responses: "200": description: API key revoked content: application/json: schema: $ref: "#/components/schemas/EnvelopeDeleted" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/smart-set/cases: parameters: - $ref: "#/components/parameters/appId" get: operationId: listSmartTestCases summary: List smart test cases description: | Returns the app's smart test set — the deduped, content-addressed set of API contracts maintained across recording sessions. Pass `branch_id` to get a branch's curated view (main cases ⊕ that branch's edits); absent → the main view. `include_obsolete=true` includes cases a user marked skip-in-replay. Requires scope: `read`. x-required-scope: read tags: [SmartSet] parameters: - $ref: "#/components/parameters/branchId" - name: include_obsolete in: query schema: { type: boolean, default: false } description: Include cases marked obsolete (skip-in-replay). responses: "200": description: Smart test cases content: application/json: schema: { type: object } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/smart-set/cases/{name}: parameters: - $ref: "#/components/parameters/appId" - name: name in: path required: true schema: { type: string } description: Smart-set-local case name (e.g. "test-3"). patch: operationId: updateSmartTestCase summary: Edit a smart test case (branch-only) description: | Edit a smart case on a branch. `branch_id` is REQUIRED — recording-driven main is never hand-edited; edits live on a branch, are reviewed via the branch diff, then merged. Value edits (noise/assertions/golden response body/description/mock links) write in place; SHAPE edits (endpoint/method/ status via request/response) recompute the contract identity (schema_ref) and, on collision with another case, return a conflict in the body rather than silently merging. Patch fields are JSON strings so the wire stays unambiguous; only provided fields are applied. Requires scope: `write`. x-required-scope: write tags: [SmartSet] requestBody: required: true content: application/json: schema: type: object required: [branch_id] properties: branch_id: { type: string, description: Branch UUID — required (edits are branch-only). } noiseJson: { type: string, description: "JSON map of fields to ignore on compare." } assertionsJson: { type: string, description: JSON assertions map. } description: { type: string } respBody: { type: string, description: Golden response body override (value edit). } requestJson: { type: string, description: "Full HTTPReq JSON (SHAPE edit — recomputes schema_ref)." } responseJson: { type: string, description: "Full HTTPResp JSON (SHAPE edit)." } mockReferencesJson: { type: string, description: "JSON array of {name,kind} — relink which mocks this case uses." } responses: "200": description: Updated case, or a schema_ref conflict content: application/json: schema: { type: object } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } delete: operationId: deleteSmartTestCase summary: Delete a smart test case (branch-only) description: | Delete a smart case on a branch — a tombstone overlay; main is untouched until merge. `branch_id` is REQUIRED: edits, including delete, are branch-only. Requires scope: `write`. x-required-scope: write tags: [SmartSet] parameters: - name: branch_id in: query required: true schema: { type: string } description: Branch UUID — required (delete is branch-only). responses: "200": description: Deleted content: application/json: schema: { type: object } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/smart-set/cases/{name}/obsolete: parameters: - $ref: "#/components/parameters/appId" - name: name in: path required: true schema: { type: string } description: Smart-set-local case name (e.g. "test-3"). post: operationId: setSmartTestCaseObsolete summary: Mark a smart test case obsolete / restore it (branch-only) description: | Toggle a smart case's obsolete flag (skip-in-replay) on a branch — an overlay edit. `branch_id` is REQUIRED: edits, including obsolete, are branch-only. Requires scope: `write`. x-required-scope: write tags: [SmartSet] requestBody: required: true content: application/json: schema: type: object required: [obsolete, branch_id] properties: obsolete: { type: boolean } branch_id: { type: string, description: Branch UUID — required (obsolete is branch-only). } responses: "200": description: Updated case content: application/json: schema: { type: object } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } /apps/{appId}/smart-set/mocks/{name}: parameters: - $ref: "#/components/parameters/appId" - name: name in: path required: true schema: { type: string } description: Smart-set mock name (e.g. "mock-2"). put: operationId: upsertSmartMock summary: Create or replace a smart-set mock's content (branch-only) description: | Write/replace a smart-set mock's content (a single NetworkTrafficDoc YAML) on a branch. `branch_id` is REQUIRED — main mocks come from recording and refresh via re-record; hand edits live on a branch, are reviewed via the branch diff, then merged. Requires scope: `write`. x-required-scope: write tags: [SmartSet] requestBody: required: true content: application/json: schema: type: object required: [branch_id, mockYaml] properties: branch_id: { type: string, description: Branch UUID — required (edits are branch-only). } mockYaml: { type: string, description: "The mock as a NetworkTrafficDoc YAML document." } responses: "200": description: Upserted content: application/json: schema: { type: object } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } delete: operationId: deleteSmartMock summary: Delete a smart-set mock (branch-only) description: | Tombstone a smart-set mock on a branch. `branch_id` is REQUIRED — edits are branch-only. Requires scope: `write`. x-required-scope: write tags: [SmartSet] parameters: - name: branch_id in: query required: true schema: { type: string } description: Branch UUID — required (edits are branch-only). responses: "200": description: Deleted content: application/json: schema: { type: object } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/InternalError" } # ─── Components ─────────────────────────────────────────────────────────── components: securitySchemes: apiKeyAuth: type: apiKey in: header name: X-API-Key description: "Personal Access Token (`kep_`-prefixed). Generate from Settings > API Keys." parameters: appId: name: appId in: path required: true schema: { type: string } suiteId: name: suiteId in: path required: true schema: { type: string } branchId: name: branch_id in: query required: false description: | Optional Keploy branch UUID. When set, scopes the read/write to that branch's overlay (copy-on-write — see /apps/{appId}/branches). When absent or empty, operations target the main branch (the historical default). Required for writes against a branch (the api-server's branch gate rejects 400 otherwise); reads are tolerant of absence. schema: { type: string } runId: name: runId in: path required: true schema: { type: string } reportId: name: reportId in: path required: true schema: { type: string } testSetId: name: testSetId in: path required: true schema: { type: string } testSetReportId: name: testSetReportId in: path required: true schema: { type: string } clusterId: name: clusterId in: path required: true schema: { type: string } jobId: name: jobId in: path required: true schema: { type: string } offset: name: offset in: query description: | Zero-based pagination offset. Negative values are rejected — the handler also clamps to 0 as a defence-in-depth fallback. schema: type: integer default: 0 minimum: 0 limit: name: limit in: query description: | Page size. Defaults to 20 when omitted; `limit=0` is also treated as "use default" (so existing clients sending an explicit zero keep the prior behaviour). Capped at 100 — the spec rejects higher values with 400 so callers fail loudly instead of being silently clamped, and the handler enforces the same cap as a defence-in-depth fallback. schema: type: integer default: 20 minimum: 0 maximum: 100 schemas: # ── Envelope / Meta ─────────────────────────────────────────────── Envelope: type: object properties: data: {} error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" Meta: type: object properties: request_id: { type: string } trace_id: { type: string } timestamp: { type: string, format: date-time } pagination: $ref: "#/components/schemas/Pagination" Pagination: type: object properties: has_next_page: { type: boolean } has_previous_page: { type: boolean } next_cursor: { type: string, nullable: true } previous_cursor: { type: string, nullable: true } total_count: { type: integer, nullable: true } APIError: type: object properties: code: { type: string } message: { type: string } details: type: array items: $ref: "#/components/schemas/ErrorDetail" ErrorDetail: type: object properties: field: { type: string } message: { type: string } # ── Domain objects ──────────────────────────────────────────────── AppResponse: type: object description: Lightweight app shape returned by listApps. Single-app reads (getApp/updateApp/createApp) return the full `App` schema below instead. properties: id: { type: string } name: { type: string } endpoint: { type: string } cid: { type: string } created_at: { type: integer, format: int64 } updated_at: { type: integer, format: int64 } # Full app shape returned by getApp / updateApp / createApp. Carries # the runtime-config fields Claude needs to diagnose + patch a # failing test suite (auth misconfig, missing custom variable, etc.) # via the read→patch→retry loop. Loose schema (additionalProperties: # true) because the underlying models.App carries many ancillary # fields the AI typically ignores; the listed properties are the # canonical surface for AI inspection and updateApp patches. App: type: object additionalProperties: true properties: id: { type: string } name: { type: string } endpoint: { type: string } cid: { type: string } auth: $ref: "#/components/schemas/Auth" appLevelCustomVariables: type: array description: Global key→value pairs shared across every suite. Reference in step bodies/urls/headers/asserts as `{{key}}`. Patch one variable at a time via updateApp.app_level_custom_variables (singular ExtractInput with Action add/update/delete). items: type: object properties: key: { type: string } value: { type: string } ignoreEndpoints: type: array items: { type: string } timeout: { type: integer, description: "Per-request timeout in seconds (0 = use default 30)" } rateLimit: { type: integer, description: "Max requests/sec the runner will fire (0 = unlimited)" } disableSchemaAssertion: { type: boolean } webhookUrl: { type: string } created_at: { type: integer, format: int64 } updated_at: { type: integer, format: int64 } Origin: type: object properties: type: { type: string } clusterId: { type: string } namespace: { type: string } deployment: { type: string } clusterName: { type: string } AppWithRecordingsResponse: type: object properties: id: { type: string } name: { type: string } origin: $ref: "#/components/schemas/Origin" TestSetResponse: type: object properties: id: { type: string } name: { type: string } app_id: { type: string } namespace: { type: string } deployment: { type: string } image_name: { type: string } image_tag: { type: string } testcase_count: { type: integer } created_at: { type: integer } HTTPRequest: type: object properties: method: { type: string } url: { type: string } header: { type: object, additionalProperties: { type: string } } body: { type: string } HTTPResponse: type: object properties: status_code: { type: integer } header: { type: object, additionalProperties: { type: string } } body: { type: string } TestCaseResponse: type: object properties: id: { type: string } test_set_id: { type: string } name: { type: string } namespace: { type: string } deployment: { type: string } failed: { type: boolean } http_req: $ref: "#/components/schemas/HTTPRequest" http_resp: $ref: "#/components/schemas/HTTPResponse" GeneratedSchemaResponse: type: object properties: app_id: { type: string } schema: { type: string, description: "Auto-generated OpenAPI schema from recorded traffic" } ClusterResponse: type: object properties: id: { type: string } name: { type: string } deployment_type: { type: string, enum: [saas, self-hosted] } CreateClusterRequest: type: object required: [name] properties: name: type: string description: Cluster name. Must be unique within the company. deployment_type: type: string enum: [saas, self-hosted] default: self-hosted description: Defaults to self-hosted when omitted. ClusterCreatedResponse: type: object properties: cluster_id: { type: string } access_key: type: string description: Cluster access key. Returned only once at creation — store it securely. name: { type: string } deployment_type: { type: string, enum: [saas, self-hosted] } AppByClusterResponse: type: object properties: id: { type: string } name: { type: string } origin: $ref: "#/components/schemas/Origin" TestRunReportResponse: type: object description: | Rollup view of a single legacy /tr test run. For AI analysis prefer `getTestReportFull` — this shape gives counts + CI/normalize/coverage metadata but doesn't include per-test diffs or mock_mismatches. properties: id: { type: string } app_id: { type: string } name: { type: string } test_set_count: { type: integer } passed_count: { type: integer } failed_count: { type: integer } ignored_count: { type: integer } tot_passed_tc: { type: integer } tot_failed_tc: { type: integer } tot_ignored_tc: { type: integer } status: type: string # Mirrors OSS keploy v3.5.49 TestStatus enum (six values). # Writers today emit PASSED/FAILED/RUNNING/PENDING at the # run level but IGNORED/OBSOLETE may surface from older # reports or future replayer behaviour, so the wire enum # tracks the full upstream set. enum: [PASSED, FAILED, RUNNING, PENDING, IGNORED, OBSOLETE] created_at: { type: integer } normalised_tc_passed_count: { type: integer } normalised_tc_failed_count: { type: integer } deleted_tc_passed_count: { type: integer } deleted_tc_failed_count: { type: integer } ignored_tc_passed_count: { type: integer } ignored_tc_failed_count: { type: integer } normalised_ts_passed_count: { type: integer } normalised_ts_failed_count: { type: integer } deleted_ts_passed_count: { type: integer } deleted_ts_failed_count: { type: integer } ignored_ts_passed_count: { type: integer } ignored_ts_failed_count: { type: integer } created_by: { $ref: "#/components/schemas/CreatorDetails" } normalize_data: { $ref: "#/components/schemas/NormalizeData" } coverage: { $ref: "#/components/schemas/TestCoverage" } schema_coverage: { type: number, nullable: true } enable_download: { type: boolean } ci_metadata: { $ref: "#/components/schemas/CIMetadata" } branch_id: { type: string } TestSetReportResponse: type: object description: | Per-test-set roll-up. Includes `tests[]` (per-case name + status + status_code) so callers can identify failing cases without fanning out to `listTestCaseReports`. properties: id: { type: string } test_set_id: { type: string } test_run_id: { type: string } name: { type: string } test_set_name: { type: string } status: { type: string } success: { type: integer } failure: { type: integer } obsolete: { type: integer } ignored: { type: integer } total: { type: integer } created_at: { type: integer } tests: type: array items: { $ref: "#/components/schemas/TestCaseInfo" } normalize_data: { $ref: "#/components/schemas/NormalizeData" } TestCaseReportResponse: type: object description: | Individual test-case result. `oss_report.result` carries the structured request/response/body/header diff; `oss_report.mock_mismatches` carries the expected-vs-actual mock list the replayer observed (populated for pass + fail cases when consumed-mock data is known — used to investigate mock drift). properties: test_set_report_id: { type: string } test_case_name: { type: string } test_case_id: { type: string } status: { type: string } oss_report: { $ref: "#/components/schemas/OssTestResult" } created_at: { type: integer } CreatorDetails: type: object properties: email: { type: string } userId: { type: string } cid: { type: string } NormalizeData: type: object properties: isNormalized: { type: boolean } editedBy: { type: string } editedAt: { type: string, format: date-time } TestCoverage: type: object properties: fileCoverage: type: object additionalProperties: { type: string } totalCoverage: { type: string } loc: type: object properties: total: { type: integer } covered: { type: integer } CIMetadata: type: object description: CI provenance stamped by the enterprise CLI when running in a pipeline. properties: provider: type: string description: e.g. github_actions, gitlab_ci, circleci, jenkins. branch: { type: string } commit_sha: { type: string } pipeline_url: { type: string } repository: { type: string } repo_url: { type: string } pr_number: { type: integer } pr_source_branch: { type: string } pr_target_branch: { type: string } trigger_actor: { type: string } trigger_actor_avatar: { type: string } build_id: { type: string } TestCaseInfo: type: object description: | Mirrors models.TestCaseInfo (pkg/models/testSetReport.go). Field names match the JSON tags on the Go struct exactly — `testName` (camelCase) for the case name, `status_code` (snake_case via the `json:"status_code"` tag on StatusCode) for the response status, `status` (lowercase string) for PASSED/FAILED/etc. properties: testName: { type: string } status: type: string description: PASSED / FAILED / OBSOLETE / IGNORED. status_code: { type: integer } MockEntry: type: object properties: name: { type: string } kind: { type: string } OssTestResult: type: object description: | Wire shape of `go.keploy.io/server/v3@v3.5.49 models.TestResult`. Property names track the JSON tags on the Go struct exactly — note that several are camelCase (`testCasePath`, `mockPath`, `testCaseID`) even though the surrounding API surface mostly uses snake_case. The yaml tags on the same struct are snake_case but encoding/json reads json tags, not yaml. properties: kind: type: string # OSS keploy v3.5.49 Kind enum — HTTP / Mongo / Generic / SQL / # Postgres / GRPC / Redis. Always emitted (no omitempty on the # OSS json tag). description: Protocol family of the test interaction. name: { type: string } status: { type: string } started: { type: integer } completed: { type: integer } time_taken: type: string # Always emitted on the wire (no omitempty upstream); typically # a duration string like "1.2s" stamped by the replayer. description: Wall-clock duration of the test case as a Go duration string. testCasePath: { type: string } mockPath: { type: string } testCaseID: { type: string } req: { $ref: "#/components/schemas/HTTPReq" } resp: { $ref: "#/components/schemas/HTTPResp" } # gRPC request/response — omitempty upstream, so populated only # for grpc-protocol test cases. Schemas are open `object` since # the OSS GrpcReq / GrpcResp structs are deep and rarely # consumed by AI/MCP analysis; callers that need them should # treat the payload as opaque. grpcReq: type: object additionalProperties: true nullable: true grpcRes: type: object additionalProperties: true nullable: true result: { $ref: "#/components/schemas/OssResult" } noise: type: object additionalProperties: type: array items: { type: string } mock_mismatches: { $ref: "#/components/schemas/MockMismatchInfo" } failure_info: { $ref: "#/components/schemas/OssFailureInfo" } OssFailureInfo: type: object description: | Wire shape of `go.keploy.io/server/v3@v3.5.49 models.FailureInfo`. Populated by the matcher (`risk`, `category`, `assessment`) and the replayer (`matched_calls`, `unmatched_calls`, `mock_mismatch`). All fields are `omitempty` upstream — a passing test case may emit `{}`, a failing case typically emits at least `risk` and `category`. properties: risk: type: string description: OSS RiskLevel enum (e.g. low / medium / high / critical). category: type: array items: type: string description: OSS FailureCategory enum entries (e.g. body_mismatch, header_mismatch). assessment: type: object additionalProperties: true nullable: true description: OSS FailureAssessment — open object; consume as opaque. mock_mismatch: { $ref: "#/components/schemas/MockMismatchInfo" } matched_calls: type: array items: type: object additionalProperties: true description: OSS MatchedCall entries — open object; consume as opaque. unmatched_calls: type: array items: type: object additionalProperties: true description: OSS UnmatchedCall entries — open object; consume as opaque. MockMismatchInfo: type: object properties: expected_mocks: type: array items: { $ref: "#/components/schemas/MockEntry" } actual_mocks: type: array items: { $ref: "#/components/schemas/MockEntry" } OssResult: type: object properties: status_code: { $ref: "#/components/schemas/IntResult" } headers_result: type: array items: { $ref: "#/components/schemas/HeaderResult" } body_result: type: array items: { $ref: "#/components/schemas/BodyResult" } trailer_result: type: array items: { $ref: "#/components/schemas/HeaderResult" } body_size_result: { $ref: "#/components/schemas/IntResult" } dep_result: type: array items: { type: object } IntResult: type: object properties: normal: { type: boolean } expected: { type: integer } actual: { type: integer } HeaderResult: type: object properties: normal: { type: boolean } expected: type: object properties: key: { type: string } value: type: array items: { type: string } actual: type: object properties: key: { type: string } value: type: array items: { type: string } BodyResult: type: object properties: normal: { type: boolean } type: { type: string } expected: { type: string } actual: { type: string } HTTPReq: type: object description: | Wire shape of `go.keploy.io/server/v3@v3.5.49 models.HTTPReq`. `header` is a `map[string]string` (single value per key), not a list. There is no `host` / `port` / `body_type` on this struct — host/port live on the enclosing TestCase/Spec, body framing is implicit in `body`. properties: method: { type: string } proto_major: { type: integer } proto_minor: { type: integer } url: { type: string } url_params: type: object additionalProperties: { type: string } header: type: object additionalProperties: { type: string } body: { type: string } body_ref: type: object description: Set when body was offloaded to asset storage (>1MB). binary: { type: string } form: type: array items: { type: object } timestamp: { type: string, format: date-time } HTTPResp: type: object description: | Wire shape of `go.keploy.io/server/v3@v3.5.49 models.HTTPResp`. `header` is a `map[string]string` (single value per key). No `body_type` on this struct. properties: status_code: { type: integer } header: type: object additionalProperties: { type: string } body: { type: string } body_skipped: { type: boolean } body_size: { type: integer, format: int64 } status_message: { type: string } proto_major: { type: integer } proto_minor: { type: integer } binary: { type: string } timestamp: { type: string, format: date-time } MockReferenceResponse: type: object properties: test_set_id: { type: string } namespace: { type: string } deployment: { type: string } storage_type: { type: string } mock_count: { type: integer } created_at: { type: integer, format: int64 } MockSpecResponse: type: object properties: name: { type: string } kind: { type: string } spec: { type: string, description: "Raw YAML of this mock document" } SubscriptionResponse: type: object properties: id: { type: string } plan_type: { type: string } plan_status: { type: string } product_type: { type: string } expiration: { type: integer, format: int64 } created_at: { type: integer, format: int64 } PublicAPIKey: type: object properties: id: { type: string } key_prefix: { type: string } name: { type: string } scopes: { type: array, items: { type: string } } cid: { type: string } created_by: { type: string } created_at: { type: integer, format: int64 } last_used_at: { type: integer, format: int64 } expires_at: { type: integer, format: int64, nullable: true } status: { type: string, enum: [active, revoked] } # ── Request bodies ──────────────────────────────────────────────── CreateAppRequest: type: object required: [name] description: | Body for POST /apps. Name is required and immutable — pick it carefully because updateApp deliberately rejects renames (the app name is the stable identifier devs and CI scripts type into commands; renaming would silently break references). Auth and the runtime-config fields can be set at creation so you don't have to follow up with an updateApp. properties: name: type: string description: App name. IMMUTABLE — cannot be changed via updateApp. endpoint: { type: string } schema: type: string description: OpenAPI/Swagger doc the validators use to suggest test cases. docs: type: string description: Free-form developer docs the AI uses as additional context. api_examples: type: string description: Sample request/response pairs the AI consults when authoring suites. webhook_url: type: string description: Optional webhook URL invoked at run lifecycle events. max_test_suites: type: integer description: Cap on how many suites generate-tests will mint at once. Server default applies if omitted. disable_schema_assertion: { type: boolean } auth: $ref: "#/components/schemas/Auth" description: | Authentication config the runner injects on every step request. Validation matches the GraphQL CreateApp resolver (and atg.Test's runtime check) — set authtype="None" for no auth, or supply the type-specific fields (BasicAuth needs Username+Password, BearerToken needs Token, etc.). UpdateAppRequest: type: object description: | RENAMING APPS IS PROHIBITED. The app name is the immutable identifier devs and CI scripts type into commands; the API rejects every attempt to change it. Do not try to work around this with raw curl, GraphQL, or any other path — they all fail. If a different name is required, create a new app and migrate the test suites manually. Other than that: PATCH-style update — only the fields you set get written. Use this to fix app-level config when a test suite run fails on a recoverable misconfiguration: wrong/expired bearer token (auth), missing global variable (app_level_custom_variables), wrong rate limit, etc. After updating, re-run `keploy create-test-suite` (or call create_test_suite again); the CLI re-fetches the app each invocation so it picks up the patched config without any extra plumbing. Concrete behavior on rename attempts: `name` is not part of this request schema, so a top-level `name` field is rejected during JSON decoding (unknown fields are disallowed) before any other validation runs. The same prohibition is enforced independently in the validation layer for non-HTTP callers. properties: endpoint: { type: string } schema: { type: string } docs: { type: string, description: "Free-form developer docs. Used by AI as additional context when authoring suites." } api_examples: { type: string, description: "Sample request/response pairs the AI consults when authoring suites." } brd: { type: string, description: "Business requirements document content the AI uses for context." } prd: { type: string, description: "Product requirements document content the AI uses for context." } postman: { type: string, description: "Postman collection JSON the AI parses for endpoint shapes / examples." } code_snippet: { type: string, description: "Server code snippet the AI uses for endpoint context." } main_curl: { type: string, description: "Reference curl that drives generation when no schema is available." } graphql_schema: { type: string, description: "GraphQL schema (SDL) the AI uses when generating GraphQL suites." } country: { type: string, description: "Two-letter country code controlling data-residency-affected behavior. Rarely set." } webhook_url: { type: string } auth: $ref: "#/components/schemas/Auth" description: Replaces the app's full auth config. To clear auth, set { authtype "None" }. app_level_custom_variables: type: object description: Add / update / delete a SINGLE global variable. The Action enum on the embedded ExtractInput controls the operation. To set multiple variables, call updateApp once per variable. properties: key: { type: string } value: { type: string } type: { type: string } level: { type: string } action: { type: string, enum: [add, update, delete] } app_level_custom_function: type: object description: Register a JS function devs can reference from suite step templates. Key uniquely identifies the function; CustomFunction is the JS source. properties: Key: { type: string } CustomFunction: { type: string } labels: type: array description: | Add or update labels on the app. New labels (no `id`) require both `name` and `color`; updates to existing labels (with `id`) require at least one of `name`/`color`. items: type: object properties: id: { type: string, description: "Existing label ID. Omit to add a new label." } name: { type: string } color: { type: string } ignore_endpoints: type: array description: Endpoint patterns the runner skips when generating / running suites. items: { type: string } max_test_suites: { type: integer, description: "Cap on how many suites generate-tests will mint at once. Server default applies if omitted." } rate_limit: { type: integer, description: "Requests-per-second cap the runner applies to outbound calls during runs." } enable_pre_hook: { type: boolean, description: "Run the pre-step hook before each test step." } enable_post_hook: { type: boolean, description: "Run the post-step hook after each test step." } private_mode: { type: boolean, description: "Restrict app visibility to the authenticated user only." } disable_schema_assertion: { type: boolean } Auth: type: object description: Authentication configuration for test execution. The runner injects the matching headers on every step request. properties: AuthType: { type: string, enum: [BearerToken, BasicAuth, APIKeyAuth, CookieAuth, LoginCurl, None] } BearerToken: { type: object, properties: { Token: { type: string } } } BasicAuth: { type: object, properties: { Username: { type: string }, Password: { type: string } } } APIKeyAuth: { type: object, properties: { Key: { type: string }, Value: { type: string } } } Cookie: { type: object, properties: { Cookie: { type: string } } } LoginCurl: type: object properties: Curl: { type: string, description: "Raw curl command that performs the login. Runner executes once at session start and parses the response." } CurlResponseType: { type: string, description: "How to interpret the response (jwt | cookie)." } JwtPath: { type: string, description: "JSONPath into the response that yields the bearer token." } GenerateTestSuitesRequest: type: object required: [base_url] properties: base_url: { type: string } schema: { type: string, description: "OpenAPI spec (YAML or JSON)" } docs: { type: string, description: "API documentation text" } examples: { type: string, description: "Example curls or request/response pairs" } user_prompt: { type: string, description: "Additional instructions for AI generation" } code_snippet: { type: string, description: "Relevant source code for context" } auth: { $ref: "#/components/schemas/Auth" } max_test_suites: { type: integer, default: 30 } ignore_endpoints: { type: array, items: { type: string } } webhook_url: { type: string } rate_limit: { type: integer } timeout: { type: integer } CreateAPIKeyRequest: type: object required: [name, scopes] properties: name: { type: string } scopes: type: array items: { type: string, enum: [read, write, admin] } ttl_days: { type: integer } # ── Typed envelopes ─────────────────────────────────────────────── EnvelopeApp: type: object properties: data: # Single-app reads return the full App (auth, custom vars, # runtime config). listApps still uses the lightweight # AppResponse via EnvelopeAppList below. $ref: "#/components/schemas/App" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeAppList: type: object properties: data: type: array items: $ref: "#/components/schemas/AppResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeAppWithRecordingsList: type: object properties: data: type: array items: $ref: "#/components/schemas/AppWithRecordingsResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestSetList: type: object properties: data: type: array items: $ref: "#/components/schemas/TestSetResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestCaseList: type: object properties: data: type: array items: $ref: "#/components/schemas/TestCaseResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestCase: type: object properties: data: $ref: "#/components/schemas/TestCaseResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeGeneratedSchema: type: object properties: data: $ref: "#/components/schemas/GeneratedSchemaResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeClusterList: type: object properties: data: type: array items: $ref: "#/components/schemas/ClusterResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeClusterCreated: type: object properties: data: $ref: "#/components/schemas/ClusterCreatedResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeAppByClusterList: type: object properties: data: type: array items: $ref: "#/components/schemas/AppByClusterResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestRunReportList: type: object properties: data: type: array items: $ref: "#/components/schemas/TestRunReportResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestRunReport: type: object properties: data: $ref: "#/components/schemas/TestRunReportResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestSetReportList: type: object properties: data: type: array items: $ref: "#/components/schemas/TestSetReportResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeTestRunReportFull: type: object properties: data: $ref: "#/components/schemas/TestRunReportFull" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" TestRunReportFull: type: object properties: report: $ref: "#/components/schemas/TestRunReportResponse" test_sets: type: array items: $ref: "#/components/schemas/TestSetReportWithCases" truncated: type: object description: | Counts of rows the response had to drop. `test_cases_dropped` counts per-case rows elided by `max_test_cases_per_set`. `test_sets_dropped` and `test_cases_capped_dropped` count rows beyond the hard DB-fetch caps applied to the underlying `listTestSetReports` and `listTestCaseReports` queries — if non-zero, the inflated response is incomplete and the caller should drill in via those endpoints with pagination. properties: test_cases_dropped: { type: integer } test_sets_dropped: { type: integer } test_cases_capped_dropped: { type: integer } TestSetReportWithCases: allOf: - $ref: "#/components/schemas/TestSetReportResponse" - type: object properties: test_cases: type: array items: { $ref: "#/components/schemas/TestCaseReportResponse" } EnvelopeTestCaseReportList: type: object properties: data: type: array items: $ref: "#/components/schemas/TestCaseReportResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeMockReferenceList: type: object properties: data: type: array items: $ref: "#/components/schemas/MockReferenceResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeMockList: type: object properties: data: type: object properties: reference: $ref: "#/components/schemas/MockReferenceResponse" mocks: type: array items: $ref: "#/components/schemas/MockSpecResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" # Single-mock CRUD envelopes. The `mock` field round-trips the # canonical OSS Mock — name, kind, full Spec — so callers can chain # follow-ups (e.g. capture the returned _id for an immediate edit) # without a separate fetch. EnvelopeMockSingle: type: object properties: data: type: object properties: mock: type: object description: OSS Mock (Name, Kind, Spec). error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeSuccess: type: object properties: data: type: object properties: success: type: boolean error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeMappingEdit: type: object properties: data: type: object properties: success: type: boolean mapping: $ref: "#/components/schemas/MappingDocument" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeBundleUpload: type: object properties: data: type: object properties: test_set: $ref: "#/components/schemas/TestSetResponse" test_case_ids: type: array items: { type: string } mock_ids: type: array items: { type: string } error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" RecordingExport: type: object properties: test_set: $ref: "#/components/schemas/TestSetResponse" test_cases: type: array items: $ref: "#/components/schemas/TestCaseResponse" mocks: type: array items: $ref: "#/components/schemas/MockSpecResponse" mappings: type: array items: $ref: "#/components/schemas/MappingDocument" MappingDocument: type: object properties: version: type: string kind: type: string test_set_id: type: string tests: type: array items: type: object properties: test_id: type: string mock_ids: type: array items: type: string EnvelopeRecordingExport: type: object properties: data: $ref: "#/components/schemas/RecordingExport" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" RecordingImportResult: type: object properties: updated: { type: integer } created: { type: integer } deleted: { type: integer } skipped: type: integer description: "Number of entries skipped (e.g., missing required name)" EnvelopeRecordingImportResult: type: object properties: data: $ref: "#/components/schemas/RecordingImportResult" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeSubscription: type: object properties: data: $ref: "#/components/schemas/SubscriptionResponse" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeAPIKeyCreated: type: object properties: data: type: object properties: key: type: string description: Raw API key (shown only once) api_key: $ref: "#/components/schemas/PublicAPIKey" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeAPIKeyList: type: object properties: data: type: array items: $ref: "#/components/schemas/PublicAPIKey" error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeDeleted: type: object properties: data: type: object properties: deleted: { type: boolean } error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" EnvelopeError: type: object properties: error: $ref: "#/components/schemas/APIError" meta: $ref: "#/components/schemas/Meta" ValidatorRule: type: object description: | A single MCP-validator rule. Returned inside ValidatorVersionMismatch.changed_rules so clients can render the specific rules that were added between their pre-validation binary and this api-server. properties: ID: type: string description: Stable rule identifier (e.g. R1, R29). AddedIn: type: string description: Rule-set version in which this rule was first introduced (e.g. v1). Summary: type: string description: Human-readable description of what the rule enforces. EscapeHatch: type: string description: | Per-rule escape-hatch flag the suite author can set to opt out of the rule (e.g. `allow_get_body`). Empty string when the rule has no escape hatch. ValidatorVersionMismatch: type: object description: | Body of the HTTP 426 response when an enterprise binary pre-validates a suite locally and stamps a `X-Keploy-Validator-Version` header whose value disagrees with the rule set this api-server expects. Returned wrapped in the standard Envelope under `data` (see EnvelopeValidatorVersion Mismatch); fields are surfaced individually so clients can render an upgrade prompt directly without re-parsing. properties: error: type: string description: Stable error key (currently always `validator_version_mismatch`). client_sent: type: string description: The version string the client supplied via the header. server_expected: type: string description: The rule-set version this api-server is built against. message: type: string description: Human-readable upgrade instruction for display in CLI/UI. changed_rules: type: array description: | Rules added between the client's version and the server's version, when known. Each entry is a full ValidatorRule object (id + summary + optional escape-hatch flag) so clients can render an explanation for each rule the user's binary doesn't yet know about. items: $ref: "#/components/schemas/ValidatorRule" EnvelopeValidatorVersionMismatch: type: object description: | Standard envelope wrapping a ValidatorVersionMismatch payload under `data`. This is the actual on-the-wire shape of the 426 response — every other apiv1 response goes through the same writeJSON helper that adds `data`/`meta`, so 426 follows suit for consistency with the rest of the surface. properties: data: $ref: "#/components/schemas/ValidatorVersionMismatch" meta: $ref: "#/components/schemas/Meta" RunTestSuitesRequest: type: object description: | Body for POST /apps/{appId}/test-suites/run. Mirrors apiv1.RunTestSuitesRequest in test_suites.go — the spec documents only the fields the SaaS path uses; nested polymorphic fields (auth) carry their type via authtype and are intentionally modelled as open objects here because the BasicAuth/BearerToken/APIKeyAuth/CookieAuth/LoginCurl variant shapes don't fit cleanly into OpenAPI 3.0.3 oneOf without adding a discriminator wrapper that doesn't match the on-the-wire shape. required: [base_url] properties: base_url: type: string description: | PUBLIC target URL the SaaS backend will hit. Loopback / private IPs are rejected with 400. For localhost runs use the MCP record_sandbox_test tool, not this endpoint. test_suite_ids: type: array description: | Suite IDs to include in the run. Empty/omitted means "run all suites for the app" — same default the GraphQL surface applies. items: type: string auth: type: object description: | Optional auth bundle the runner injects into every step. Carries an `authtype` discriminator (BearerToken / BasicAuth / APIKeyAuth / CookieAuth / LoginCurl / None) plus the matching variant block. See models.Auth in pkg/models/e2e.go for the full shape. rate_limit: type: integer description: Per-second cap on outgoing requests; 0 means unbounded. timeout: type: integer description: Per-request timeout in seconds; 0 means use the runner default. sandbox_mode: type: string description: | Empty for normal in-backend runs. `rerecord` and `integration_test` switch to sandbox flow where the local keploy agent or k8s-proxy drives the run. Surfaced for completeness; MCP tools (record_sandbox_test / replay_sandbox_test) are the supported entry points. enum: ["", "rerecord", "integration_test"] responses: BadRequest: description: Validation error content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" Unauthorized: description: Authentication required content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" Forbidden: description: "Insufficient scope or role (error code: INSUFFICIENT_SCOPE)" content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" Conflict: description: "Resource conflict (e.g., duplicate name)" content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" RateLimited: description: Too many requests content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" InternalError: description: Internal server error content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" PayloadTooLarge: description: | Request body exceeded the 5 MB cap enforced by the validation middleware (and decodeJSONBody as defence-in-depth). Returned for any body-accepting operation when the wrapped reader trips MaxBytesReader. The body is the standard error envelope with code `VALIDATION_ERROR`. content: application/json: schema: $ref: "#/components/schemas/EnvelopeError" UpgradeRequired: description: | Validator-version mismatch — the client's enterprise binary validates against an older or newer rule set than this api-server expects. The body names the expected version and (when known) the rules added between them so the user can decide whether to upgrade the binary or roll back the server. content: application/json: schema: $ref: "#/components/schemas/EnvelopeValidatorVersionMismatch"