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.

    You can create additional keys with different permissions via `POST /v1/account/api-keys`.

    ## Scopes

    API keys support permission scopes (Phase 1):
    - `*` — Full access (read + write)
    - `read` — Read-only access (GET endpoints only)

    Keys with `read` scope will receive `403 Forbidden` on mutating endpoints (POST, PATCH, DELETE).

    ## 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 <token-from-unlock-response>" \
      --output confidential.pdf
    ```

    ## Agent Skills (AI Coding Agents)

    If you're an AI coding agent (Claude, Cursor, VS Code Copilot, Gemini CLI, etc.), install Prismism skills for expert-level API knowledge:

    ```
    npx skills add prismism-dev/prismism-skills
    ```

    This installs 4 sub-skills:
    - **publish-file** — Upload any file format to get a shareable link
    - **publish-html** — Publish raw HTML content directly
    - **gated-content** — Add password protection and email capture gates
    - **manage-artifacts** — List, update, and delete your artifacts

    Learn more: [github.com/prismism-dev/prismism-skills](https://github.com/prismism-dev/prismism-skills)

    ## MCP (Model Context Protocol)

    Prismism exposes a hosted **Streamable HTTP** MCP endpoint at `/mcp`.
    Any MCP-compatible client (Claude Desktop, Cursor, Windsurf, OpenAI agents)
    can connect directly — no npm install required.

    **Claude Desktop config:**

    ```json
    {
      "mcpServers": {
        "prismism": {
          "url": "https://prismism.dev/mcp",
          "headers": {
            "x-api-key": "pal_your_key_here"
          }
        }
      }
    }
    ```

    **Discovery:** `GET /.well-known/mcp/server-card.json` returns tool metadata for Smithery and other registries.

    **8 tools available:** `prismism_health`, `prismism_register`, `prismism_publish`,
    `prismism_list`, `prismism_get`, `prismism_update`, `prismism_delete`, `prismism_account`.

    ## 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 |
    | 403 | `PLAN_LIMIT_METADATA_KEYS` | Metadata keys exceed plan limit |
    | 403 | `PLAN_LIMIT_TAGS` | Tags exceed plan limit |
    | 422 | `INVALID_KEY_FORMAT` | Invalid metadata key format |
    | 422 | `EMPTY_VALUE` | Metadata key or value is empty |
    | 422 | `VALUE_TOO_LONG` | Metadata value exceeds 512 char limit |
    | 422 | `TAG_TOO_LONG` | Tag exceeds 100 char limit |
    | 422 | `PAYLOAD_TOO_LARGE` | Metadata payload exceeds 10KB size limit |
    | 422 | `RESERVED_KEY` | Metadata key uses reserved prefix (_) |
    | 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 |
    | `video/mp4` | .mp4 | 100–1000 MB* | video | video_player |

    *Video max size varies by plan: Free/Plus 100 MB, Pro 500 MB, Business 1 GB.
    Duration limit: 2 hours. Resolution limit: 4K. Supported codecs: H.264, H.265.

    ## Internal Webhook Events

    Prismism fires internal webhook notifications for key lifecycle events. These are
    fire-and-forget POST requests — they never block user-facing responses.

    | Event | Trigger | Payload Fields |
    |-------|---------|---------------|
    | `user.registered` | New account created via `POST /v1/auth/register` | `event`, `timestamp`, `user{id, name, email, plan}` |
    | `user.first_artifact` | User uploads their first artifact | `event`, `timestamp`, `user{id, name, email, plan}`, `artifact{id, title, format}` |

    Webhook URL is configured via the `WEBHOOK_NEW_USER_SIGNUP` environment variable.

  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, name} (password optional)
  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: Metadata
    description: Manage artifact tags and key-value metadata
  - name: Webhooks
    description: |
      Internal webhook notifications for lifecycle events (`user.registered`, `user.first_artifact`).
      User-facing webhook subscriptions are coming soon.
  - name: MCP
    description: |
      Model Context Protocol (MCP) Streamable HTTP endpoint.
      Connect Claude Desktop, Cursor, Windsurf, or any MCP client directly.
  - name: System
    description: Health checks and system status

paths:
  /mcp:
    post:
      tags: [MCP]
      operationId: mcpPost
      summary: MCP Streamable HTTP endpoint
      description: |
        JSON-RPC 2.0 endpoint for the Model Context Protocol.
        Stateless — each request creates a fresh server instance.
        Pass API key via `x-api-key` header for authenticated tools.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                jsonrpc:
                  type: string
                  enum: ["2.0"]
                method:
                  type: string
                  description: "JSON-RPC method (initialize, tools/list, tools/call)"
                params:
                  type: object
                id:
                  oneOf:
                    - type: string
                    - type: integer
              required: [jsonrpc, method, id]
      responses:
        "200":
          description: JSON-RPC response (as SSE text/event-stream)
          content:
            text/event-stream:
              schema:
                type: string
        "405":
          description: Method not allowed (GET/DELETE)
    get:
      tags: [MCP]
      operationId: mcpGet
      summary: MCP endpoint — GET not supported
      description: Returns 405. Use POST for MCP JSON-RPC.
      security: []
      responses:
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [MCP]
      operationId: mcpDelete
      summary: MCP endpoint — DELETE not supported
      description: Returns 405. Stateless server — sessions are not tracked.
      security: []
      responses:
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /.well-known/mcp/server-card.json:
    get:
      tags: [MCP]
      operationId: mcpServerCard
      summary: MCP server discovery card
      description: Returns server metadata and tool list for Smithery and other MCP registries.
      security: []
      responses:
        "200":
          description: Server card with tool metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  description:
                    type: string
                  url:
                    type: string
                  transport:
                    type: string
                  tools:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        description:
                          type: string
                        requiresAuth:
                          type: boolean
  /v1/health:
    get:
      tags: [System]
      operationId: health
      summary: Health check
      security: []
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: "1.0.0"
  /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'
                docs: https://prismism.dev/docs
                agentSkills: "npx skills add prismism-dev/prismism-skills"
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: EMAIL_EXISTS
                  message:
                    type: string
                    example: "An account with this email already exists. Log in to create a new API key."
                  createKey:
                    type: string
                    description: Direct URL to the API key management page. The agent should share this with the user.
                    example: "https://prismism.dev/settings/api-keys"
                  docs:
                    type: string
                    description: Link to API documentation
                    example: "https://prismism.dev/docs"
                  agentSkills:
                    type: string
                    description: Install command for AI agent skills
                    example: "npx skills add prismism-dev/prismism-skills"

  /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'
        '403':
          description: Account created via API without a password
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Password not set
                  message:
                    type: string
                    example: This account was created via API. Set a password to access the dashboard.
                  action:
                    type: string
                    example: set_password

  /v1/auth/set-password:
    post:
      tags: [Auth]
      operationId: setPassword
      summary: Send set-password email
      description: |
        Sends a "set your password" email to the given address. Useful for accounts created
        via the API without a password. Uses the same token infrastructure as forgot-password.
        Always returns 200 regardless of whether the email exists (prevents enumeration).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
            example:
              email: agent@example.com
      responses:
        '200':
          description: If the account exists, an email has been sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
              example:
                message: "If an account with that email exists, we've sent a link to set your password."

  /v1/auth/me:
    get:
      tags: [Auth]
      operationId: getMe
      summary: Get current user info
      description: Returns the authenticated user's profile. Requires a valid session cookie.
      security: []
      responses:
        '200':
          description: Current user info
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  email:
                    type: string
                    format: email
                  name:
                    type: string
                  plan:
                    type: string
                    description: "User's current plan: free, plus, pro, or business"
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/account/api-keys:
    get:
      tags: [API Keys]
      operationId: listApiKeys
      summary: List API keys
      description: Returns metadata for all API keys belonging to the authenticated user. Key hashes are never exposed.
      responses:
        '200':
          description: List of API key metadata
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyMeta'
    post:
      tags: [API Keys]
      operationId: createApiKey
      summary: Create a new API key
      description: |
        Creates a new API key. The full key is returned **once** in the response and cannot be retrieved again.
        Keys are hashed with SHA-256 before storage.

        **Scopes (Phase 1):**
        - `*` — full access (read + write)
        - `read` — read-only access (GET endpoints only)

        **Expiration:** Optional. If set, the key becomes invalid after the specified duration.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyFull'
              example:
                id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                key: pal_abc123def456abc123def456abc123def456abc123def456abc123def456abcd
                prefix: pal_abc123de
                name: ci-pipeline
                scopes: ['*']
                expiresAt: null
                createdAt: '2026-01-15T10:30:00Z'
        '403':
          description: API key limit reached for current plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/account/api-keys/{id}:
    patch:
      tags: [API Keys]
      operationId: renameApiKey
      summary: Rename an API key
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 64
                  description: New display name for the key
                  example: production-agent
      responses:
        '200':
          description: API key renamed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyMeta'
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags: [API Keys]
      operationId: deleteApiKey
      summary: Delete an API key
      description: Permanently revokes the key. Integrations using it will immediately stop working.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: API key deleted
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/auth/api-keys:
    get:
      tags: [Auth]
      operationId: listApiKeysLegacy
      summary: List API keys (deprecated)
      deprecated: true
      description: "**Deprecated.** Use `GET /v1/account/api-keys` instead."
      responses:
        '200':
          description: List of API key metadata
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyMeta'

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

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

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

        **Markdown (easiest for agents):** Send as `application/json` with a `markdown` field.

        You may send `html` OR `markdown`, but not both. If neither a file nor inline content is provided, you'll get a helpful error.

        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"]'
                allowedEmails:
                  type: string
                  description: |
                    JSON-encoded array of specific email addresses allowed to view.
                    When set, only these verified emails can access the artifact.
                    Requires Pro plan or above. Pass as a JSON string.
                  example: '["jane@acme.com", "bob@investors.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'
                tags:
                  type: string
                  description: |
                    JSON-encoded array of tags for categorization. Pass as a JSON string.
                    Max 20 tags, each max 64 characters.
                  example: '["report", "q4"]'
                metadata:
                  type: string
                  description: |
                    JSON-encoded object of key-value metadata pairs. Pass as a JSON string.
                    Keys: lowercase alphanumeric with hyphens/underscores, max 40 chars.
                    Values: strings, max 512 chars. Max 50 keys.
                  example: '{"department": "engineering", "project": "alpha"}'
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInlineArtifactRequest'
            examples:
              html:
                summary: Create from HTML
                value:
                  title: My Report
                  html: <h1>Hello</h1><p>World</p>
                  requireEmail: false
              markdown:
                summary: Create from Markdown (easiest for agents)
                value:
                  title: My Doc
                  markdown: "# Hello\n\nThis is **markdown** content."
      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
        - name: tag
          in: query
          schema:
            type: string
          description: Filter by tag. Returns artifacts that have this tag. Available on all plans.
        - name: metadata[key]
          in: query
          schema:
            type: string
          description: |
            Filter by metadata key-value pair. Replace `key` with the metadata key name
            (e.g. `?metadata[department]=engineering`). Pro plan and above only — returns
            403 for Free and Plus plans.
        - name: hints
          in: query
          schema:
            type: boolean
            default: false
          description: When true, include `_hints` object in each artifact response with contextual guidance.
      responses:
        '200':
          description: Paginated list of artifacts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtifactList'
        '403':
          description: Metadata filtering requires Pro plan or above
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: FORBIDDEN
                message: "Metadata filtering requires Pro plan or above"
        '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
        - name: hints
          in: query
          schema:
            type: boolean
            default: false
          description: When true, include `_hints` object in the response with contextual guidance.
      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
      description: |
        Update an artifact's metadata or content. You can replace content by sending
        `html` or `markdown` (but not both).
      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 content with HTML. Mutually exclusive with `markdown`.
                markdown:
                  type: string
                  description: Replace content with Markdown. Mutually exclusive with `html`.
                password:
                  type: string
                  nullable: true
                  description: Set to change password, null to remove
                requireEmail:
                  type: boolean
                allowedDomains:
                  type: array
                  items:
                    type: string
                  nullable: true
                allowedEmails:
                  type: array
                  items:
                    type: string
                    format: email
                  nullable: true
                  description: Specific email addresses allowed to view. Requires Pro plan or above.
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
                allowNetwork:
                  type: boolean
                  description: Allow HTML/Markdown content to make external network requests
                allowDownload:
                  type: boolean
                  description: Whether viewers can download the original file
                tags:
                  type: array
                  items:
                    type: string
                  description: Replace the artifact's tags array. Max 20 tags, each max 64 characters.
                metadata:
                  type: object
                  additionalProperties:
                    type: string
                  description: |
                    Full replacement of metadata key-value pairs. Keys must be lowercase alphanumeric with
                    hyphens/underscores (max 40 chars). Values are strings (max 512 chars).
                    Set a value to null to delete that key. Max 50 keys total.
      responses:
        '200':
          description: Updated artifact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '403':
          description: Plan limit exceeded for metadata keys or tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                metadataKeysLimit:
                  summary: Metadata keys limit exceeded
                  value:
                    error: PLAN_LIMIT_METADATA_KEYS
                    message: "Metadata exceeds the 10 key limit for your plan"
                    _hints:
                      upgrade: "Upgrade to a higher plan for more metadata keys. See /pricing"
                tagsLimit:
                  summary: Tags limit exceeded
                  value:
                    error: PLAN_LIMIT_TAGS
                    message: "Tags exceed the 5 tag limit for your plan"
                    _hints:
                      upgrade: "Upgrade to a higher plan for more tags. See /pricing"
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error on metadata or tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidKeyFormat:
                  summary: Invalid metadata key format
                  value:
                    error: INVALID_KEY_FORMAT
                    message: "Metadata key contains invalid characters. Only alphanumeric, underscores, and hyphens allowed."
                emptyValue:
                  summary: Empty metadata value
                  value:
                    error: EMPTY_VALUE
                    message: "Metadata value cannot be empty"
                valueTooLong:
                  summary: Metadata value too long
                  value:
                    error: VALUE_TOO_LONG
                    message: "Metadata value exceeds 512 character limit"
                tagTooLong:
                  summary: Tag too long
                  value:
                    error: TAG_TOO_LONG
                    message: "Tag exceeds 100 character limit"
                payloadTooLarge:
                  summary: Metadata payload too large
                  value:
                    error: PAYLOAD_TOO_LARGE
                    message: "Metadata payload exceeds the 10KB size limit"
                reservedKey:
                  summary: Reserved key prefix
                  value:
                    error: RESERVED_KEY
                    message: "Metadata key uses a reserved prefix (_)"
        '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/artifacts/{id}/content:
    get:
      tags: [Artifacts]
      operationId: getArtifactContent
      summary: Get raw artifact content
      description: |
        Returns the raw stored file content for an artifact. Supports public access,
        gate negotiation via 402 responses, and authenticated owner bypass.

        **Authentication is optional.** Public artifacts (no password, email gate, or domain
        restriction) are served without any credentials. Gated artifacts return structured
        402 responses with `_hints` that guide agents through the gate negotiation flow.

        **Owner API key access** bypasses all gates unconditionally. Non-owner API keys
        receive no special treatment and are subject to all gates.

        The `:id` parameter accepts UUID, `art_`-prefixed ID, or shortId (the 8-char code
        from share links). Markdown artifacts return HTML (converted on upload).

        **Gate negotiation flow (sequential):**
        1. Password gate: if artifact has a password, supply via `X-Prismism-Password` header
           or `?password=` query param. Header takes precedence.
        2. Email gate: if artifact requires email, supply via `X-Prismism-Email` header
           or `?email=` query param. Header takes precedence. Email may be checked against
           an allowlist or domain allowlist.
        3. Domain restriction: Origin-based browser restriction. If `Origin` header is present
           and not in the allowlist, returns 403. Requests without `Origin` (API/agent traffic)
           pass this check.

        **Rate limiting:** Unauthenticated requests are limited to 30 req/min per IP.
        Consecutive 404s (5 within 60s) trigger a 5-minute block.
      security:
        - ApiKeyAuth: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Artifact UUID, art_-prefixed ID, or shortId from share link
        - name: password
          in: query
          required: false
          schema:
            type: string
          description: Password for password-protected artifacts. X-Prismism-Password header is preferred.
        - name: email
          in: query
          required: false
          schema:
            type: string
            format: email
          description: Email address for email-gated artifacts. X-Prismism-Email header is preferred.
        - name: X-Prismism-Password
          in: header
          required: false
          schema:
            type: string
          description: Password for password-protected artifacts. Takes precedence over query param.
        - name: X-Prismism-Email
          in: header
          required: false
          schema:
            type: string
            format: email
          description: Email address for email-gated artifacts. Takes precedence over query param.
      responses:
        '200':
          description: Raw file content with appropriate Content-Type header
          headers:
            X-Original-Format:
              schema:
                type: string
                enum: [html, markdown, pdf, image, video, text]
              description: The original format of the artifact content
            Content-Disposition:
              schema:
                type: string
              description: Set to "attachment" for HTML and markdown artifacts (XSS prevention). Omitted for other formats.
            Cache-Control:
              description: >
                max-age=60, public for public artifacts; no-store for gated or owner access.
              schema:
                type: string
          content:
            text/html:
              schema:
                type: string
            application/pdf:
              schema:
                type: string
                format: binary
            image/*:
              schema:
                type: string
                format: binary
            video/mp4:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
        '402':
          description: Gate requires credentials. Follow _hints to negotiate access.
          headers:
            Cache-Control:
              schema:
                type: string
                enum: ["no-store"]
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum: [PASSWORD_REQUIRED, INVALID_PASSWORD, EMAIL_REQUIRED]
                  _hints:
                    type: object
                    properties:
                      unlock:
                        type: string
                        description: Human-readable instruction for resolving the gate
              examples:
                passwordRequired:
                  summary: Artifact requires a password
                  value:
                    error: PASSWORD_REQUIRED
                    _hints:
                      unlock: "This artifact is password protected. Ask the user for the password and retry with the X-Prismism-Password header (preferred) or ?password=<value> query param."
                invalidPassword:
                  summary: Incorrect password provided
                  value:
                    error: INVALID_PASSWORD
                    _hints:
                      unlock: "The password provided was incorrect. Ask the user to double-check and retry with the X-Prismism-Password header or ?password=<value>."
                emailRequired:
                  summary: Artifact requires an email address
                  value:
                    error: EMAIL_REQUIRED
                    _hints:
                      unlock: "This artifact requires an email address to unlock. Ask the user if you can share their email, then retry with the X-Prismism-Email header (preferred) or ?email=<value> query param."
        '403':
          description: Domain restriction — request origin not in allowlist
          headers:
            Cache-Control:
              schema:
                type: string
                enum: ["no-store"]
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum: [DOMAIN_RESTRICTED]
                  _hints:
                    type: object
                    properties:
                      unlock:
                        type: string
              examples:
                domainRestricted:
                  summary: Origin not in domain allowlist
                  value:
                    error: DOMAIN_RESTRICTED
                    _hints:
                      unlock: "This artifact is restricted to specific domains. Access is not available from this origin. The user may need to open it directly in a browser."
        '404':
          description: Artifact not found, archived, or expired
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum: [ARTIFACT_NOT_FOUND]
        '429':
          description: Rate limit exceeded (IP-based for unauthenticated requests, or 404 throttle)
        '500':
          description: Internal server error or content unavailable

  /v1/artifacts/{id}/metadata/{key}:
    patch:
      tags: [Metadata]
      operationId: setMetadataKey
      summary: Set or delete a single metadata key
      description: |
        Set a metadata key-value pair on an artifact. Send `{"value": "string"}` to set,
        or `{"value": null}` to delete the key. Requires auth with write scope.

        Key format: lowercase alphanumeric with hyphens and underscores, max 40 characters.
        Value max length: 512 characters. Max 50 keys per artifact.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Artifact UUID
        - name: key
          in: path
          required: true
          schema:
            type: string
            pattern: '^[a-z0-9][a-z0-9_-]{0,39}$'
          description: Metadata key name (lowercase alphanumeric with hyphens/underscores, max 40 chars)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:
                  type: string
                  nullable: true
                  description: The value to set, or null to delete the key.
                  maxLength: 512
            examples:
              set:
                summary: Set a metadata value
                value:
                  value: engineering
              delete:
                summary: Delete a metadata key
                value:
                  value: null
      responses:
        '200':
          description: Updated metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetadataResponse'
        '403':
          description: Plan limit exceeded or insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                planLimit:
                  summary: Metadata keys limit exceeded
                  value:
                    error: PLAN_LIMIT_METADATA_KEYS
                    message: "Metadata exceeds the 10 key limit for your plan"
                    _hints:
                      upgrade: "Upgrade to a higher plan for more metadata keys. See /pricing"
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Invalid key format, empty value, or payload too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidKey:
                  summary: Invalid key format
                  value:
                    error: INVALID_KEY_FORMAT
                    message: "Metadata key must be lowercase alphanumeric with hyphens/underscores (max 40 chars)"
                emptyValue:
                  summary: Empty value
                  value:
                    error: EMPTY_VALUE
                    message: "Metadata value must not be empty. Use null to delete the key."
                valueTooLong:
                  summary: Value too long
                  value:
                    error: VALUE_TOO_LONG
                    message: "Metadata value exceeds 512 character limit"
                reservedKey:
                  summary: Reserved key prefix
                  value:
                    error: RESERVED_KEY
                    message: "Metadata key uses a reserved prefix (_)"
                payloadTooLarge:
                  summary: Metadata payload too large
                  value:
                    error: PAYLOAD_TOO_LARGE
                    message: "Metadata payload exceeds the 10KB size limit"
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Metadata]
      operationId: deleteMetadataKey
      summary: Delete a single metadata key
      description: |
        Remove a metadata key from an artifact. If the key does not exist, this is a no-op
        and still returns 200. Requires auth with write scope.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Artifact UUID
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Metadata key name to delete
      responses:
        '200':
          description: Updated metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetadataResponse'
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/artifacts/{id}/tags:
    patch:
      tags: [Metadata]
      operationId: replaceTags
      summary: Replace tags array
      description: |
        Replace the entire tags array on an artifact. Requires auth with write scope.
        Max 20 tags, each max 64 characters. Tags are case-sensitive strings.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Artifact UUID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tags]
              properties:
                tags:
                  type: array
                  items:
                    type: string
                    maxLength: 64
                  maxItems: 20
                  description: New tags array (replaces existing tags)
            example:
              tags: ["report", "q4", "finance"]
      responses:
        '200':
          description: Updated metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetadataResponse'
        '403':
          description: Plan limit exceeded or insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                planLimit:
                  summary: Tags limit exceeded
                  value:
                    error: PLAN_LIMIT_TAGS
                    message: "Tags exceed the 5 tag limit for your plan"
                    _hints:
                      upgrade: "Upgrade to a higher plan for more tags. See /pricing"
        '404':
          description: Artifact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Invalid tag format or too many tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                emptyTag:
                  summary: Empty tag
                  value:
                    error: EMPTY_VALUE
                    message: "Tag cannot be empty"
                tagTooLong:
                  summary: Tag too long
                  value:
                    error: TAG_TOO_LONG
                    message: "Tag exceeds 100 character limit"
        '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}/poster:
    get:
      tags: [Public]
      operationId: getVideoPoster
      summary: Get video poster thumbnail
      description: |
        Returns the poster/thumbnail image for a video artifact. This endpoint
        is ungated (no password or email required) as the poster serves as a preview.
        Returns 404 for non-video artifacts or artifacts without a poster.
        Aggressively cached (1 year, immutable).
      security: []
      parameters:
        - $ref: '#/components/parameters/ShortId'
      responses:
        '200':
          description: Poster JPEG image
          headers:
            Cache-Control:
              description: 'public, max-age=31536000, immutable'
              schema:
                type: string
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
        '404':
          description: Artifact not found or has no poster

  /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.

        **Note:** The returned token is not a JWT. Treat it as an opaque string — pass it as-is in the Authorization header. Do not attempt to decode or parse it.
      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: AbCdEf12:1700000000:a1b2c3d4e5f6...
        '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
      description: |
        Create a new webhook subscription. The secret is returned once in the response —
        store it securely for HMAC signature verification. Plan limits apply (Free=1, Plus=3, Pro=10, Business=25).
        Event types: `artifact.created`, `artifact.updated`, `artifact.deleted` (all plans),
        `artifact.viewed`, `artifact.downloaded` (Plus+), `artifact.email_captured` (Pro+).
        Filters (Pro+): optional tags[] and metadata{} with AND semantics.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [artifact.created, artifact.updated, artifact.deleted, artifact.viewed, artifact.downloaded, artifact.email_captured]
                filters:
                  type: object
                  properties:
                    tags:
                      type: array
                      items:
                        type: string
                    metadata:
                      type: object
                      additionalProperties:
                        type: string
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: wh_a1b2c3d4
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  secret:
                    type: string
                    description: Shown once. Store securely for HMAC verification.
                  active:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
        '403':
          description: Plan limit reached or insufficient entitlements
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhook subscriptions
      description: Returns all webhooks for the authenticated user. Secrets are omitted.
      responses:
        '200':
          description: List of webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        url:
                          type: string
                        events:
                          type: array
                          items:
                            type: string
                        active:
                          type: boolean
                        createdAt:
                          type: string
                          format: date-time

  /v1/webhooks/{id}:
    get:
      tags: [Webhooks]
      operationId: getWebhook
      summary: Get webhook details
      description: Returns webhook details with delivery summary (last 10 deliveries, success/fail counts).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Webhook ID (UUID or wh_-prefixed)
      responses:
        '200':
          description: Webhook details with delivery summary
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      summary: Update webhook
      description: Update url, events, filters, and/or active status.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                filters:
                  type: object
                active:
                  type: boolean
      responses:
        '200':
          description: Updated webhook
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete webhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Webhook deleted

  /v1/webhooks/{id}/rotate-secret:
    post:
      tags: [Webhooks]
      operationId: rotateWebhookSecret
      summary: Rotate webhook secret
      description: Generates a new secret. Old secret is immediately invalidated. New secret returned once.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: New secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string

  /v1/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Send test payload
      description: |
        Send a synthetic test payload to the webhook URL. Does not create a delivery record.
        Available on all plans.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [artifact.created, artifact.updated, artifact.deleted, artifact.viewed, artifact.downloaded, artifact.email_captured]
      responses:
        '200':
          description: Test result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  httpStatus:
                    type: integer
                    nullable: true
                  responseBody:
                    type: string
                  latencyMs:
                    type: integer

  /v1/webhooks/{id}/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Delivery history
      description: Paginated delivery log for a webhook. Plus+ only.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Delivery history
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        eventType:
                          type: string
                        httpStatus:
                          type: integer
                          nullable: true
                        latencyMs:
                          type: integer
                          nullable: true
                        attempt:
                          type: integer
                        deliveredAt:
                          type: string
                          format: date-time
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '403':
          description: Free plan — upgrade required

  /v1/artifacts/{id}/views:
    get:
      tags: [Analytics]
      operationId: listArtifactViews
      summary: Raw view events
      description: |
        Returns raw view events for an artifact. Plus+ only (Free gets 403).
        Rate limited: 60 req/min per user.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Artifact ID (UUID or art_-prefixed)
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - name: contact_id
          in: query
          schema:
            type: string
        - name: utm_source
          in: query
          schema:
            type: string
        - name: utm_campaign
          in: query
          schema:
            type: string
      responses:
        '200':
          description: View events
          content:
            application/json:
              schema:
                type: object
                properties:
                  views:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        sessionId:
                          type: string
                        viewedAt:
                          type: string
                          format: date-time
                        duration:
                          type: integer
                          nullable: true
                        contact:
                          type: object
                          nullable: true
                        utm:
                          type: object
                        referrer:
                          type: string
                          nullable: true
                        location:
                          type: object
                        device:
                          type: object
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer

  /v1/analytics:
    get:
      tags: [Analytics]
      operationId: getAccountAnalytics
      summary: Account-wide or tag-scoped analytics
      description: |
        Without `?tag=`: account-wide roll-up with top artifacts, UTM breakdowns, daily views.
        With `?tag=X`: tag-scoped roll-up. Free plan gets summary only (account) or 403 (tag).
        Rate limited: 60 req/min per user.
      parameters:
        - name: tag
          in: query
          schema:
            type: string
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Analytics roll-up
        '403':
          description: Free plan (tag roll-up) — upgrade required

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key passed in the X-API-Key header. Get your key from the dashboard
        at https://prismism.dev/dashboard/settings or by registering via
        POST /v1/auth/register. Keys are prefixed with `pal_`.

        **Important:** Use `X-API-Key` header, NOT `Authorization: Bearer`.

        ```
        X-API-Key: pal_your_key_here
        ```

  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, video]
          description: Normalized file type
        renderer:
          type: string
          enum: [html_iframe, pdfjs, image, text, markdown_iframe, video_player]
          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.
        allowedEmails:
          type: array
          items:
            type: string
            format: email
          nullable: true
          description: Specific email addresses allowed to view. Requires Pro plan or above. Null means no email-level restriction.
        allowNetwork:
          type: boolean
          description: Whether HTML/Markdown content can make external network requests.
        allowDownload:
          type: boolean
          description: Whether viewers can download the original file. Only applies to video artifacts.
        durationSeconds:
          type: number
          nullable: true
          description: Video duration in seconds. Null for non-video artifacts.
        width:
          type: integer
          nullable: true
          description: Video width in pixels. Null for non-video artifacts.
        height:
          type: integer
          nullable: true
          description: Video height in pixels. Null for non-video artifacts.
        hasAudio:
          type: boolean
          nullable: true
          description: Whether the video has an audio track. Null for non-video artifacts.
        posterUrl:
          type: string
          format: uri
          nullable: true
          description: URL to the video poster/thumbnail image. Null for non-video artifacts.
        playCount:
          type: integer
          description: Number of times the video has been played. Only applicable to video artifacts.
        completionCount:
          type: integer
          description: Number of times the video has been watched to completion. Only applicable to video artifacts.
        tags:
          type: array
          items:
            type: string
          description: User-defined tags for categorization and filtering.
          example: ["report", "q4", "finance"]
        metadata:
          type: object
          additionalProperties:
            type: string
          description: User-defined key-value pairs. Keys must be lowercase alphanumeric with hyphens/underscores (max 40 chars). Values are strings (max 512 chars).
          example:
            department: engineering
            project: alpha
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        _hints:
          type: object
          description: Contextual hints about optional features relevant to this artifact. Only included when hints are applicable (e.g., HTML artifacts with allowNetwork=false).
          additionalProperties:
            type: string
          example:
            allowNetwork: "This HTML will not load external CSS, JS, or fonts. Set allowNetwork=true during upload or via PATCH if needed."

    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]
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
          description: Optional. If omitted, a passwordless account is created and a "set your password" email is sent automatically. The user can set a password later to access the dashboard.

    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
        docs:
          type: string
          description: Link to the API documentation
          example: "https://prismism.dev/docs"
        agentSkills:
          type: string
          description: Install command for AI agent skills (provides expert-level API knowledge)
          example: "npx skills add prismism-dev/prismism-skills"
        _hints:
          type: object
          properties:
            email:
              type: string
              description: Guidance for AI agents about using real user emails
              example: "If acting on behalf of a user, register with their real email. They can set a password later to access the dashboard and manage API keys."

    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'

    CreateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 64
          description: Friendly display name for the key
          example: ci-pipeline
        scopes:
          type: array
          items:
            type: string
            enum: ['*', read]
          default: ['*']
          description: "Permission scopes. Phase 1: `*` (full access) or `read` (GET only)"
        expiresIn:
          type: string
          enum: [30d, 90d, 365d]
          nullable: true
          description: Optional expiration duration. Null means the key never expires.
          example: 90d

    ApiKeyFull:
      type: object
      description: Returned once at creation. The `key` field is never shown again.
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
          description: "Full API key (SHA-256 hashed before storage). Format: `pal_` + 64 hex chars."
        prefix:
          type: string
          description: First 12 characters of the key, used for identification.
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    ApiKeyMeta:
      type: object
      description: API key metadata. The key hash is never exposed.
      properties:
        id:
          type: string
          format: uuid
        prefix:
          type: string
          description: First 12 characters of the key
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    CreateInlineArtifactRequest:
      type: object
      required: [title]
      description: |
        Send `html` OR `markdown` (not both). At least one content field is required.
      properties:
        title:
          type: string
          description: Display name for the artifact
        html:
          type: string
          description: Raw HTML content. Mutually exclusive with `markdown`.
        markdown:
          type: string
          description: Raw Markdown content. Mutually exclusive with `html`. Easiest option for AI agents.
        password:
          type: string
        requireEmail:
          type: boolean
          default: false
        allowedDomains:
          type: array
          items:
            type: string
        allowedEmails:
          type: array
          items:
            type: string
            format: email
          description: Specific email addresses allowed to view. Requires Pro plan or above.
        allowNetwork:
          type: boolean
          description: Allow content to make external network requests
          default: false
        expiresAt:
          type: string
          format: date-time
        tags:
          type: array
          items:
            type: string
            maxLength: 64
          maxItems: 20
          description: Tags for categorization and filtering. Max 20 tags, each max 64 characters.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: |
            Key-value metadata pairs. Keys: lowercase alphanumeric with hyphens/underscores,
            max 40 chars. Values: strings, max 512 chars. Max 50 keys.

    MetadataResponse:
      type: object
      description: Response returned by metadata and tags endpoints.
      properties:
        tags:
          type: array
          items:
            type: string
          description: Current tags array for the artifact
        metadata:
          type: object
          additionalProperties:
            type: string
          description: Current metadata key-value pairs for the artifact
        _hints:
          type: object
          additionalProperties:
            type: string
          description: Optional contextual hints about metadata features and limits
      example:
        tags: ["report", "q4"]
        metadata:
          department: engineering
          project: alpha
        _hints:
          limits: "Free plan: 5 tags, 10 metadata keys. Upgrade for more."

    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
      required: [url, events]
      properties:
        id:
          type: string
          description: Prefixed webhook ID (wh_...)
          readOnly: true
        url:
          type: string
          format: uri
          description: URL to receive webhook events
        events:
          type: array
          items:
            type: string
            enum: [artifact.created, artifact.updated, artifact.deleted, artifact.viewed, artifact.downloaded, artifact.email_captured]
          description: Event types to subscribe to
        filters:
          type: object
          description: Optional event filters (Pro+ only). AND semantics.
          properties:
            tags:
              type: array
              items:
                type: string
            metadata:
              type: object
              additionalProperties:
                type: string
        secret:
          type: string
          description: whs_-prefixed secret for HMAC-SHA256 verification. Shown once on create/rotate.
        active:
          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
