{
  "openapi": "3.1.2",
  "info": {
    "title": "QuollVault External API",
    "version": "1.0.0",
    "description": "Supported Automation Token integration surface for uploading, listing, reading and downloading retained mobile builds.",
    "contact": { "url": "https://quollvault.com/contact" }
  },
  "externalDocs": { "url": "https://docs.quollvault.com/reference/api" },
  "servers": [
    { "url": "https://app.quollvault.com/api", "description": "Production" }
  ],
  "tags": [
    { "name": "Builds", "description": "Retained build upload and read operations." }
  ],
  "security": [ { "AutomationToken": [] } ],
  "paths": {
    "/builds": {
      "post": {
        "tags": ["Builds"],
        "operationId": "reserveBuild",
        "summary": "Reserve a build upload",
        "description": "Creates or resumes an idempotent build reservation and returns a short-lived direct R2 upload capability when upload is still required.",
        "x-quollvault-scope": "upload",
        "parameters": [ { "$ref": "#/components/parameters/WorkspaceContext" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ReserveBuildRequest" },
              "example": {
                "app_id": "00000000-0000-4000-8000-000000000001",
                "environment_id": "00000000-0000-4000-8000-000000000002",
                "filename": "Example.ipa",
                "size_bytes": 1048576,
                "content_md5": "AAAAAAAAAAAAAAAAAAAAAA==",
                "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                "idempotency_key": "00000000-0000-4000-8000-000000000003",
                "artifact_type": "ipa",
                "platform": "ios",
                "app_identifier": "com.example.mobile",
                "version": "1.2.3",
                "build_number": "42",
                "installability": "eligible",
                "ios_profile_expires_at": 1798761600000,
                "provenance": {
                  "provider": "github_actions",
                  "repository": "example/mobile",
                  "commit_sha": "0000000000000000000000000000000000000000",
                  "run_id": "12345",
                  "branch": "main",
                  "tag": null
                }
              }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/BuildReserved" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "413": { "$ref": "#/components/responses/TooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMedia" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/builds/{id}/finalise": {
      "post": {
        "tags": ["Builds"],
        "operationId": "finaliseBuild",
        "summary": "Finalise an uploaded build",
        "description": "Verifies the reserved artifact and transitions the build to ready. Repeating the request after success is idempotent.",
        "x-quollvault-scope": "upload",
        "parameters": [
          { "$ref": "#/components/parameters/BuildId" },
          { "$ref": "#/components/parameters/WorkspaceContext" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/BuildFinalised" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/apps/{id}/builds": {
      "get": {
        "tags": ["Builds"],
        "operationId": "listBuilds",
        "summary": "List builds for an app",
        "description": "Returns up to 50 ready builds visible to the token owner, newest first, with an opaque cursor for the next page.",
        "x-quollvault-scope": "read",
        "parameters": [
          { "$ref": "#/components/parameters/AppId" },
          { "$ref": "#/components/parameters/WorkspaceContext" },
          { "name": "environment", "in": "query", "schema": { "$ref": "#/components/schemas/Uuid" } },
          { "name": "version", "in": "query", "schema": { "type": "string", "minLength": 1, "maxLength": 100 } },
          { "name": "build", "in": "query", "schema": { "type": "string", "minLength": 1, "maxLength": 100 } },
          { "name": "platform", "in": "query", "schema": { "type": "string", "enum": ["ios", "android"] } },
          { "name": "commit", "in": "query", "schema": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" } },
          { "name": "cursor", "in": "query", "description": "Opaque cursor returned by the previous page.", "schema": { "type": "string", "minLength": 1, "maxLength": 4096 } }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/BuildList" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/builds/{id}": {
      "get": {
        "tags": ["Builds"],
        "operationId": "getBuild",
        "summary": "Get a build",
        "description": "Returns metadata for one visible retained build.",
        "x-quollvault-scope": "read",
        "parameters": [
          { "$ref": "#/components/parameters/BuildId" },
          { "$ref": "#/components/parameters/WorkspaceContext" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/BuildDetail" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/builds/{id}/download-url": {
      "post": {
        "tags": ["Builds"],
        "operationId": "createBuildDownloadUrl",
        "summary": "Create a short-lived build download URL",
        "description": "Returns a five-minute direct artifact download capability for one visible ready build. Treat the returned URL as a secret until it expires.",
        "x-quollvault-scope": "read",
        "parameters": [
          { "$ref": "#/components/parameters/BuildId" },
          { "$ref": "#/components/parameters/WorkspaceContext" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/DownloadCapability" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "AutomationToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "QuollVault Automation Token",
        "description": "F05 Automation Token. Tokens are bound to one Workspace and Project or App, and have fixed read + upload permissions."
      }
    },
    "parameters": {
      "WorkspaceContext": {
        "name": "X-QuollVault-Workspace-Id",
        "in": "header",
        "required": false,
        "description": "Optional for Automation Tokens because the Workspace is token-bound. If supplied, it must exactly match the token Workspace.",
        "schema": { "$ref": "#/components/schemas/Uuid" }
      },
      "BuildId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "$ref": "#/components/schemas/Uuid" }
      },
      "AppId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "$ref": "#/components/schemas/Uuid" }
      }
    },
    "headers": {
      "RequestId": {
        "description": "Request identifier for support and error correlation.",
        "schema": { "$ref": "#/components/schemas/Uuid" }
      }
    },
    "schemas": {
      "Uuid": {
        "type": "string",
        "format": "uuid",
        "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
      },
      "TimestampMs": { "type": "integer", "minimum": 0, "description": "Unix timestamp in milliseconds." },
      "ErrorEnvelope": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error", "request_id"],
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "fields": {
                "type": "object",
                "additionalProperties": { "type": "string" },
                "maxProperties": 10
              }
            }
          },
          "request_id": { "$ref": "#/components/schemas/Uuid" }
        }
      },
      "Provenance": {
        "type": "object",
        "additionalProperties": false,
        "required": ["provider", "repository", "commit_sha", "run_id", "branch", "tag"],
        "properties": {
          "provider": { "type": "string", "enum": ["local", "github_actions", "gitlab_ci", "azure_pipelines", "bitrise", "codemagic"] },
          "repository": { "type": ["string", "null"], "maxLength": 500 },
          "commit_sha": { "type": ["string", "null"], "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" },
          "run_id": { "type": ["string", "null"], "maxLength": 500 },
          "branch": { "type": ["string", "null"], "maxLength": 500 },
          "tag": { "type": ["string", "null"], "maxLength": 500 }
        }
      },
      "ReserveBuildRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["app_id", "environment_id", "filename", "size_bytes", "content_md5", "sha256", "idempotency_key", "artifact_type", "platform", "app_identifier", "version", "build_number", "installability", "ios_profile_expires_at", "provenance"],
        "properties": {
          "app_id": { "$ref": "#/components/schemas/Uuid" },
          "environment_id": { "$ref": "#/components/schemas/Uuid" },
          "filename": { "type": "string", "minLength": 1, "maxLength": 255 },
          "size_bytes": { "type": "integer", "minimum": 1, "maximum": 2147483648 },
          "content_md5": { "type": "string", "pattern": "^[A-Za-z0-9+/]{22}==$", "description": "Base64-encoded MD5 digest required by the upload capability." },
          "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
          "idempotency_key": { "$ref": "#/components/schemas/Uuid" },
          "artifact_type": { "type": "string", "enum": ["ipa", "apk", "aab"] },
          "platform": { "type": "string", "enum": ["ios", "android"] },
          "app_identifier": { "type": "string", "minLength": 1, "maxLength": 255 },
          "version": { "type": "string", "minLength": 1, "maxLength": 100 },
          "build_number": { "type": "string", "minLength": 1, "maxLength": 100 },
          "installability": { "type": "string", "enum": ["eligible", "ineligible", "unknown"] },
          "ios_profile_expires_at": { "oneOf": [ { "$ref": "#/components/schemas/TimestampMs" }, { "type": "null" } ] },
          "source_build_id": { "oneOf": [ { "$ref": "#/components/schemas/Uuid" }, { "type": "null" } ] },
          "provenance": { "$ref": "#/components/schemas/Provenance" }
        }
      },
      "PutCapability": {
        "type": "object",
        "additionalProperties": false,
        "required": ["url", "expires_in", "headers"],
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "expires_in": { "type": "integer", "minimum": 1, "maximum": 900 },
          "headers": { "type": "object", "additionalProperties": { "type": "string" } }
        }
      },
      "BuildReservation": {
        "type": "object",
        "additionalProperties": false,
        "required": ["build_id", "state"],
        "properties": {
          "build_id": { "$ref": "#/components/schemas/Uuid" },
          "state": { "type": "string", "enum": ["pending_upload", "ready"] },
          "expires_at": { "$ref": "#/components/schemas/TimestampMs" },
          "put": { "$ref": "#/components/schemas/PutCapability" }
        }
      },
      "BuildState": {
        "type": "object",
        "additionalProperties": false,
        "required": ["build_id", "state"],
        "properties": {
          "build_id": { "$ref": "#/components/schemas/Uuid" },
          "state": { "type": "string", "const": "ready" }
        }
      },
      "BuildSummary": {
        "type": "object",
        "required": ["id", "environment_id", "artifact_type", "platform", "app_identifier", "version", "build_number", "installability", "state", "created_at"],
        "properties": {
          "id": { "$ref": "#/components/schemas/Uuid" },
          "environment_id": { "$ref": "#/components/schemas/Uuid" },
          "artifact_type": { "type": "string", "enum": ["ipa", "apk", "aab"] },
          "platform": { "type": "string", "enum": ["ios", "android"] },
          "app_identifier": { "type": "string" },
          "version": { "type": "string" },
          "build_number": { "type": "string" },
          "installability": { "type": "string", "enum": ["eligible", "ineligible", "unknown"] },
          "ios_profile_expires_at": { "type": ["integer", "null"] },
          "release_notes": { "type": ["string", "null"] },
          "state": { "type": "string", "const": "ready" },
          "created_at": { "$ref": "#/components/schemas/TimestampMs" },
          "provider": { "type": ["string", "null"] },
          "commit_sha": { "type": ["string", "null"], "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" }
        }
      },
      "BuildList": {
        "type": "object",
        "additionalProperties": false,
        "required": ["items", "next_cursor"],
        "properties": {
          "items": { "type": "array", "maxItems": 50, "items": { "$ref": "#/components/schemas/BuildSummary" } },
          "next_cursor": { "type": ["string", "null"] }
        }
      },
      "BuildDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/BuildSummary" },
          {
            "type": "object",
            "properties": {
              "workspace_id": { "$ref": "#/components/schemas/Uuid" },
              "app_id": { "$ref": "#/components/schemas/Uuid" },
              "source_build_id": { "oneOf": [ { "$ref": "#/components/schemas/Uuid" }, { "type": "null" } ] },
              "filename": { "type": "string" },
              "updated_at": { "$ref": "#/components/schemas/TimestampMs" },
              "size_bytes": { "type": ["integer", "null"] },
              "sha256": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" },
              "repository": { "type": ["string", "null"] },
              "run_id": { "type": ["string", "null"] },
              "branch": { "type": ["string", "null"] },
              "tag": { "type": ["string", "null"] },
              "app_name": { "type": ["string", "null"] },
              "environment_name": { "type": ["string", "null"] }
            }
          }
        ]
      },
      "DownloadCapability": {
        "type": "object",
        "additionalProperties": false,
        "required": ["url", "expires_in", "filename"],
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "expires_in": { "type": "integer", "minimum": 1, "maximum": 600 },
          "filename": { "type": "string" }
        }
      }
    },
    "responses": {
      "BuildReserved": {
        "description": "Reservation created or resumed.",
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildReservation" } } }
      },
      "BuildFinalised": {
        "description": "Build is ready.",
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildState" } } }
      },
      "BuildList": {
        "description": "Visible builds.",
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildList" } } }
      },
      "BuildDetail": {
        "description": "Build metadata.",
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildDetail" } } }
      },
      "DownloadCapability": {
        "description": "Short-lived direct download capability.",
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DownloadCapability" }, "example": { "url": "https://example.invalid/download", "expires_in": 300, "filename": "Example.ipa" } } }
      },
      "BadRequest": { "$ref": "#/components/responses/Error400" },
      "Unauthorized": { "$ref": "#/components/responses/Error401" },
      "NotFound": { "$ref": "#/components/responses/Error404" },
      "Conflict": { "$ref": "#/components/responses/Error409" },
      "TooLarge": { "$ref": "#/components/responses/Error413" },
      "UnsupportedMedia": { "$ref": "#/components/responses/Error415" },
      "RateLimited": { "$ref": "#/components/responses/Error429" },
      "ServerError": { "$ref": "#/components/responses/Error500" },
      "Error400": { "description": "Invalid request.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error401": { "description": "Authentication required or token invalid/expired/revoked.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error404": { "description": "Resource is absent or outside effective token scope.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error409": { "description": "Request conflicts with current build/reservation state.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error413": { "description": "Artifact exceeds the upload size limit.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error415": { "description": "Unsupported request media type.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error429": { "description": "Workspace quota or rate limit blocked the operation.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
      "Error500": { "description": "Server-side failure with safe F15 error envelope.", "headers": { "X-Request-Id": { "$ref": "#/components/headers/RequestId" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
    }
  }
}
