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

# Tail the tenant's live event stream over SSE or WebSocket

> Read-only live tail of every event fanned out for the key's tenant. It never exposes stored records or the admin API, and it is not project-scoped — it carries the whole tenant's stream. GET only: any other method returns 405 {"error":"Use GET"}.



## OpenAPI

````yaml /openapi.json get /v1/stream
openapi: 3.1.0
info:
  title: Hookie API
  version: 1.0.0
  summary: Capture, route and deliver webhooks.
  description: >-
    Hookie exposes three HTTP surfaces.


    **Ingest + streaming** is the public plane. A path-form endpoint URL
    authenticates by its own high-entropy slug — there is no key, token or
    cookie — and an `ik_live_…` ingest key authenticates the `/v1/*` routes.
    Whenever the URL names a dataset the payload is stored whole into it;
    project mapping rules are evaluated on exactly one shape, `POST
    /v1/ingest/{ingest_key}` with no dataset segment.


    **The customer portal API** is a separate, token-authed surface for your end
    customers, scoped to one portal and one customer.


    **The admin API** drives the console. It accepts either a browser session or
    an OAuth bearer token from a connected coding agent — the same routes, the
    same validation, the same audit trail. An agent's authority is its user's
    workspace role narrowed to the scopes granted in the console.


    This document is generated from the request handlers and adversarially
    verified against them.
  contact:
    name: Hookie
    url: https://hookie.ai
  license:
    name: Proprietary
    url: https://hookie.ai/legal/terms
servers:
  - url: https://app.hookie.ai
    description: Production
  - url: https://app.preview.hookie.ai
    description: Preview
security: []
tags:
  - name: Ingest
    description: Send events to Hookie.
  - name: Streaming
    description: Tail events in real time over SSE or WebSocket.
  - name: Portal
    description: Token-authed surface for your end customers.
  - name: Projects
    description: Projects and their settings.
  - name: Routing
    description: Rules, endpoints, datasets and records.
  - name: Delivery
    description: Destinations, deliveries and replay.
  - name: Workflows
    description: Multi-step workflows, triggers and AI agents.
  - name: Observability
    description: Search, correlation, stats and the audit log.
  - name: Workspace
    description: Members, connected agents, SSO and billing.
paths:
  /v1/stream:
    get:
      tags:
        - Streaming
      summary: Tail the tenant's live event stream over SSE or WebSocket
      description: >-
        Read-only live tail of every event fanned out for the key's tenant. It
        never exposes stored records or the admin API, and it is not
        project-scoped — it carries the whole tenant's stream. GET only: any
        other method returns 405 {"error":"Use GET"}.
      operationId: streamEvents
      parameters:
        - name: key
          in: query
          required: false
          description: >-
            Ingest key as a query parameter, for browser EventSource clients
            that cannot set headers. Used only when no 'Authorization: Bearer
            <ingest key>' header is present; if neither is supplied the request
            is 401. Same key resolution as ingest (prefix lookup + constant-time
            SHA-256 comparison; revoked keys never match).
          schema:
            type: string
        - name: after_seq
          in: query
          required: false
          description: >-
            Resume cursor: replay buffered events with seq greater than this
            value. Ignored when a Last-Event-ID header is present. Non-numeric
            or negative values are treated as absent. The resume buffer holds at
            most 1000 events and drops anything older than 5 minutes; a request
            for evicted events still succeeds but is preceded by a stream-gap
            control event.
          schema:
            type: integer
            minimum: 0
        - name: Last-Event-ID
          in: header
          required: false
          description: >-
            Standard SSE resume header, sent automatically by EventSource on
            reconnect. Takes precedence over after_seq when present. Same
            semantics and same 1000-event / 5-minute buffer window.
          schema:
            type: integer
            minimum: 0
        - name: Upgrade
          in: header
          required: false
          description: >-
            Send exactly 'websocket' (compared case-sensitively) to get a 101
            WebSocket upgrade instead of an SSE stream. Any other value, or its
            absence, yields the SSE response.
          schema:
            type: string
            enum:
              - websocket
      responses:
        '101':
          description: >-
            WebSocket upgrade accepted (the request carried 'Upgrade:
            websocket'). Each live event arrives as one JSON text frame: {seq,
            id, dataset, received_at, data}. When a resume cursor was supplied
            and part of the requested window had been evicted, a
            {"type":"stream-gap","missed_from":<int>,"resume_from":<int>} frame
            is sent first, followed by the replayable buffered events. The
            socket is hibernatable and answers a 'ping' text frame with 'pong'.
            The live path is lossy by contract — durable delivery is the
            outbound-delivery queue, not this stream.
        '200':
          description: >-
            Server-sent event stream (no Upgrade header). Frames: ': connected'
            on open, ': keep-alive' every 25 seconds, one 'id: <seq>\ndata:
            <json>\n\n' frame per event where the JSON is {seq, id, dataset,
            received_at, data}, and — on a resume that missed evicted events —
            an un-ided 'event: stream-gap\ndata:
            {"missed_from":<int>,"resume_from":<int>}' control frame. A
            subscriber that stops draining is dropped once its backlog runs past
            the stream's high-water mark.
          headers:
            X-Accel-Buffering:
              description: >-
                Always 'no' — disables proxy/CDN buffering so events flush
                immediately.
              schema:
                type: string
                enum:
                  - 'no'
            Access-Control-Allow-Origin:
              description: >-
                Always '*'. Safe because the stream is key-authenticated and
                never cookie-authenticated; it also blocks any credentialed
                cross-origin EventSource.
              schema:
                type: string
                enum:
                  - '*'
            Cache-Control:
              description: Always 'no-store'.
              schema:
                type: string
          content:
            text/event-stream:
              schema:
                type: string
                description: >-
                  SSE frame stream; Content-Type is 'text/event-stream;
                  charset=utf-8'.
        '401':
          description: >-
            error: 'Ingest key required' — neither an 'Authorization: Bearer
            <key>' header nor a ?key= query parameter was supplied (or both were
            empty).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            error: 'Source IP not allowed' — the Cloudflare client IP must
            satisfy both the key's allowlist and the tenant's (an empty
            allowlist matches everything). Unlike ingest, this block is not
            written to the audit log.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            error: 'Unknown ingest key' — no non-revoked key with this prefix,
            hash mismatch, or the key's tenant row is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            The tenant's plan cap on concurrent stream connections is reached
            (open SSE responses and WebSockets are counted together). This
            response comes from the stream Durable Object rather than the shared
            apiError helper, so it carries a Retry-After header and no
            CSP/security headers.
          headers:
            Retry-After:
              description: Always '30'.
              schema:
                type: string
                enum:
                  - '30'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    required:
                      - limit
                    properties:
                      limit:
                        type: integer
                        description: the plan's concurrent-connection cap
                description: error is always 'stream connection limit reached'.
      security:
        - ingestKey: []
components:
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable reason.
        retry_after:
          type: integer
          description: Seconds to wait. Present on 429.
        upgrade_url:
          type: string
          format: uri
          description: Present only on a monthly-quota 429.
  securitySchemes:
    ingestKey:
      type: http
      scheme: bearer
      description: >-
        An `ik_live_…` ingest key. Stored hashed; the plaintext is returned once
        at creation.

````