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

# Ingest an event with an ingest key, routed by the project's mapping rules

> Generic ingest. PUT is accepted on this same path and behaves identically (see ingestEventPut); every other method returns 405 {"error":"Use POST"}, including OPTIONS — there are no CORS headers on this route.



## OpenAPI

````yaml /openapi.json post /v1/ingest/{ingest_key}
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/ingest/{ingest_key}:
    post:
      tags:
        - Ingest
      summary: >-
        Ingest an event with an ingest key, routed by the project's mapping
        rules
      description: >-
        Generic ingest. PUT is accepted on this same path and behaves
        identically (see ingestEventPut); every other method returns 405
        {"error":"Use POST"}, including OPTIONS — there are no CORS headers on
        this route.
      operationId: ingestEvent
      parameters:
        - name: ingest_key
          in: path
          required: true
          description: >-
            The plaintext ingest key. It is looked up by its first 16 characters
            (key_prefix) among non-revoked keys and then compared constant-time
            against the stored SHA-256 hash.
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Optional. If a submission with this key already exists for the
            tenant, the request short-circuits with 200 before the monthly quota
            is charged.
          schema:
            type: string
        - name: X-Hookie-Signature
          in: header
          required: false
          description: >-
            Required only when the ingest key has require_signature set; ignored
            otherwise. Format: 't=<unix-timestamp>,v1=<hex>' where v1 is
            HMAC-SHA-256 over '<t>.<raw body>' keyed with the key's signing
            secret.
          schema:
            type: string
            example: t=1700000000,v1=3ba8c0e9...
      requestBody:
        required: false
        description: >-
          Content-Type selects the parser: 'application/x-www-form-urlencoded'
          and 'multipart/form-data' are flattened into a shallow object
          (repeated fields become arrays; file parts become {filename, type,
          size} and their bytes are discarded). EVERY other Content-Type —
          including missing or wrong ones, which is deliberate so webhook
          providers that send JSON with an unhelpful Content-Type still work —
          is parsed as JSON. An empty body is accepted and becomes {}. Over
          1,000,000 bytes yields 413.
        content:
          application/json:
            schema:
              description: >-
                Any JSON value. On this route the payload is fed to the
                project's mapping rules, so its shape is whatever the rules'
                conditions and mappings expect.
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties:
                oneOf:
                  - type: string
                  - type: array
                    items:
                      type: string
          multipart/form-data:
            schema:
              type: object
              additionalProperties:
                oneOf:
                  - type: string
                  - type: array
                    items:
                      type: string
                  - type: object
                    required:
                      - filename
                      - type
                      - size
                    properties:
                      filename:
                        type: string
                      type:
                        type: string
                      size:
                        type: integer
      responses:
        '200':
          description: >-
            Idempotent replay — a submission with this Idempotency-Key already
            exists for the tenant. No quota is charged when the duplicate is
            detected up front; a request that loses the unique-index race has
            already been counted against the monthly quota.
          content:
            application/json:
              schema:
                type: object
                required:
                  - submission_id
                  - idempotent
                additionalProperties: false
                properties:
                  submission_id:
                    type: string
                  idempotent:
                    type: boolean
                    enum:
                      - true
        '201':
          description: >-
            Stored. With no dataset in the path the payload runs through the
            project's enabled mapping rules (webhook mirror rules excluded), and
            'routed' holds the NAME of every rule that matched and applied. No
            matching rule is still a 201 with routed: [] and records: 0, as is a
            swallowed routing failure — the raw submission is always kept.
          content:
            application/json:
              schema:
                type: object
                required:
                  - submission_id
                  - routed
                  - records
                additionalProperties: false
                properties:
                  submission_id:
                    type: string
                  routed:
                    type: array
                    items:
                      type: string
                    description: names of the mapping rules that applied
                  records:
                    type: integer
                    description: routed.length
        '400':
          description: >-
            error: 'Body must be valid JSON' or 'Body is not valid
            multipart/form-data'.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >-
            Only when the key has require_signature: 'Signature required'
            (header absent or no stored secret) or 'Invalid signature'.
          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); the block is audited.
          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'
        '413':
          description: >-
            error: 'Body exceeds 1000000 bytes' — from Content-Length, or from
            the buffered byte length.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            Edge burst limit: {error: 'Rate limit exceeded', retry_after: 10}.
            Monthly plan quota: {error: 'Monthly event quota exceeded',
            upgrade_url: '<APP_ORIGIN>/settings/billing', retry_after: 3600}.
            retry_after is a body field only — no Retry-After response header is
            set. Both limiters fail open when their backing service is
            unavailable.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      retry_after:
                        type: integer
                        description: 10 (rate limit) or 3600 (quota)
                      upgrade_url:
                        type: string
                        format: uri
                        description: quota 429 only
        '500':
          description: >-
            error: 'Key misconfigured' (signing secret could not be decrypted)
            or 'Could not store submission'.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      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.

````