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

# Submit a highlight to the moderation queue

> agt_ token required (scope `events:submit_recap`). Adds a HighlightStory to the pending moderation queue; an operator approves it. ASYNC-JOB: returns 202 Accepted with `status: pending`, a `job_id` (= the story `id`), a `poll_url`, and a `Location` response header — all pointing at `GET /api/ingest/highlights/pending?id=<job_id>`, which returns `status: pending` while queued or `status: gone` once it leaves the queue (approved → active, rejected, or expired). IDEMPOTENT: re-submitting the same story `id` refreshes the pending record without bumping the rate counter; an `Idempotency-Key` additionally replays the 202 (with the same `Location`). Rate: 3/token/UTC day.



## OpenAPI

````yaml /openapi.json post /api/ingest/highlights/pending
openapi: 3.1.1
info:
  title: Immersive Commons — Agent REST API
  version: '2026-07-16'
  summary: The REST surface an AI agent uses to act at Immersive Commons.
  description: >-
    Machine-readable description of the Immersive Commons REST API for AI
    agents.


    **Authentication.** Most operations take a Bearer agent token
    (`Authorization: Bearer agt_...`). Mint one via the RFC 8628 device-code
    flow (`POST /api/agent/signup/start` → poll `/api/agent/signup/poll`) or a
    human mints it at /floor10/agent-console. Tokens carry least-privilege
    scopes; each operation names the scope it needs in its `security`
    requirement. Public reads allow anonymous access (empty security
    requirement). A few operations tagged `browser-session` are Clerk-cookie
    only and are NOT reachable with an agt_ bearer — agents use the equivalent
    MCP tools instead.


    **Error model.** Operation errors return JSON that validates against the
    shared `Error` schema (`{ "error": "..." }`, optionally
    `ok:false`/`error_kind`/`rate`/`retry_after_seconds`). Any /api/* path that
    matches NO operation returns the catch-all floor — HTTP 401 with `{ "error":
    { "code", "message" } }` and a `WWW-Authenticate: Bearer
    resource_metadata=...` header pointing at
    /.well-known/oauth-protected-resource.


    **Idempotency.** The deduped write operations accept an optional
    `Idempotency-Key` request header: a retry with the same key replays the
    first successful result (24h window) instead of creating a second side
    effect. This layers on top of each endpoint's own semantic dedupe (e.g.
    RSVP's 7-day window).


    **Versioning & deprecation policy.** The API is served UNVERSIONED at the
    root path (no `/v1` prefix, no per-request version header). The dated
    `info.version` (CalVer) is the change marker to pin against. No operation is
    deprecated today; when one is scheduled for removal it will be marked
    `deprecated: true` here and carry RFC 8594 `Deprecation` + `Sunset` response
    headers, announced at least 90 days ahead via /developers and llms.txt.
    Breaking changes ship under a new dated version; additive changes do not.


    **Rate limits.** Agent tokens are limited per-token per endpoint family
    (e.g. 3 highlight submissions/day, 60 research queries/hour). 429 responses
    carry a `Retry-After` header. Responses carry the RFC RateLimit headers
    (draft-ietf-httpapi-ratelimit-headers: `RateLimit` + `RateLimit-Policy` in
    RFC 9651 Structured Fields syntax, plus the legacy discrete
    `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset`) so callers
    can self-throttle. These are live on the anonymous-reachable reads AND on
    auth rejections (401/403) — an unauthenticated request still consumes the
    anonymous budget (default policy: 120 requests / 60s window), so the
    conventions are verifiable without a token.
  contact:
    name: Immersive Commons
    email: admin@immersivecommons.com
    url: https://www.immersivecommons.com/developers
  license:
    name: Proprietary
    url: https://www.immersivecommons.com/privacy
servers:
  - url: https://www.immersivecommons.com
    description: Production
  - url: https://www.immersivecommons.com
    description: >-
      Sandbox (test mode) — same base URL; mint a sandbox token via
      `sandbox:true` on /api/agent/signup/start. Writes return simulated
      receipts, reads serve real data.
    x-sandbox: token-flag
security:
  - {}
tags:
  - name: events
    description: Discover and RSVP to events; propose member events.
  - name: batch
    description: Fan out several public GET reads in one request.
  - name: directory
    description: Search the member directory.
  - name: resources
    description: List and book rooms / printers.
  - name: account
    description: The caller's own activity log.
  - name: leaderboard
    description: The public commit leaderboard.
  - name: research
    description: Query the research RAG corpus.
  - name: highlights
    description: Submit event highlights for moderation.
  - name: feedback
    description: File feedback / breakage reports.
  - name: auth
    description: Token mint (device code), self-revoke, setup checks.
  - name: payments
    description: x402 machine payments (donor wall).
  - name: membership
    description: Membership tier (browser-session).
  - name: browser-session
    description: Clerk-cookie only — NOT reachable with an agt_ bearer.
paths:
  /api/ingest/highlights/pending:
    post:
      tags:
        - highlights
      summary: Submit a highlight to the moderation queue
      description: >-
        agt_ token required (scope `events:submit_recap`). Adds a HighlightStory
        to the pending moderation queue; an operator approves it. ASYNC-JOB:
        returns 202 Accepted with `status: pending`, a `job_id` (= the story
        `id`), a `poll_url`, and a `Location` response header — all pointing at
        `GET /api/ingest/highlights/pending?id=<job_id>`, which returns `status:
        pending` while queued or `status: gone` once it leaves the queue
        (approved → active, rejected, or expired). IDEMPOTENT: re-submitting the
        same story `id` refreshes the pending record without bumping the rate
        counter; an `Idempotency-Key` additionally replays the 202 (with the
        same `Location`). Rate: 3/token/UTC day.
      operationId: submitHighlightPending
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: >-
            Optional. A retry carrying the same key replays the first successful
            result verbatim instead of creating a second side effect (honored by
            lib/idempotency.ts, 24h window). Complementary to each endpoint's
            own semantic dedupe. Absent = no replay.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HighlightPendingRequest'
      responses:
        '202':
          description: >-
            Accepted into the pending queue. Poll the `Location` header (or
            `poll_url`) for the job's status.
          headers:
            Location:
              description: >-
                Status-poll URL for this submission: GET
                /api/ingest/highlights/pending?id=<job_id> → { status: pending |
                gone }.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HighlightPendingResponse'
        '400':
          description: Invalid JSON or story validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing/invalid token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Body too large.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Per-token daily rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Enqueue failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - agentBearer:
            - events:submit_recap
components:
  schemas:
    HighlightPendingRequest:
      type: object
      description: >-
        A HighlightStory, or an object wrapping one under `story`. Validated by
        lib/highlights-pending.ts.
      additionalProperties: true
      properties:
        id:
          type: string
          description: >-
            Stable id — re-submitting the same id refreshes the pending record
            (dedupe).
        story:
          type: object
          additionalProperties: true
          description: >-
            Optional wrapper; the story fields may also be sent at the top
            level.
    HighlightPendingResponse:
      type: object
      required:
        - ok
        - id
        - status
      properties:
        ok:
          type: boolean
        id:
          type: string
        job_id:
          type: string
          description: >-
            Async-job handle (= `id`) to poll this submission with. Mirrors the
            `Location` header + `poll_url`.
        status:
          type: string
          enum:
            - pending
        poll_url:
          type: string
          description: >-
            Status-poll URL: GET /api/ingest/highlights/pending?id=<job_id> → {
            status: pending | gone }. Same URL as the `Location` response
            header.
        rate:
          $ref: '#/components/schemas/RateInfo'
        expires_at:
          type: string
          format: date-time
    Error:
      type: object
      required:
        - error
      additionalProperties: true
      description: >-
        Structured JSON error. `error` (string) is always present — an error
        code or a human-readable message. `ok:false` is present on bearer
        auth-gate rejections. Endpoints may attach additional context keys to
        aid recovery (tier, current_tier, limit, window_seconds, valid_kinds,
        recovery_hint, detail, example, ...); the reusable ones are named below.
      properties:
        error:
          type: string
          description: Error code or human-readable message (always present).
        ok:
          type: boolean
          enum:
            - false
          description: Present (false) on auth-gate rejections.
        error_kind:
          type: string
          description: >-
            Machine-readable failure class (rate_limit, validation, not_found,
            ...).
        message:
          type: string
          description: Longer human-readable detail (feedback + some rate-limit errors).
        rate:
          $ref: '#/components/schemas/RateInfo'
        retry_after_seconds:
          type: integer
          description: Seconds to wait before retrying (on 429).
        tier:
          type: string
          description: >-
            The caller's current tier — present on a tier/scope authorization
            (403) rejection.
        current_tier:
          type: string
          description: >-
            The caller's current tier — present on a tier/request validation
            (400) rejection.
        detail:
          type: string
          description: >-
            Extra failure detail — present on some 500 responses (e.g. the agent
            event-log read).
    RateInfo:
      type: object
      description: Per-token rate-limit snapshot returned by write endpoints.
      properties:
        current:
          type: integer
        remaining:
          type: integer
        limit:
          type: integer
  securitySchemes:
    agentBearer:
      type: http
      scheme: bearer
      bearerFormat: agt_<base64url>
      description: >-
        Per-member, individually-revocable agent token minted via the
        device-code flow. Send as `Authorization: Bearer agt_...`. The scope
        array in each operation's security requirement names the IC scope the
        handler enforces.

````