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

# Rotate an ingest key with an overlap

> Top-level alias: POST /admin/api/ingest-keys/{id}/rotate. Write role required. Issues a NEW key with the old one's name, dataset_default, require_signature and ip_allowlist, and gives the OLD key an expiry overlap_hours from now (an earlier expiry it already had is kept). Both authenticate until then, at ingest and on /v1/stream. A key that requires signatures gets a new signing secret. The new key and secret are returned once, exactly as on create. A key can be rotated once; rotate its replacement after that. Audited as rotate_ingest_key on the old key, naming the new one.



## OpenAPI

````yaml /openapi.json post /admin/api/projects/{pid}/ingest-keys/{id}/rotate
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.


    **A refused agent is told how to proceed.** An agent's grant is one rung of
    a ladder — `hookie:read` < `hookie:write` < `hookie:manage` — and a call
    above it answers 403 with `code: "agent_scope_insufficient"`, the
    `required_scope`, the `granted_scopes` and a `manage_url` to the console
    page where its user widens it. When the user's own role is the limit the
    code is `insufficient_role` and there is no link, because no grant can
    exceed the user; billing, platform admin, managing connected agents and
    destination signing secrets answer `agent_not_permitted` at every scope. A
    human in the console still reads `Insufficient role`.


    **A suspended workspace** (an operator action, for abuse or non-payment)
    answers every surface with 403 and `reason: "workspace_suspended"`: ingest
    (endpoint URLs and ingest keys), `/v1/data` and `/v1/stream` refuse before
    anything is stored or counted, and every admin API write — console, OAuth
    agent or hosted MCP alike — is refused except billing, platform admin,
    connected-agent management and the POST-bodied searches. The customer portal
    API is read-only in the same way: destination writes answer 403
    `workspace_suspended`. Reads keep working. Nothing is sent while it is
    suspended: pending deliveries are paused, workflow runs already under way
    are paused, open `/v1/stream` connections are closed, and cron triggers,
    database sources and WebSocket listeners stop. Reinstating sends the paused
    deliveries, resumes the paused workflow runs where they stopped, and resumes
    the rest; no stored data is lost.


    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.
  - name: Data API
    description: >-
      Read-only access to the datasets and columns a project exposes, with
      per-project keys.
  - name: Broadcast Logs
    description: >-
      Forward a project's logs and traces over OTLP/HTTP (JSON) to Datadog,
      Grafana Cloud, an OpenTelemetry Collector, PostHog or Sentry.
paths:
  /admin/api/projects/{pid}/ingest-keys/{id}/rotate:
    post:
      tags:
        - Ingest keys
      summary: Rotate an ingest key with an overlap
      description: >-
        Top-level alias: POST /admin/api/ingest-keys/{id}/rotate. Write role
        required. Issues a NEW key with the old one's name, dataset_default,
        require_signature and ip_allowlist, and gives the OLD key an expiry
        overlap_hours from now (an earlier expiry it already had is kept). Both
        authenticate until then, at ingest and on /v1/stream. A key that
        requires signatures gets a new signing secret. The new key and secret
        are returned once, exactly as on create. A key can be rotated once;
        rotate its replacement after that. Audited as rotate_ingest_key on the
        old key, naming the new one.
      operationId: rotateIngestKey
      parameters:
        - name: pid
          in: path
          required: true
          description: Project id (UUID) belonging to the caller workspace.
          schema:
            type: string
        - name: id
          in: path
          required: true
          description: Ingest key id (the key being replaced).
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                overlap_hours:
                  type: number
                  minimum: 0
                  maximum: 720
                  default: 24
                  description: >-
                    How long the OLD credential keeps working, in hours (at most
                    30 days). 0 ends it at once.
                expires_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: 'Optional expiry for the NEW key. Default: none.'
      responses:
        '201':
          description: The new key. key and signing_secret are shown exactly once.
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - key
                  - key_prefix
                  - signing_secret
                  - expires_at
                  - previous_key
                properties:
                  id:
                    type: string
                  key:
                    type: string
                    description: ik_live_ followed by 48 hex characters.
                  key_prefix:
                    type: string
                  signing_secret:
                    type:
                      - string
                      - 'null'
                    description: >-
                      A new whsec_ secret when the key requires signatures;
                      otherwise null.
                  expires_at:
                    type:
                      - string
                      - 'null'
                    format: date-time
                  previous_key:
                    type: object
                    required:
                      - id
                      - expires_at
                    properties:
                      id:
                        type: string
                      expires_at:
                        type: string
                        format: date-time
                        description: When the old key stops working.
        '400':
          description: >-
            overlap_hours is not a number from 0 to 720, or expires_at is not a
            future timestamp.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >-
            Not signed in, or the agent bearer token is invalid, unknown or
            revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            Insufficient role (write capability required: owner, admin or
            developer), or a cookie-authenticated mutation missing the
            X-Requested-With: fetch header. Also `reason: "workspace_suspended"`
            while the workspace is suspended. For an OAuth-connected agent the
            body also carries `code`, `required_scope` and `granted_scopes`,
            plus `manage_url` when widening its grant in Settings → Connected
            agents would let the call through (see Error).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Key not found in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The key is revoked, expired, or already rotated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Internal error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - session: []
        - bearerAuth: []
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.
        reason:
          type: string
          description: >-
            Machine-readable reason, where the status alone is ambiguous.
            `workspace_suspended` (403): the workspace is suspended;
            `delivery_allowance_exhausted` (429): a replay was refused because
            the monthly delivery allowance is used. `endpoint_disabled` (503):
            the endpoint is switched off; `handshake_failed`: a provider
            URL-verification challenge could not be answered (#193).
        code:
          type: string
          enum:
            - agent_scope_insufficient
            - insufficient_role
            - agent_not_permitted
          description: >-
            Present on a 403 to an OAuth-connected agent.
            `agent_scope_insufficient`: the agent's grant is below what the call
            needs, and widening it would let the call through (see
            `required_scope` and `manage_url`). `insufficient_role`: the user's
            OWN workspace role does not allow the call, so no grant can — a
            workspace owner or admin has to change the role.
            `agent_not_permitted`: no connected agent may do this at any scope
            (billing, platform admin, managing connected agents, a destination's
            signing secret).
        required_scope:
          type: string
          enum:
            - hookie:read
            - hookie:write
            - hookie:manage
          description: 'With `code`: the scope the refused call needs.'
        granted_scopes:
          type: array
          items:
            type: string
          description: 'With `code`: the scopes the agent''s grant holds now.'
        manage_url:
          type: string
          format: uri
          description: >-
            With `code: "agent_scope_insufficient"` only: the console's Settings
            → Connected agents page, opened on this agent
            (`…/#/settings/agents?client=<client_id>`), where its user widens
            the grant. The change applies on the agent's next request.
  securitySchemes:
    session:
      type: apiKey
      in: cookie
      name: hookie_session
      description: >-
        The console's sealed session cookie, set by WorkOS AuthKit. Mutating
        requests also require the `X-Requested-With` CSRF header.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        An OAuth 2.1 access token from a connected agent, audienced at the
        `/mcp` resource URI. The agent acts as its user, with that user's role
        narrowed to the granted `hookie:*` scopes. No CSRF header is required —
        a bearer token is not ambient credentials.

````