openapi: 3.1.0
info:
  title: Sefer APIs
  version: "2026-07-17"
  description: |
    Torah reference, strict citation, inferred-lead, search, translation, and asynchronous
    transcription APIs.

    Every authenticated call is metered through the account usage ledger. Transcription usage is
    settled before a job is marked successful, and promotional credits are applied automatically.
    Account endpoints expose each invocation's price, credit applied, and final charge.
servers:
  - url: https://api.sefer.apiiam.com
    description: Production edge gateway
security:
  - bearerAuth: []
tags:
  - { name: System }
  - { name: Graph }
  - { name: Transcription }
  - { name: Account }
paths:
  /v1/health:
    get:
      tags: [System]
      summary: Service liveness and readiness
      operationId: health
      security: []
      responses:
        "200":
          description: Graph and transcription readiness
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
  /v1/lookup:
    get:
      tags: [Graph]
      summary: Resolve one reference to canonical Hebrew text
      operationId: lookup
      x-required-scope: lookup
      parameters:
        - $ref: "#/components/parameters/Ref"
      responses:
        "200":
          description: Resolution result
          headers: { X-Engine-Invocation-Id: { $ref: "#/components/headers/InvocationId" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/citations:
    get:
      tags: [Graph]
      summary: Get strict incoming and outgoing citations
      description: |
        Returns curated links or explicit extracted citations with non-empty Hebrew evidence.
        Model confidence is not exposed on this surface.
      operationId: citations
      x-required-scope: citations
      parameters:
        - $ref: "#/components/parameters/Ref"
        - $ref: "#/components/parameters/EdgeLimit"
      responses:
        "200":
          description: Citation result containing `cites` and `cited_by` arrays
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/leads:
    get:
      tags: [Graph]
      summary: Get AI-inferred incoming and outgoing relationships
      description: Returns non-curated relationships that do not meet the strict citation contract.
      operationId: leads
      x-required-scope: leads
      parameters:
        - $ref: "#/components/parameters/Ref"
        - $ref: "#/components/parameters/EdgeLimit"
      responses:
        "200":
          description: Lead result containing `leads_to` and `led_to_by` arrays
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/search:
    get:
      tags: [Graph]
      summary: Search the Hebrew corpus
      operationId: search
      x-required-scope: search
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 500 }
          example: מצטמק
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: category
          in: query
          schema: { type: string, maxLength: 100 }
      responses:
        "200":
          description: Ranked search hits
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/resolve:
    post:
      tags: [Graph]
      summary: Resolve up to 200 references in one request
      operationId: resolve
      x-required-scope: resolve
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [refs]
              properties:
                refs:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items: { type: string, minLength: 1, maxLength: 500 }
                include:
                  type: array
                  items: { type: string, enum: [text, citers] }
                  default: [text, citers]
                cite_limit: { type: integer, minimum: 1, maximum: 200, default: 20 }
            example:
              refs: ["Genesis 1:1", "Berakhot מב., י"]
              include: [text, citers]
              cite_limit: 3
      responses:
        "200":
          description: Resolution map keyed by the caller's original references
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/translate:
    post:
      tags: [Graph]
      summary: Translate Hebrew or Aramaic to English
      description: Cache-first translation with per-request Zero Data Retention enforcement.
      operationId: translate
      x-required-scope: translate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [hebrew]
              properties:
                hebrew: { type: string, minLength: 1, maxLength: 12000 }
      responses:
        "200":
          description: English translation and cache state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/uploads:
    post:
      tags: [Transcription]
      summary: Upload raw audio bytes
      operationId: uploadAudio
      x-required-scope: transcribe
      parameters:
        - name: X-Filename
          in: header
          schema: { type: string, maxLength: 120, default: audio }
      requestBody:
        required: true
        content:
          audio/mpeg: { schema: { type: string, format: binary } }
          audio/wav: { schema: { type: string, format: binary } }
          audio/ogg: { schema: { type: string, format: binary } }
          audio/webm: { schema: { type: string, format: binary } }
          audio/mp4: { schema: { type: string, format: binary } }
          audio/flac: { schema: { type: string, format: binary } }
      responses:
        "201":
          description: Upload accepted; unused uploads expire after 24 hours
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UploadEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { description: Unsupported audio content type }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/transcriptions:
    post:
      tags: [Transcription]
      summary: Create an asynchronous transcription job
      operationId: createTranscription
      x-required-scope: transcribe
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, minLength: 1, maxLength: 200 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [upload_id]
              properties:
                upload_id: { type: string, format: uuid }
                language:
                  type: string
                  pattern: "^[A-Za-z]{2,3}$"
      responses:
        "200":
          description: Idempotent replay of an existing job
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobEnvelope" }
        "202":
          description: Job created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/transcriptions/{job_id}:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      tags: [Transcription]
      summary: Inspect transcription job state
      operationId: getTranscription
      x-required-scope: transcribe
      responses:
        "200":
          description: Current job state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobEnvelope" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Transcription]
      summary: Permanently delete stored job content
      operationId: deleteTranscription
      x-required-scope: transcribe
      responses:
        "200":
          description: Content removed and job marked deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobEnvelope" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/transcriptions/{job_id}/result:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      tags: [Transcription]
      summary: Retrieve a completed transcript
      operationId: getTranscriptionResult
      x-required-scope: transcribe
      responses:
        "200":
          description: Transcript, provider/model metadata, hash, and deletion deadline
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Result is not ready }
        "410": { description: Result retention expired or content was deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/transcriptions/{job_id}/cancel:
    parameters:
      - $ref: "#/components/parameters/JobId"
    post:
      tags: [Transcription]
      summary: Cancel a queued transcription job
      operationId: cancelTranscription
      x-required-scope: transcribe
      responses:
        "200":
          description: Job cancelled without a provider call
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobEnvelope" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Job is already processing; use DELETE to discard content }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/account/credits:
    get:
      tags: [Account]
      summary: Get the authenticated customer's dollar-credit balance
      operationId: getCreditBalance
      responses:
        "200":
          description: Available credit in USD
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties: { available_usd: { type: number, minimum: 0 } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
  /v1/account/usage:
    get:
      tags: [Account]
      summary: List the authenticated customer's usage and settlements
      operationId: getUsage
      parameters:
        - name: since
          in: query
          schema: { type: string, format: date-time }
        - name: until
          in: query
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: Usage rows for this engine customer only
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/BillingUnavailable" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: scoped partner key
  parameters:
    Ref:
      name: ref
      in: query
      required: true
      schema: { type: string, minLength: 1, maxLength: 500 }
      example: Genesis 1:1
    EdgeLimit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 200, default: 20 }
    JobId:
      name: job_id
      in: path
      required: true
      schema: { type: string, format: uuid }
  headers:
    InvocationId:
      description: Usage-ledger invocation UUID for this metered graph call
      schema: { type: string, format: uuid }
  responses:
    BadRequest:
      description: Invalid parameter or body
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing or invalid bearer key
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: Key lacks the endpoint scope
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Resource not found for this customer
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    PayloadTooLarge:
      description: Request exceeds the endpoint limit
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    BillingUnavailable:
      description: The usage ledger could not open or settle the invocation
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Envelope:
      type: object
      properties:
        ok: { type: boolean }
        data: {}
        error: { type: string }
        request_id: { type: string }
        latency_ms: { type: number, minimum: 0 }
      required: [ok, request_id]
    Error:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            ok: { const: false }
            error: { type: string }
          required: [error]
    Upload:
      type: object
      required: [upload_id, filename, content_type, size_bytes, sha256, expires_at]
      properties:
        upload_id: { type: string, format: uuid }
        filename: { type: string }
        content_type: { type: string }
        size_bytes: { type: integer, minimum: 1 }
        sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        expires_at: { type: string, format: date-time }
    UploadEnvelope:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties: { data: { $ref: "#/components/schemas/Upload" } }
    Job:
      type: object
      required: [id, state, model, created_at, state_at]
      properties:
        id: { type: string, format: uuid }
        state: { type: string, enum: [queued, processing, succeeded, failed, cancelled, deleted] }
        model: { type: string }
        language: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        state_at: { type: string, format: date-time }
        audio_delete_at: { type: [string, "null"], format: date-time }
        result_delete_at: { type: [string, "null"], format: date-time }
        billing: { type: [object, "null"] }
        error: { type: [string, "null"] }
    JobEnvelope:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties: { data: { $ref: "#/components/schemas/Job" } }
    StrictCitation:
      type: object
      required: [ref, type, provenance, trust_basis, human_verified]
      properties:
        ref: { type: string }
        type: { type: string }
        provenance: { type: string }
        evidence_hebrew: { type: string }
        trust_basis: { type: string, enum: [curated_link, explicit_evidence] }
        human_verified: { type: boolean }
    Lead:
      type: object
      required: [ref, type, provenance, model_score]
      properties:
        ref: { type: string }
        type: { type: string }
        provenance: { type: string }
        evidence_hebrew: { type: string }
        model_score: { type: number, minimum: 0, maximum: 1 }
