> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hq.zone/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload a document

> Uploads a document by sending its bytes inline as base64 in the JSON body (capped at
roughly 12 MiB of file content), together with filename, content type, scope
(private, channel, or team; defaults to private), and optional channel_id, category,
tags, and caption. The operation is content-addressed and idempotent per owner and
scope: re-uploading identical bytes returns the existing document rather than
creating a duplicate, and any newly supplied tags, caption, or category are merged
onto that existing row (reflected by deduplicated and enriched flags in the
response). channel_id is required when scope is channel and rejected otherwise.
Returns the document id, download URL, SHA-256, and size. Requires the
documents:write scope.



## OpenAPI

````yaml POST /v1/api/documents
openapi: 3.1.0
info:
  title: HQ API
  description: >-
    Public HTTP API for HQ. Authenticate with a Personal Access Token
    (`Authorization: Bearer hq_pat_...`) for server-side integrations, or an
    OAuth 2.1 authorization-code + PKCE flow for browser apps acting on a user's
    behalf. Both grant from the same resource:action scope vocabulary; an
    endpoint's required scope is listed under its `security`.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
  version: 1.0.0
servers:
  - url: https://api.hq.zone
    description: HQ API (production)
security: []
tags:
  - name: me
    description: The signed-in user's own account
  - name: conversations
    description: Conversations and their messages
  - name: documents
    description: The content-addressed documents library
  - name: schedules
    description: Scheduled prompts and recurring tasks
  - name: agents
    description: Agents, their skills and integrations
  - name: memory
    description: What the assistant remembers (L5 governance)
  - name: tokens
    description: Personal Access Token management
  - name: billing
    description: Usage and billing
  - name: notifications
    description: In-app notification center
  - name: admin
    description: Workspace administration
  - name: integrations
    description: Workspace integrations (Slack, MCP, skills)
  - name: onboarding
    description: New-workspace onboarding wizard
  - name: auth
    description: Sign-in, sessions, and OAuth
paths:
  /v1/api/documents:
    post:
      tags:
        - documents
      summary: Upload a document
      description: >-
        Uploads a document by sending its bytes inline as base64 in the JSON
        body (capped at

        roughly 12 MiB of file content), together with filename, content type,
        scope

        (private, channel, or team; defaults to private), and optional
        channel_id, category,

        tags, and caption. The operation is content-addressed and idempotent per
        owner and

        scope: re-uploading identical bytes returns the existing document rather
        than

        creating a duplicate, and any newly supplied tags, caption, or category
        are merged

        onto that existing row (reflected by deduplicated and enriched flags in
        the

        response). channel_id is required when scope is channel and rejected
        otherwise.

        Returns the document id, download URL, SHA-256, and size. Requires the

        documents:write scope.
      operationId: upload_document
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentUploadReq'
        required: true
      responses:
        '200':
          description: Document saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUploadResp'
        '400':
          description: Invalid base64, invalid scope, or empty filename
      security:
        - bearer_pat:
            - documents:write
        - oauth2:
            - documents:write
components:
  schemas:
    DocumentUploadReq:
      type: object
      required:
        - filename
        - content_type
        - body_b64
      properties:
        body_b64:
          type: string
          description: |-
            Base64-encoded payload. 16 MiB cap on the JSON body limits raw
            to ~12 MiB - the multipart streaming path lands in pass 2.
        caption:
          type:
            - string
            - 'null'
        category:
          type:
            - string
            - 'null'
          description: |-
            Free-form user category; the auto-categoriser may overwrite
            this in a follow-up worker pass once we have Haiku wired.
        channel_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Required iff `scope == "channel"`.
        content_type:
          type: string
        filename:
          type: string
        scope:
          type: string
          description: '`private` | `channel` | `team`. Defaults to `private`.'
        tags:
          type: array
          items:
            type: string
    DocumentUploadResp:
      type: object
      required:
        - document_id
        - download_url
        - sha256
        - size_bytes
        - deduplicated
      properties:
        deduplicated:
          type: boolean
          description: |-
            True iff this hashed identical to an existing document in this
            tenant - we still record the new metadata row but the Trove
            object is shared. The UI surfaces this as a "saved (dedup'd
            against existing copy)" hint.
        document_id:
          type: string
          format: uuid
        download_url:
          type: string
        enriched:
          type: boolean
          description: |-
            True iff this was a same-owner/same-scope dedup AND the caller
            supplied new metadata (tags/caption/category) that we actually
            merged onto the existing row. Lets the UI say "already in your
            library - details updated" honestly vs a bare "already there".
        sha256:
          type: string
        size_bytes:
          type: integer
          format: int64
          minimum: 0
  securitySchemes:
    bearer_pat:
      type: http
      scheme: bearer
      bearerFormat: hq_pat
      description: 'Personal Access Token. Send as `Authorization: Bearer hq_pat_...`.'
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://app.hq.zone/v1/oauth/authorize
          tokenUrl: https://api.hq.zone/v1/oauth/token
          refreshUrl: https://api.hq.zone/v1/oauth/token
          scopes:
            admin: Administer the workspace (users, settings, integrations)
            agents:read: View the agents in your workspace
            agents:write: Create and configure agents
            billing:read: View usage and billing information
            conversations:read: Read your conversations and their messages
            conversations:write: Start conversations and send messages on your behalf
            documents:read: Read your documents library
            documents:write: Upload and manage documents in your library
            memory:read: Read what the assistant remembers about you
            memory:write: Correct or delete what the assistant remembers
            schedules:read: View your scheduled tasks
            schedules:write: Create and manage scheduled tasks
            tables:read: Read your tables and their rows
            tables:write: Create tables and add, edit, or delete rows

````