openapi: 3.0.3
info:
  title: Prismism API
  version: 1.0.0
  description: |
    Upload any file. Get a shareable link with built-in analytics and gating.
    API-first document delivery — built for AI agents.

    ## Authentication

    All `/v1/` endpoints (except auth) require an API key via the `x-api-key` header.

    ```
    x-api-key: pal_your_key_here
    ```

    Get a key by creating an account via `POST /v1/auth/register` — the API key is returned
    once in the registration response and is not retrievable again. Store it securely.

    ## Quick Start

    Upload your first file in 30 seconds:

    ```bash
    curl -X POST https://prismism.dev/v1/artifacts \
      -H "x-api-key: pal_your_key_here" \
      -F "file=@report.pdf" \
      -F "title=Q4 Report"
    ```

    ## Gated Content Quick Start

    Upload with password protection, unlock, and fetch with a Bearer token:

    ```bash
    # 1. Upload with password
    curl -X POST https://prismism.dev/v1/artifacts \
      -H "x-api-key: pal_your_key_here" \
      -F "file=@confidential.pdf" \
      -F "title=Board Deck" \
      -F "password=secret123"

    # 2. Unlock (get a Bearer token)
    curl -X POST https://prismism.dev/p/AbCdEf12/unlock \
      -H "Content-Type: application/json" \
      -d '{"gate": "password", "value": "secret123"}'

    # 3. Fetch content with the token
    curl https://prismism.dev/p/AbCdEf12/content \
      -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
      --output confidential.pdf
    ```

    ## Error Handling

    All errors return consistent JSON:

    ```json
    {
      "error": "ERROR_CODE",
      "message": "Human-readable description"
    }
    ```

    | HTTP Code | Error Code | When |
    |-----------|-----------|------|
    | 400 | `INVALID_REQUEST` | Missing required fields, bad format |
    | 401 | `UNAUTHORIZED` | Missing or invalid API key |
    | 403 | `FORBIDDEN` | Access denied (wrong password, domain not allowed) |
    | 404 | `NOT_FOUND` | Artifact doesn't exist |
    | 410 | `EXPIRED` | Artifact has passed its expiresAt date |
    | 413 | `FILE_TOO_LARGE` | Upload exceeds size limit |
    | 415 | `UNSUPPORTED_FORMAT` | File type not accepted |
    | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |

    ## Rate Limits

    | Route | Scope | Limit |
    |-------|-------|-------|
    | `/v1/*` (authenticated) | Per API key | 60 req/min |
    | `/v1/auth/*` | Per IP | 10 req/min |
    | `/p/*` (public) | Per IP per shortId | 120 req/min |

    When rate limiting is active, responses include the following headers:
    - `x-ratelimit-limit` — max requests in window
    - `x-ratelimit-remaining` — requests left
    - `x-ratelimit-reset` — UTC epoch seconds when window resets

    These headers are present on rate-limited routes. If you receive a `429` response, wait until the reset time before retrying.

    ## Supported File Formats

    | MIME Type | Extensions | Max Size | Format | Renderer |
    |-----------|-----------|----------|--------|----------|
    | `application/pdf` | .pdf | 50 MB | pdf | pdfjs |
    | `text/html` | .html, .htm | 10 MB | html | html_iframe |
    | `text/markdown` | .md | 5 MB | markdown | markdown_iframe |
    | `image/png` | .png | 20 MB | image | image |
    | `image/jpeg` | .jpg, .jpeg | 20 MB | image | image |
    | `image/gif` | .gif | 20 MB | image | image |
    | `image/svg+xml` | .svg | 5 MB | image | image |
    | `image/webp` | .webp | 20 MB | image | image |
    | `text/plain` | .txt, .csv, .json | 5 MB | text | text |
    | `application/json` | .json | 5 MB | text | text |

  contact:
    name: Prismism
    url: https://prismism.dev

x-agent-instructions: |
  Prismism API — shareable links for any file.
  
  Getting started:
  1. Register: POST /v1/auth/register with {email, password, name}
  2. Login: POST /v1/auth/login to get session cookie, or use x-api-key header
  3. Upload: POST /v1/artifacts (multipart form, field name: "file") → returns shareable URL
  4. Share: The response includes a "url" field with the public link
  
  Authentication: Either cookie session (from login) or x-api-key header with your API key.
  Base URL: https://prismism.dev
  Full docs: https://prismism.dev/docs

servers:
  - url: https://prismism.dev
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Auth
    description: Account registration and login
  - name: Artifacts
    description: Upload, list, update, and delete artifacts
  - name: Public
    description: Public artifact viewing and downloads (no auth required)
  - name: Account
    description: Account usage and limits
  - name: Webhooks
    description: Webhook subscriptions (coming soon)

paths:
  /v1/auth/register:
    post:
      tags: [Auth]
      operationId: register
      summary: Create account and get API key
      description: |
        Register a new account. Returns the API key **once** — it is not
        retrievable via any other endpoint. Store it securely.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            example:
              name: Jane Smith
              email: jane@example.com
              password: securepassword123
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterResponse'
              example:
                user:
                  id: d4f7a1b2-3c9e-4e8f-b5d6-1a2b3c4d5e6f
                  name: Jane Smith
                  email: jane@example.com
                  plan: free
                apiKey:
                  id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                  key: pal_abc123def456...
                  prefix: pal_abc123de
                  name: default
                  createdAt: '2026-01-15T10:30:00Z'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Email already registered

  /v1/auth/login:
    post:
      tags: [Auth]
      operationId: login
      summary: Web login (session cookie)
      description: |
        Authenticates via email/password and returns user info with API key metadata.
        This is primarily for the web dashboard. For programmatic API access, use
        the `x-api-key` header instead.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: jane@example.com
              password: securepassword123
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: Invalid email or password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/auth/api-keys:
    post:
      tags: [Auth]
      operationId: createApiKey
      summary: Generate new API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Friendly name for the key
                  example: ci-pipeline
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyFull'
              example:
                id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                key: pal_newkey123...
                prefix: pal_newkey12
                name: ci-pipeline
                createdAt: '2026-01-15T10:30:00Z'
        '403':
          description: API key limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags: [Auth]
      operationId: listApiKeys
      summary: List API keys
      responses:
        '200':
          description: List of API key metadata (keys are never exposed)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyMeta'

  /v1/auth/api-keys/{id}:
    delete:
      tags: [Auth]
      operationId: deleteApiKey
      summary: Revoke API key
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: API key deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/artifacts:
    post:
      tags: [Artifacts]
      operationId: createArtifact
      summary: Upload a file or raw HTML
      description: |
        Create an artifact by uploading a file (multipart/form-data) or sending raw HTML (application/json).

        **File upload (primary):** Send as `multipart/form-data` with a `file` field.

        **Raw HTML (convenience):** Send as `application/json` with an `html` field.

        The `allowedDomains` field in multipart uploads must be a JSON-encoded string
        (e.g. `'["acme.com"]'`), not repeated form fields.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to upload
                title:
                  type: string
                  description: Display name. Defaults to original filename.
                  example: Q4 Report
                password:
                  type: string
                  description: Password to gate access to this artifact.
                  example: secret123
                requireEmail:
                  type: boolean
                  description: If true, viewers must enter email before seeing content.
                  default: false
                allowedDomains:
                  type: string
                  description: |
                    JSON-encoded array of allowed email domains. Only applies
                    when requireEmail is true. Pass as a JSON string.
                  example: '["acme.com", "partner.co"]'
                allowNetwork:
                  type: boolean
                  description: |
                    Allow HTML and Markdown content to make external network requests
                    (fetch, XHR, images, scripts). Defaults to false. Only applies to
                    html and markdown formats.
                  default: false
                expiresAt:
                  type: string
                  format: date-time
                  description: Auto-expire date in RFC3339 format.
                  example: '2026-04-01T00:00:00Z'
          application/json:
            schema:
              $ref: '#/components/schemas/CreateHtmlArtifactRequest'
            example:
              title: My Report
              html: <h1>Hello</h1><p>World</p>
              password: optional
              requireEmail: false
              expiresAt: '2026-04-01T00:00:00Z'
      responses:
        '201':
          description: Artifact created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: File too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: FILE_TOO_LARGE
                message: File exceeds the 50MB upload limit
        '415':
          description: Unsupported file type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      tags: [Artifacts]
      operationId: listArtifacts
      summary: List your artifacts
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
          description: Page number (1-indexed)
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Items per page (max 100)
        - name: status
          in: query
          schema:
            type: string
            enum: [processing, converting, ready, failed]
          description: Filter by artifact status
      responses:
        '200':
          description: Paginated list of artifacts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtifactList'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/artifacts/{id}:
    get:
      tags: [Artifacts]
      operationId: getArtifact
      summary: Get artifact details
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Artifact details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
    patch:
      tags: [Artifacts]
      operationId: updateArtifact
      summary: Update artifact metadata
      description: |
        Update an artifact's metadata. File content cannot be changed after upload —
        create a new artifact instead.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                html:
                  type: string
                  description: Replace HTML content (html format artifacts only)
                password:
                  type: string
                  nullable: true
                  description: Set to change password, null to remove
                requireEmail:
                  type: boolean
                allowedDomains:
                  type: array
                  items:
                    type: string
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
                allowNetwork:
                  type: boolean
                  description: Allow HTML/Markdown content to make external network requests
      responses:
        '200':
          description: Updated artifact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Artifacts]
      operationId: deleteArtifact
      summary: Delete artifact and files
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Artifact deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
                  id:
                    type: string
                    format: uuid
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/account:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Get usage and limits
      responses:
        '200':
          description: Account usage and plan limits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountResponse'
        '429':
          $ref: '#/components/responses/RateLimited'

  /p/{shortId}:
    get:
      tags: [Public]
      operationId: viewArtifact
      summary: View artifact wrapper page
      description: |
        Serves the HTML wrapper page that displays the artifact in the appropriate viewer
        (PDF.js, iframe, image viewer, etc.). If the artifact is gated, shows the
        unlock form instead.
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      responses:
        '200':
          description: HTML wrapper page
          content:
            text/html:
              schema:
                type: string
        '404':
          description: Artifact not found
          content:
            text/html:
              schema:
                type: string
        '410':
          description: Artifact expired or archived
          content:
            text/html:
              schema:
                type: string

  /p/{shortId}/content:
    get:
      tags: [Public]
      operationId: getContent
      summary: Stream artifact content
      description: |
        Streams the artifact's file bytes. The `Content-Type` header reflects the
        artifact's original MIME type (e.g. `application/pdf`, `text/html`), not
        `application/octet-stream`. The OpenAPI schema uses `application/octet-stream`
        for tooling compatibility only.

        If the artifact is gated, requires either the httpOnly cookie (browser) or
        `Authorization: Bearer <token>` header (programmatic).

        The Bearer token is obtained from the `POST /p/{shortId}/unlock` endpoint.
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      responses:
        '200':
          description: File content streamed with original MIME type
          headers:
            Content-Type:
              description: Original MIME type of the artifact (varies per file)
              schema:
                type: string
            Content-Length:
              description: File size in bytes (when known)
              schema:
                type: integer
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '403':
          description: Artifact is gated and no valid token provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: FORBIDDEN
                message: This artifact requires a password to view
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /p/{shortId}/download/original:
    get:
      tags: [Public]
      operationId: downloadOriginal
      summary: Download original file
      description: |
        Downloads the original uploaded file with `Content-Disposition: attachment`.
        If gated, requires the httpOnly cookie (browser) or
        `Authorization: Bearer <token>` header (programmatic).
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      responses:
        '200':
          description: Original file download
          headers:
            Content-Type:
              description: Original MIME type
              schema:
                type: string
            Content-Length:
              description: File size in bytes
              schema:
                type: integer
            Content-Disposition:
              description: 'attachment; filename="original-filename.ext"'
              schema:
                type: string
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '403':
          description: Artifact is gated and no valid token provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: FORBIDDEN
                message: This artifact requires a password to view
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /p/{shortId}/download/pdf:
    get:
      tags: [Public]
      operationId: downloadPdf
      summary: Download PDF version
      description: |
        Downloads the PDF conversion of the artifact (when available).
        For native PDFs, downloads the original. For converted formats (Phase 2),
        downloads the converted PDF. If gated, requires the httpOnly cookie (browser)
        or `Authorization: Bearer <token>` header (programmatic).
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      responses:
        '200':
          description: PDF file download
          headers:
            Content-Type:
              description: application/pdf
              schema:
                type: string
            Content-Length:
              description: File size in bytes
              schema:
                type: integer
            Content-Disposition:
              description: 'attachment; filename="filename.pdf"'
              schema:
                type: string
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '403':
          description: Artifact is gated and no valid token provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: FORBIDDEN
                message: This artifact requires a password to view
        '404':
          description: Artifact not found or no PDF conversion available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /p/{shortId}/unlock:
    post:
      tags: [Public]
      operationId: unlockArtifact
      summary: Unlock gated content
      description: |
        Unlock a password- or email-gated artifact. Returns access through **two mechanisms**:

        - **httpOnly cookie** — set automatically via `Set-Cookie` for browser use
        - **Bearer token** — returned in the JSON response body for programmatic/agent access

        Pass the Bearer token as `Authorization: Bearer <token>` on subsequent
        `/content` and `/download` requests.
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnlockRequest'
            examples:
              password:
                summary: Unlock with password
                value:
                  gate: password
                  value: secret123
              email:
                summary: Unlock with email
                value:
                  gate: email
                  value: viewer@acme.com
      responses:
        '200':
          description: Access granted
          headers:
            Set-Cookie:
              description: httpOnly access cookie for browser use
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnlockResponse'
              example:
                success: true
                token: eyJhbGciOiJIUzI1NiIs...
        '403':
          description: Wrong password or email domain not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/webhooks:
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Create webhook subscription (coming soon)
      description: |
        **Not yet implemented.** Webhook subscriptions are an account-level resource
        that will be available in Phase 2.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscription'
      responses:
        '501':
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: NOT_IMPLEMENTED
                message: Webhooks coming soon. See https://prismism.dev/docs for updates.
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhook subscriptions (coming soon)
      description: |
        **Not yet implemented.** Returns 501 until Phase 2.
      responses:
        '501':
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: NOT_IMPLEMENTED
                message: Webhooks coming soon. See https://prismism.dev/docs for updates.

  /v1/webhooks/{id}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete webhook subscription (coming soon)
      description: |
        **Not yet implemented.** Returns 501 until Phase 2.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '501':
          description: Not implemented
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: NOT_IMPLEMENTED
                message: Webhooks coming soon. See https://prismism.dev/docs for updates.

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        API key for programmatic access. Keys are prefixed with `pal_`.
        Get a key by creating an account via POST /v1/auth/register —
        the API key is returned once in the registration response and is
        not retrievable again. Store it securely.

  parameters:
    ShortId:
      name: shortId
      in: path
      required: true
      schema:
        type: string
      description: Public short ID slug from the artifact URL

  responses:
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          description: Seconds until the rate limit window resets
          schema:
            type: integer
        x-ratelimit-limit:
          description: Maximum requests allowed in the current window
          schema:
            type: integer
        x-ratelimit-remaining:
          description: Requests remaining in the current window
          schema:
            type: integer
        x-ratelimit-reset:
          description: UTC epoch seconds when the window resets
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: RATE_LIMIT_EXCEEDED
            message: Too many requests. Retry after 42 seconds.

  schemas:
    Artifact:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Internal UUID. Used in API paths.
        shortId:
          type: string
          description: Public slug. Used in /p/ URLs.
        url:
          type: string
          format: uri
          description: Full shareable URL (e.g. https://prismism.dev/p/AbCdEf12)
        title:
          type: string
        format:
          type: string
          enum: [html, pdf, markdown, image, text]
          description: Normalized file type
        renderer:
          type: string
          enum: [html_iframe, pdfjs, image, text, markdown_iframe]
          description: How the file is displayed in the wrapper
        status:
          type: string
          enum: [processing, converting, ready, failed]
          description: |
            Artifact processing status:
            - processing: Upload received, being stored
            - converting: Office format being converted to PDF (Phase 2)
            - ready: Available for viewing
            - failed: Processing or conversion error (see statusMessage)
        statusMessage:
          type: string
          nullable: true
          description: Error details when status is "failed". Null otherwise.
        fileSize:
          type: integer
          description: File size in bytes
        originalFilename:
          type: string
        mimeType:
          type: string
        viewCount:
          type: integer
        hasPassword:
          type: boolean
          description: Whether this artifact is password-gated. Use POST /p/{shortId}/unlock with gate type "password" to access.
        requireEmail:
          type: boolean
          description: Whether viewers must provide an email before viewing. Use POST /p/{shortId}/unlock with gate type "email" to access.
        isGated:
          type: boolean
          description: Whether any access gate is active (password, email, or both). Convenience field — true when hasPassword or requireEmail is true.
        allowedDomains:
          type: array
          items:
            type: string
          nullable: true
          description: Email domains allowed when requireEmail is true. Null means any domain.
        allowNetwork:
          type: boolean
          description: Whether HTML/Markdown content can make external network requests.
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ArtifactList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Artifact'
        pagination:
          type: object
          properties:
            page:
              type: integer
              description: Current page number
            limit:
              type: integer
              description: Items per page
            total:
              type: integer
              description: Total number of artifacts matching the query
            hasMore:
              type: boolean
              description: Whether more pages exist after the current one
      example:
        data:
          - id: d4f7a1b2-3c9e-4e8f-b5d6-1a2b3c4d5e6f
            shortId: AbCdEf12
            url: https://prismism.dev/p/AbCdEf12
            title: Q4 Report
            format: pdf
            renderer: pdfjs
            status: ready
            fileSize: 54321
            originalFilename: report.pdf
            mimeType: application/pdf
            viewCount: 42
            hasPassword: false
            requireEmail: false
            isGated: false
            allowedDomains: null
            allowNetwork: false
            expiresAt: null
            createdAt: '2026-01-15T10:30:00Z'
            updatedAt: '2026-01-15T10:30:00Z'
        pagination:
          page: 1
          limit: 20
          total: 1
          hasMore: false

    RegisterRequest:
      type: object
      required: [name, email, password]
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8

    RegisterResponse:
      type: object
      properties:
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            email:
              type: string
              format: email
            plan:
              type: string
        apiKey:
          type: object
          properties:
            id:
              type: string
              format: uuid
            key:
              type: string
              description: |
                Full API key prefixed with pal_. Returned ONLY in this response.
                Not retrievable via any other endpoint. Store securely.
            prefix:
              type: string
            name:
              type: string
            createdAt:
              type: string
              format: date-time

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string

    LoginResponse:
      type: object
      properties:
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            email:
              type: string
              format: email
            plan:
              type: string
        apiKeys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyMeta'

    ApiKeyFull:
      type: object
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
          description: Full API key (only shown once at creation)
        prefix:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time

    ApiKeyMeta:
      type: object
      properties:
        id:
          type: string
          format: uuid
        prefix:
          type: string
        name:
          type: string
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    CreateHtmlArtifactRequest:
      type: object
      required: [html]
      properties:
        html:
          type: string
          description: Raw HTML content
        title:
          type: string
        password:
          type: string
        requireEmail:
          type: boolean
          default: false
        allowedDomains:
          type: array
          items:
            type: string
        allowNetwork:
          type: boolean
          description: Allow HTML content to make external network requests
          default: false
        expiresAt:
          type: string
          format: date-time

    UnlockRequest:
      type: object
      required: [gate, value]
      properties:
        gate:
          type: string
          enum: [password, email]
          description: Which gate to unlock
        value:
          type: string
          description: The password or email address

    UnlockResponse:
      type: object
      properties:
        success:
          type: boolean
        token:
          type: string
          description: |
            Bearer token for programmatic access to gated content.
            Pass as `Authorization: Bearer <token>` on subsequent
            /content and /download requests.
            Browser users can ignore this — the httpOnly cookie
            is set automatically via Set-Cookie header.

    AccountResponse:
      type: object
      properties:
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
              format: email
            name:
              type: string
            plan:
              type: string
            createdAt:
              type: string
              format: date-time
        usage:
          type: object
          properties:
            artifacts:
              type: integer
            storageBytes:
              type: integer
            monthlyViews:
              type: integer
        limits:
          type: object
          properties:
            maxArtifacts:
              type: integer
            maxHtmlBytes:
              type: integer
            maxStorageBytes:
              type: integer
            maxMonthlyViews:
              type: integer
            maxApiKeys:
              type: integer
            rateLimitRpm:
              type: integer

    WebhookSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        url:
          type: string
          format: uri
          description: HTTPS URL to receive webhook events
        events:
          type: array
          items:
            type: string
            enum: [artifact.ready, artifact.failed, artifact.viewed, artifact.downloaded]
          description: Which events to subscribe to
        secret:
          type: string
          description: Shared secret for HMAC signature verification
        isEnabled:
          type: boolean
          default: true
        createdAt:
          type: string
          format: date-time
          readOnly: true

    Error:
      type: object
      properties:
        error:
          type: string
          description: Machine-readable error code
        message:
          type: string
          description: Human-readable description
