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

# Edit a destination: name, URL, on/off, dataset filter, request options, alert URL or delivery limits

> Top-level alias: PATCH /admin/api/destinations/{id}. Write role required.



## OpenAPI

````yaml /openapi.json patch /admin/api/projects/{pid}/destinations/{id}
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. An
    endpoint URL files each payload into the endpoint's own dataset through the
    endpoint's own criteria and mappings (none means the whole payload, as one
    record); `/v1/ingest/{ingest_key}/{dataset}` stores the payload whole into
    the named dataset. Project mapping rules are evaluated on exactly one shape,
    `POST /v1/ingest/{ingest_key}` with no dataset segment, and never on an
    endpoint URL.


    **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. It also
    accepts an **admin API key** (`hk_…`, Settings → API keys) for CI,
    infrastructure-as-code and vendor backends: the key acts as the member who
    created it, capped at the key's role (viewer, developer or admin — never
    billing or platform administration), optionally limited to one project, with
    an optional expiry and IP allowlist. Keys are stored as a SHA-256 hash and
    shown once; if their creator leaves the workspace they stop working. The
    same key works on the hosted MCP server at `/mcp`.


    **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, revoking an API key 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.


    **Rate limits.** `/admin/api/*` and `/mcp` allow 100 requests every 10
    seconds per credential — per API key, per connected agent, or per signed-in
    person. Over that, the answer is 429 with `Retry-After` (seconds).


    **Idempotency.** A `POST` to `/admin/api/*` may carry an `Idempotency-Key`
    header (1–255 printable characters, e.g. a UUID). The first request runs and
    its response is stored for 24 hours; a retry with the same key from the same
    credential, with the same path and body, gets that response back with
    `Idempotent-Replayed: true` and runs nothing. The same key with a different
    path, body or credential is refused with 422; a retry while the first
    request is still running gets 409. 5xx responses are not stored. `POST
    /admin/api/api-keys` refuses the header, because its response is a secret
    shown once and is never stored.


    **Versioning.** Every `/admin/api/*` and `/mcp` response carries
    `Hookie-Version` (currently `2026-09-29`). Additive changes — a new route, a
    new response field, a new optional parameter — ship under the current
    version, so clients must ignore fields they do not know. A breaking change
    gets a new dated version, announced in the changelog, and the previous
    version stays available for at least 12 months; a client pins one by sending
    `Hookie-Version` on its requests. A version this deployment does not serve
    is refused with 400 and the list of `supported_versions`.


    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}/destinations/{id}:
    patch:
      tags:
        - Destinations
      summary: >-
        Edit a destination: name, URL, on/off, dataset filter, request options,
        alert URL or delivery limits
      description: >-
        Top-level alias: PATCH /admin/api/destinations/{id}. Write role
        required.
      operationId: updateDestination
      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: Destination id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              description: >-
                At least one of name, url, enabled, dataset_filter,
                timeout_seconds, max_per_second, max_concurrency, headers, auth,
                transform or alert_url. null on a limit restores its default.
                name and url are validated as on create (url must be https). The
                signing secret does not change with the URL - the signature
                covers the timestamp and body, not the address; rotate it on its
                own route. enabled: true also clears a failing destination's
                streak and any automatic disable (OBS-2), and sends the
                deliveries it held (the response's `resumed`).
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                url:
                  type: string
                  format: uri
                  description: >-
                    https only. Refused (409) for a destination that belongs to
                    a customer portal - its URL is the customer's.
                enabled:
                  type: boolean
                dataset_filter:
                  type:
                    - array
                    - 'null'
                  items:
                    type: string
                    pattern: ^[A-Za-z][A-Za-z0-9_]{0,62}$
                  maxItems: 50
                  description: >-
                    SNAKE_CASE on patch (create uses datasetFilter). null, or a
                    list that dedupes to empty, means every dataset.
                timeout_seconds:
                  type:
                    - integer
                    - 'null'
                  minimum: 1
                  maximum: 30
                  description: >-
                    Seconds to wait for the receiver's answer before the attempt
                    counts as failed (1-30). null: the default, 10.
                max_per_second:
                  type:
                    - number
                    - 'null'
                  minimum: 0.1
                  maximum: 1000
                  description: >-
                    Most deliveries a second to this destination (0.1-1000).
                    null: no limit. A delivery over it waits on the queue for
                    its reserved slot; it is not failed and is not counted as an
                    attempt.
                max_concurrency:
                  type:
                    - integer
                    - 'null'
                  minimum: 1
                  maximum: 100
                  description: >-
                    Most requests in flight to this destination at once (1-100).
                    null: no cap. A delivery over it waits on the queue; it is
                    not failed and is not counted as an attempt.
                headers:
                  type:
                    - array
                    - 'null'
                  maxItems: 20
                  items:
                    type: object
                    required:
                      - name
                    properties:
                      name:
                        type: string
                        maxLength: 100
                        description: >-
                          An HTTP header name (RFC 9110 token). Reserved and
                          refused: Hookie-*, X-Hookie-*, webhook-*, Host,
                          Content-Length, Content-Type, Idempotency-Key,
                          User-Agent, and hop-by-hop headers (Connection,
                          Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade,
                          Proxy-*). Names are unique, case-insensitively.
                      value:
                        type: string
                        maxLength: 4096
                        description: >-
                          No CR, LF or other control characters. On PATCH, a
                          secret header sent WITHOUT a value keeps the value
                          already stored for that name.
                      secret:
                        type: boolean
                        default: false
                        description: >-
                          Store the value AES-256-GCM encrypted and never return
                          it.
                    additionalProperties: false
                  description: >-
                    Custom headers sent with every delivery (DLV-7). Replaces
                    the whole list; null clears it. Sent before Hookie's own
                    headers, which always win.
                auth:
                  description: >-
                    Authentication (DLV-7), stored AES-256-GCM encrypted and
                    never returned. null or {type:'none'} removes it. On PATCH,
                    the same type with no credential keeps the stored credential
                    (so an API key's header can be renamed without re-entering
                    it).
                  oneOf:
                    - type: 'null'
                    - type: object
                      required:
                        - type
                      properties:
                        type:
                          const: none
                      additionalProperties: false
                    - type: object
                      required:
                        - type
                      properties:
                        type:
                          const: bearer
                        token:
                          type: string
                          maxLength: 4096
                      additionalProperties: false
                      description: 'Sent as Authorization: Bearer <token>.'
                    - type: object
                      required:
                        - type
                      properties:
                        type:
                          const: basic
                        username:
                          type: string
                          description: May not contain ':'.
                        password:
                          type: string
                      additionalProperties: false
                      description: >-
                        Sent as Authorization: Basic base64(username:password),
                        UTF-8.
                    - type: object
                      required:
                        - type
                        - header
                      properties:
                        type:
                          const: api_key
                        header:
                          type: string
                          description: >-
                            The header the key goes in, e.g. DD-API-KEY or
                            X-Api-Key. Not a reserved header.
                        value:
                          type: string
                          maxLength: 4096
                      additionalProperties: false
                transform:
                  description: >-
                    Payload transformation (DLV-7): data, never code. null sends
                    Hookie's envelope {id, dataset, received_at, data}. A
                    template is any JSON value whose string leaves may hold
                    {{path}} placeholders, read from the envelope with the
                    mapping engine's dotted-path reader ({{dataset}},
                    {{data.customer.email}}, {{$payload}} for the whole
                    envelope); a string that is exactly one placeholder keeps
                    the value's type, an embedded one becomes text (objects as
                    JSON, missing as empty). A mapping builds a flat object, one
                    key per {path, key}, as routing rules do ($submission.id and
                    $submission.received_at are the record's id and time). At
                    most 16 KB serialised. The body is always sent as
                    application/json and Hookie-Signature is computed over it as
                    sent.
                  oneOf:
                    - type: 'null'
                    - type: object
                      required:
                        - type
                        - template
                      properties:
                        type:
                          const: template
                        template:
                          description: Any JSON value, nested at most 20 levels.
                      additionalProperties: false
                    - type: object
                      required:
                        - type
                        - mappings
                      properties:
                        type:
                          const: mapping
                        mappings:
                          type: array
                          minItems: 1
                          maxItems: 100
                          items:
                            type: object
                            required:
                              - path
                              - key
                            properties:
                              path:
                                type: string
                                maxLength: 200
                              key:
                                type: string
                                pattern: ^[A-Za-z][A-Za-z0-9_]{0,62}$
                            additionalProperties: false
                      additionalProperties: false
                alert_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: >-
                    OBS-2: a public https URL (no credentials, no private or
                    internal host) that receives one signed POST when Hookie
                    switches this destination off for failing. Body:
                    {type:'destination.disabled', occurred_at,
                    destination:{id,name,url,project_id}, reason,
                    consecutive_dead, failing_since, last_failure}; headers
                    Hookie-Signature (the destination's signing secret, same
                    scheme as a delivery) and Hookie-Alert:
                    destination.disabled. Sent once, never retried, redirects
                    not followed. null or empty removes it.
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                properties:
                  ok:
                    type: boolean
                    const: true
                  resumed:
                    type: integer
                    description: >-
                      Present when enabling sent deliveries the destination held
                      while disabled.
                  warning:
                    type: string
                    description: >-
                      When a new url points back at this workspace's own ingest
                      - legal, but usually a loop.
        '400':
          description: >-
            A name or url that create would refuse (blank name, non-https url),
            enabled must be a boolean, dataset_filter must be an array of
            dataset names or null, an invalid dataset name, more than 50
            datasets, a delivery limit out of its bounds (timeout_seconds 1-30
            whole, max_per_second 0.1-1000, max_concurrency 1-100 whole), a
            reserved, malformed or duplicated header, a secret header with no
            value and none stored, an invalid auth or transform, a custom header
            that is also the authentication's header, an alert_url that is not a
            public https URL, or Nothing to update when no field was sent.
          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'
        '402':
          description: >-
            `Plan limit reached` - enabled: true while the workspace already
            runs as many destinations as its plan allows (only possible after a
            downgrade paused some, #212). Upgrade, or turn another off first.
          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: Destination not found in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            The destination belongs to a customer portal - its dataset filter is
            fixed by the portal event types, and its URL, headers,
            authentication and payload are the customer's. Only raised when
            dataset_filter, url, headers, auth or transform is in the body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Internal error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - session: []
        - bearerAuth: []
        - apiKey: []
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).
            `missing_header`, `bad_signature`, `stale_timestamp` (401): an
            endpoint's provider signature check failed; `scheme` names the
            check.
        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.
        scheme:
          type: string
          description: 'Present on a signature 401: the endpoint''s verification scheme.'
  responses:
    RateLimited:
      description: >-
        Over this credential's limit (100 requests per 10 seconds). Retry after
        `Retry-After` seconds.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  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.
    apiKey:
      type: http
      scheme: bearer
      description: >-
        An `hk_…` admin API key from Settings → API keys. Acts as the member who
        created it, capped at the key's role (viewer, developer or admin) and
        optionally one project. Stored as a SHA-256 hash, shown once. No CSRF
        header is required.

````