
openapi: 3.0.3
info:
  title: Synsigra SaaS API
  version: 0.1.0
  description: Private-beta API for deterministic synthetic signal QA packages. Synthetic engineering data only; do not submit PHI.
servers:
  - url: https://www.timeonion.com/syn_sig_ra
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
    cookieAuth:
      type: apiKey
      in: cookie
      name: syn_sig_ra_session
  schemas:
    Account:
      type: object
      required: [user_id, organization_id, email, display_name, role, email_verified]
      properties:
        user_id: {type: string}
        organization_id: {type: string}
        email: {type: string, format: email}
        display_name: {type: string}
        role: {type: string, enum: [owner, admin, developer, viewer]}
        email_verified: {type: boolean}
        email_verified_at: {type: string, format: date-time}
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: {type: string}
            message: {type: string}
    PackTarget:
      type: object
      required: [target, support, scoreable, score_type, case_count, case_ids]
      properties:
        target: {type: string}
        support: {type: string, example: local_scoring}
        scoreable: {type: boolean}
        score_type: {type: string}
        description: {type: string}
        primary_metric: {type: string}
        case_count: {type: integer}
        case_ids: {type: array, items: {type: string}}
        accepted_formats: {type: array, items: {type: string}}
        reference_artifacts: {type: array, items: {type: string}}
        default_tolerance_seconds: {type: number, nullable: true}
    PackScenario:
      type: object
      required: [scenario_id, targets, scoreable_targets, reference_only_targets]
      properties:
        scenario_id: {type: string}
        targets: {type: array, items: {type: string}}
        scoreable_targets: {type: array, items: {type: string}}
        reference_only_targets: {type: array, items: {type: string}}
        duration_seconds: {type: integer}
        sampling_rate_hz: {type: integer}
        channel_count: {type: integer}
        sample_count: {type: integer, format: int64}
        estimated_package_bytes: {type: integer, format: int64}
    Pack:
      type: object
      required: [pack_id, display_name, version, scoreable_targets, reference_only_targets, scoring_mode]
      properties:
        pack_id: {type: string}
        display_name: {type: string}
        version: {type: string}
        description: {type: string}
        release_status: {type: string, enum: [beta, stable, deprecated]}
        released_at: {type: string, format: date}
        targets: {type: array, items: {type: string}}
        scoring_mode: {type: string, enum: [local, mixed, reference_only]}
        scoreable_targets: {type: array, items: {$ref: '#/components/schemas/PackTarget'}}
        reference_only_targets: {type: array, items: {$ref: '#/components/schemas/PackTarget'}}
        detector_output_schemas: {type: array, items: {type: string}}
        recommended_profile: {type: string, nullable: true}
        supported_threshold_profiles: {type: array, items: {type: string}}
        recommended_for: {type: array, items: {type: string}}
        not_recommended_for: {type: array, items: {type: string}}
        difficulty: {type: array, items: {type: string}}
        feature_tags: {type: array, items: {type: string}}
        modality: {type: array, items: {type: string}}
        duration:
          type: object
          properties:
            minimum_case_seconds: {type: integer}
            maximum_case_seconds: {type: integer}
            total_seconds: {type: integer}
        sampling_rates_hz: {type: array, items: {type: integer}}
        channels:
          type: object
          properties:
            minimum_channel_count: {type: integer}
            maximum_channel_count: {type: integer}
        estimated_package:
          type: object
          properties:
            bytes: {type: integer, format: int64}
            peak_memory_bytes: {type: integer, format: int64}
            size_class: {type: string}
        scenarios: {type: array, items: {$ref: '#/components/schemas/PackScenario'}}
        scenario_count: {type: integer}
        generator_contract: {type: string}
        compatible_generator_versions: {type: array, items: {type: string}}
        generator_compatibility:
          type: object
          properties:
            local_verifier_min_version: {type: string}
            challenge_package_contract: {type: string}
            scoring_manifest_contract: {type: string}
        output_artifact_roles: {type: array, items: {type: string}}
        primary_badges: {type: array, items: {type: string}}
        changelog:
          type: array
          items:
            type: object
            properties:
              version: {type: string}
              date: {type: string, format: date}
              summary: {type: string}
        deprecation_message: {type: string}
        pack_fingerprint: {type: string}
    Job:
      type: object
      required: [job_id, project_id, pack_id, status, created_at]
      properties:
        job_id: {type: string}
        project_id: {type: string}
        pack_id: {type: string}
        status:
          type: string
          enum: [queued, running, succeeded, failed, cancelled]
        created_at: {type: string, format: date-time}
        started_at: {type: string, format: date-time}
        completed_at: {type: string, format: date-time}
        package_id: {type: string}
        artifact_status: {type: string, enum: [available, expired]}
        package_fingerprint: {type: string}
        generator_version: {type: string}
        generator_build_identity: {type: string}
        manifest_url: {type: string}
        archive_url: {type: string}
        detection_templates_url: {type: string}
        verification_kit_url: {type: string}
        error:
          type: object
          properties:
            code: {type: string}
            message: {type: string}
    VerifierDownloads:
      type: object
      required: [schema_version, package, version, console_script, generator_included, files]
      properties:
        schema_version: {type: integer}
        package: {type: string, example: synsigra}
        version: {type: string, example: 0.2.0}
        console_script: {type: string, example: synsigra-verify}
        generator_included: {type: boolean, example: false}
        generated_at: {type: string, format: date-time}
        files:
          type: array
          items:
            type: object
            properties:
              label: {type: string}
              filename: {type: string}
              versioned_filename: {type: string}
              content_type: {type: string}
              sha256: {type: string}
              description: {type: string}
        install: {type: array, items: {type: string}}
        verify_example: {type: string}
paths:
  /openapi.yaml:
    get:
      summary: Download this complete live machine-readable API contract
      description: The document is embedded from the same repository revision as the running Apache module. Dynamic signal-authoring fields, enum catalogs, templates, and target requirements are exposed by the authenticated authoring endpoints referenced below.
      responses:
        '200':
          description: OpenAPI 3.0 YAML
          content:
            application/yaml:
              schema: {type: string}
        '405': {description: Method not allowed}
  /v1/legal:
    get:
      summary: Current private-beta legal, support, retention and billing metadata
      responses:
        '200':
          description: Current terms version and public legal/support destinations
          content:
            application/json:
              schema:
                type: object
                required: [terms_version, terms_url, privacy_url, support_url, billing_status, uptime_sla, artifact_retention_days]
                properties:
                  terms_version: {type: string, example: private-beta-2026-07-12-r3}
                  terms_url: {type: string}
                  privacy_url: {type: string}
                  support_url: {type: string, format: uri}
                  billing_status: {type: string, enum: [free_beta]}
                  uptime_sla: {type: string, nullable: true}
                  artifact_retention_days: {type: integer, example: 7}
  /healthz:
    get:
      summary: Liveness
      responses:
        '200': {description: Module is serving requests}
  /readyz:
    get:
      summary: Component readiness
      responses:
        '200': {description: DB, generator, catalog, and artifact store are ready}
        '503': {description: One or more components are not ready}
  /v1/packs:
    get:
      summary: List curated versioned packs
      responses:
        '200':
          description: Rich curated pack catalog with scoreable/reference-only discovery metadata
          content:
            application/json:
              schema:
                type: object
                required: [packs]
                properties:
                  packs:
                    type: array
                    items: {$ref: '#/components/schemas/Pack'}
  /v1/packs/{pack_id}:
    get:
      summary: Read a curated versioned pack
      parameters:
        - {in: path, name: pack_id, required: true, schema: {type: string}}
      responses:
        '200':
          description: Curated pack detail
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Pack'}
        '400': {description: Invalid pack ID}
        '404': {description: Pack not found}
  /v1/downloads/verifier:
    get:
      summary: Read local verifier download metadata
      description: Returns metadata for generator-free Synsigra verifier downloads. The files install synsigra-verify only and do not include signal-synth or generator source.
      security: [{cookieAuth: []}, {bearerAuth: []}]
      responses:
        '200':
          description: Verifier download metadata
          content:
            application/json:
              schema: {$ref: '#/components/schemas/VerifierDownloads'}
        '401': {description: Unauthorized}
        '404': {description: Downloads not built or not deployed}
  /v1/downloads/verifier/{filename}:
    get:
      summary: Download local verifier bundle or wheel
      security: [{cookieAuth: []}, {bearerAuth: []}]
      parameters:
        - {in: path, name: filename, required: true, schema: {type: string, enum: [metadata.json, synsigra-verifier.zip, synsigra-wheel.whl]}}
      responses:
        '200':
          description: Verifier file bytes
          content:
            application/zip:
              schema: {type: string, format: binary}
            application/octet-stream:
              schema: {type: string, format: binary}
        '401': {description: Unauthorized}
        '404': {description: File not available}
  /v1/authoring/schema:
    get:
      summary: Read core scenario-authoring schema metadata
      security: [{cookieAuth: []}, {bearerAuth: []}]
      responses:
        '200': {description: Core authoring schema metadata}
        '401': {description: Unauthorized}
  /v1/authoring/templates:
    get:
      summary: Read core scenario templates
      security: [{cookieAuth: []}, {bearerAuth: []}]
      responses:
        '200': {description: Template catalog with complete valid scenario documents}
        '401': {description: Unauthorized}
  /v1/authoring/preview:
    post:
      summary: Preview a scenario and target set before saving or composing a custom pack
      security: [{cookieAuth: []}, {bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [scenario, targets]
              properties:
                scenario: {type: object}
                targets: {type: array, minItems: 1, items: {type: string}}
              additionalProperties: false
      responses:
        '200': {description: Core pack-analysis preview, including scoreable/reference-only targets and size estimates}
        '422': {description: Scenario JSON failed core validation}
  /v1/authoring/curated-scenarios/{pack_id}/{case_id}:
    get:
      summary: Clone a curated scenario into the draft editor
      security: [{cookieAuth: []}, {bearerAuth: []}]
      parameters:
        - {in: path, name: pack_id, required: true, schema: {type: string}}
        - {in: path, name: case_id, required: true, schema: {type: string}}
      responses:
        '200': {description: Curated scenario JSON}
        '404': {description: Pack/case not found}
  /v1/auth/register:
    post:
      summary: Create an account and send an ownership-verification email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, display_name, accept_terms, terms_version]
              properties:
                email: {type: string, format: email}
                password: {type: string, minLength: 12, maxLength: 128}
                display_name: {type: string, minLength: 1, maxLength: 100}
                accept_terms:
                  type: boolean
                  description: Must be true.
                terms_version:
                  type: string
                  enum: [private-beta-2026-07-12-r3]
              additionalProperties: false
      responses:
        '202': {description: Verification required or generic accepted response}
        '400': {description: Invalid account request or current terms were not accepted}
        '503': {description: Transactional email delivery is not configured or temporarily unavailable}
  /v1/auth/verify-email:
    post:
      summary: Verify email ownership and start a browser session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: {type: string, maxLength: 512}
              additionalProperties: false
      responses:
        '200': {description: Verified and signed in, content: {application/json: {schema: {$ref: '#/components/schemas/Account'}}}}
        '400': {description: Invalid or expired single-use token}
        '429': {description: Too many token submissions}
  /v1/auth/resend-verification:
    post:
      summary: Request another email-verification link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: {type: string, format: email}
              additionalProperties: false
      responses:
        '202': {description: Generic accepted response independent of account eligibility}
        '503': {description: Transactional email delivery is unavailable}
  /v1/auth/password-reset/request:
    post:
      summary: Request a forgotten-password email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: {type: string, format: email}
              additionalProperties: false
      responses:
        '202': {description: Generic accepted response independent of account existence}
        '503': {description: Transactional email delivery is unavailable}
  /v1/auth/password-reset/complete:
    post:
      summary: Consume a reset token, change password, and start a browser session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token: {type: string, maxLength: 512}
                password: {type: string, minLength: 12, maxLength: 128}
              additionalProperties: false
      responses:
        '200': {description: Password changed and signed in, content: {application/json: {schema: {$ref: '#/components/schemas/Account'}}}}
        '400': {description: Invalid or expired single-use token or invalid password}
        '429': {description: Too many token submissions}
  /v1/auth/login:
    post:
      summary: Start a browser session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: {type: string, format: email}
                password: {type: string}
              additionalProperties: false
      responses:
        '200': {description: Signed in, content: {application/json: {schema: {$ref: '#/components/schemas/Account'}}}}
        '401': {description: Invalid email or password}
        '403': {description: Email ownership is not verified}
  /v1/auth/me:
    get:
      summary: Current browser account
      security: [{cookieAuth: []}]
      responses:
        '200': {description: Current account, content: {application/json: {schema: {$ref: '#/components/schemas/Account'}}}}
        '401': {description: No valid browser session}
  /v1/auth/logout:
    post:
      summary: End the browser session
      security: [{cookieAuth: []}]
      responses:
        '200': {description: Signed out}
  /v1/projects:
    get:
      summary: List caller organization projects
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Project list}
        '401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
    post:
      summary: Create project (owner/admin)
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [display_name]
              properties: {display_name: {type: string, maxLength: 100}}
              additionalProperties: false
      responses:
        '201': {description: Project created}
        '403': {description: Role forbidden}
  /v1/api-keys:
    get:
      summary: List personal API keys without secrets
      security: [{cookieAuth: []}, {bearerAuth: []}]
      responses:
        '200': {description: Personal API keys}
    post:
      summary: Create a personal API key; secret is returned once
      security: [{cookieAuth: []}, {bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label]
              properties:
                label: {type: string, minLength: 1, maxLength: 100}
              additionalProperties: false
      responses:
        '201': {description: API key metadata and one-time secret}
  /v1/api-keys/{api_key_id}:
    delete:
      summary: Revoke an owned personal API key
      security: [{cookieAuth: []}, {bearerAuth: []}]
      parameters:
        - {in: path, name: api_key_id, required: true, schema: {type: string}}
      responses:
        '200': {description: Revoked}
        '404': {description: Key not found}
  /v1/jobs:
    get:
      summary: List jobs
      security: [{bearerAuth: []}]
      parameters:
        - {in: query, name: limit, schema: {type: integer, minimum: 1, maximum: 100, default: 25}}
        - {in: query, name: offset, schema: {type: integer, minimum: 0, default: 0}}
      responses:
        '200': {description: Paginated jobs}
    post:
      summary: Create challenge job
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, pack_id]
              properties:
                project_id: {type: string}
                pack_id: {type: string}
              additionalProperties: false
      responses:
        '202': {description: Job queued}
        '429': {description: Job quota exceeded}
        '507': {description: Generation paused to preserve disk reserve}
  /v1/jobs/{job_id}:
    parameters:
      - {in: path, name: job_id, required: true, schema: {type: string}}
    get:
      summary: Read job
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Job, content: {application/json: {schema: {$ref: '#/components/schemas/Job'}}}}
        '404': {description: Not found}
    delete:
      summary: Soft-delete non-running job
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Deleted}
        '409': {description: Running job cannot be deleted}
  /v1/jobs/{job_id}/cancel:
    post:
      summary: Cancel queued job
      security: [{bearerAuth: []}]
      parameters:
        - {in: path, name: job_id, required: true, schema: {type: string}}
      responses:
        '200': {description: Cancelled}
        '409': {description: Invalid state}
  /v1/jobs/{job_id}/retry:
    post:
      summary: Retry failed or cancelled job as a new job
      security: [{bearerAuth: []}]
      parameters:
        - {in: path, name: job_id, required: true, schema: {type: string}}
      responses:
        '202': {description: New job queued}
        '409': {description: Invalid state}
  /v1/jobs/{job_id}/rebuild:
    post:
      summary: Queue an exact-version rebuild for an expired succeeded job
      description: Uses the immutable pack recipe and SHA-256-addressed historical generator release. A newer generator is never substituted and a package fingerprint mismatch fails the new job.
      security: [{bearerAuth: []}]
      parameters:
        - {in: path, name: job_id, required: true, schema: {type: string}}
      responses:
        '202': {description: Exact-version rebuild queued as a new job}
        '409': {description: Invalid state, recipe unavailable, or historical generator unavailable}
        '507': {description: Rebuild paused to preserve disk reserve}
  /v1/jobs/{job_id}/detection-templates.zip:
    get:
      summary: Download detector-output templates for a completed curated job
      description: Returns a ZIP containing README.md plus one template file per scoreable case/target. Reference-only targets are excluded.
      security: [{cookieAuth: []}, {bearerAuth: []}]
      parameters:
        - {in: path, name: job_id, required: true, schema: {type: string}}
      responses:
        '200':
          description: Detection-template ZIP
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '404': {description: Job not found}
        '409': {description: Job not succeeded, custom pack unsupported, or no scoreable targets}
  /v1/jobs/{job_id}/verification-kit.zip:
    get:
      summary: Download a job-local verification starter kit
      description: Returns the challenge package directly as one ZIP, augmented with SYNSIGRA_README.md, notices, and curated detector-output templates when available. After one unzip, the extracted directory can be passed directly to synsigra-verify; there is no nested package ZIP. The kit does not include the generator or verifier wheel.
      security: [{cookieAuth: []}, {bearerAuth: []}]
      parameters:
        - {in: path, name: job_id, required: true, schema: {type: string}}
      responses:
        '200':
          description: Verification kit ZIP
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '404': {description: Job or artifact not found}
        '409': {description: Job not succeeded or artifacts expired}
  /v1/artifacts/{package_id}/{filename}:
    get:
      summary: Download immutable manifest or ZIP
      security: [{bearerAuth: []}]
      parameters:
        - {in: path, name: package_id, required: true, schema: {type: string}}
        - {in: path, name: filename, required: true, schema: {type: string, enum: [manifest.json, package.zip]}}
      responses:
        '200': {description: Artifact bytes}
        '404': {description: Missing, expired, or cross-tenant artifact}
  /v1/usage:
    get:
      summary: Caller usage and limits
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Usage summary}
  /v1/metrics:
    get:
      summary: Owner/admin operational metrics
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Operational counters and worker heartbeat}
        '403': {description: Role forbidden}
  /v1/scenarios:
    get:
      summary: List caller-owned scenario drafts
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Scenario draft list including saved target_intent values}
    post:
      summary: Validate and create an editable scenario draft
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scenario, target_intent]
              properties:
                name: {type: string, maxLength: 100}
                scenario: {type: object}
                target_intent:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: {type: string}
                  description: User-selected verification goals inherited by custom pack composition.
              additionalProperties: false
      responses:
        '201': {description: Valid draft saved with fingerprint}
        '422': {description: Invalid draft saved with actionable validation errors}
  /v1/scenarios/{scenario_id}:
    parameters:
      - {in: path, name: scenario_id, required: true, schema: {type: string}}
    get:
      summary: Read caller-owned scenario draft
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Scenario draft}
        '404': {description: Missing or owned by another user}
    put:
      summary: Replace and revalidate scenario draft
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scenario, target_intent]
              properties:
                name: {type: string, maxLength: 100}
                scenario: {type: object}
                target_intent:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: {type: string}
              additionalProperties: false
      responses:
        '200': {description: Valid draft updated}
        '422': {description: Invalid draft updated with validation errors}
    delete:
      summary: Delete scenario draft
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Draft deleted}
        '404': {description: Missing or owned by another user}
  /v1/custom-packs:
    get:
      summary: List caller-owned immutable custom packs
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Custom pack list}
    post:
      summary: Compose a custom pack from valid owned scenario drafts
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, description, targets, scenario_ids]
              properties:
                name: {type: string, maxLength: 100}
                description: {type: string}
                targets: {type: array, minItems: 1, items: {type: string}}
                scenario_ids: {type: array, minItems: 1, items: {type: string}}
              additionalProperties: false
      responses:
        '201': {description: Immutable custom pack created}
        '400': {description: Invalid, duplicate, non-owned, or invalid-status draft}
        '422': {description: Core pack analysis rejected one or more scenario-target combinations}
  /v1/custom-packs/{pack_id}:
    parameters:
      - {in: path, name: pack_id, required: true, schema: {type: string}}
    get:
      summary: Read caller-owned custom pack
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Custom pack}
        '404': {description: Missing or owned by another user}
    delete:
      summary: Hide custom pack while retaining immutable job snapshots
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Custom pack deleted}
        '404': {description: Missing or owned by another user}

