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

# Create or update a notification channel

> Upserts one channel and **replaces** its subscription set.

Without `id`, the channel is keyed on its type's natural identity, so an identical call is idempotent rather than duplicating: `email` by address, `webhook` by URL, `slack` by channel id, personal types by (organization, type, caller). With `id` it is an authoritative edit of that row — the only way to update a `teams` channel, whose natural identity is a secret URL.

`subscriptions` is replaced wholesale, not merged: send the complete desired set, and expect an omitted list to unsubscribe the channel from everything.

Creating a `webhook` channel reveals its signing secret in `signing_secret` ONCE; it is not readable afterwards. The secret is only generated when the row is new, so a retried create that lands after the first succeeded returns `signing_secret: null`. If you need a secret and got `null`, call `POST /channels/{channel_id}/regenerate-secret`.



## OpenAPI

````yaml /api-reference/openapi.yaml put /api/notification/public/v1/channels
openapi: 3.1.0
info:
  title: Barndoor.ai Public API
  description: Unified public API specification for Barndoor.ai platform
  version: 2.18.0
servers:
  - url: https://{host}
    description: Barndoor public API
    variables:
      host:
        default: platform.barndoor.ai
        description: >-
          The host serving your Barndoor deployment. Defaults to production;
          override it for another environment or a dedicated deployment.
security:
  - ApiKey: []
  - OAuth2:
      - openid
      - profile
      - email
tags:
  - name: dlp
    x-group: Data Control Center
  - name: identity
    x-group: Identity
  - name: llm-gateway
    x-group: LLM Gateway
  - name: notification
    x-group: Notifications
  - name: policy
    x-group: Policies
  - name: registry
    x-group: MCP Gateway
  - name: system-management
    x-group: Log Exports
paths:
  /api/notification/public/v1/channels:
    put:
      tags:
        - notification
      summary: Create or update a notification channel
      description: >-
        Upserts one channel and **replaces** its subscription set.


        Without `id`, the channel is keyed on its type's natural identity, so an
        identical call is idempotent rather than duplicating: `email` by
        address, `webhook` by URL, `slack` by channel id, personal types by
        (organization, type, caller). With `id` it is an authoritative edit of
        that row — the only way to update a `teams` channel, whose natural
        identity is a secret URL.


        `subscriptions` is replaced wholesale, not merged: send the complete
        desired set, and expect an omitted list to unsubscribe the channel from
        everything.


        Creating a `webhook` channel reveals its signing secret in
        `signing_secret` ONCE; it is not readable afterwards. The secret is only
        generated when the row is new, so a retried create that lands after the
        first succeeded returns `signing_secret: null`. If you need a secret and
        got `null`, call `POST /channels/{channel_id}/regenerate-secret`.
      operationId: upsert_alert_channel
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpsertRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelResponse'
        '401':
          description: Missing, malformed, or expired bearer token.
        '403':
          description: Authenticated, but not permitted to manage this channel.
        '404':
          description: '`id` was supplied but names no channel in the caller''s organization.'
        '422':
          description: >-
            The body violates the rules for its `type` — a missing or forbidden
            destination field, a duplicated `alert_type`, or a webhook/Teams URL
            rejected as non-`https` or resolving to a private address. The
            `detail` names the reason.
components:
  schemas:
    ChannelUpsertRequest:
      properties:
        id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Id
          description: >-
            Existing channel to edit authoritatively. Omit to create-or-dedup on
            the type's natural identity (see the model description). When
            supplied and unknown — or owned by another organization — the
            request is a `404`, never a create.
        type:
          $ref: '#/components/schemas/ChannelType'
          description: >-
            The kind of destination, and the discriminator for every other
            field. Immutable in practice: an edit-by-id that changes `type` is
            rejected, and an upsert without `id` and a different `type`
            addresses a different natural identity, so it creates a separate
            channel.
        enabled:
          type: boolean
          title: Enabled
          description: >-
            Whether the channel delivers. `false` suspends delivery while
            keeping the channel and its subscriptions intact — the reversible
            alternative to deleting it.
          default: true
        email_address:
          anyOf:
            - type: string
            - type: 'null'
          title: Email Address
          description: >-
            Destination address for `type: email`. Any deliverable address — a
            team alias or a ticketing inbox, not necessarily a platform user.
            Validated and normalized; also the channel's natural identity, so
            reusing an address updates that channel. Forbidden on every other
            type.
          examples:
            - security-ops@example.com
        url:
          anyOf:
            - type: string
            - type: 'null'
          title: Url
          description: >-
            Destination endpoint for `type: webhook`. Must be `https` and must
            resolve to a public address — private, loopback, link-local, and
            cloud-metadata targets are rejected with a `422` naming the reason.
            Also the channel's natural identity. Forbidden on every other type.
          examples:
            - https://hooks.example.com/barndoor
        label:
          anyOf:
            - type: string
            - type: 'null'
          title: Label
          description: >-
            Human-readable name, shown wherever the channel is listed. Required
            for `slack` and `teams` (for `teams` it is the only non-secret
            identifier you get back). Forbidden on the other types.
          examples:
            - '#security-alerts'
        slack_channel_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Slack Channel Id
          description: >-
            Slack channel id (not its name) for `type: slack`; the
            organization's Slack app must already be installed. Also the
            channel's natural identity. Forbidden on every other type.
          examples:
            - C0123456789
        teams_workflow_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Teams Workflow Url
          description: >-
            Microsoft Teams Workflows incoming-webhook URL for `type: teams`.
            Write-only: it is stored as a secret and never returned by any
            endpoint — reads expose only `has_workflow_url`. Required on create,
            optional on edit-by-id (omit to keep the stored URL). Must be
            `https`, resolve to a public address, and sit on the Power Automate
            host allowlist. Forbidden on every other type.
        subscriptions:
          items:
            $ref: '#/components/schemas/ChannelSubscriptionInput'
          type: array
          title: Subscriptions
          description: >-
            The complete set of alert types to deliver to this channel.
            **Replaces** the channel's existing set — an omitted or empty list
            unsubscribes it from everything. At most one entry per alert type;
            duplicates are a `422`.
      type: object
      required:
        - type
      title: ChannelUpsertRequest
      description: >-
        Create or update one notification channel and replace its subscription
        set.


        This is a full upsert, not a patch: ``subscriptions`` is **replaced** by
        what you send, so

        omitting it removes every subscription the channel had. Send the
        complete desired set on

        every call — which is what makes the endpoint safe to drive from a
        declarative tool.


        Which fields are permitted depends on ``type``; sending one that does
        not belong to the

        type is a ``422``, not a silently ignored field. See ``ChannelType`` for
        the full matrix.


        | ``type`` | required | forbidden |

        |---|---|---|

        | ``in_app`` / ``user_email`` | — | every destination field |

        | ``email`` | ``email_address`` | ``url``, ``label``,
        ``slack_channel_id``, ``teams_workflow_url`` |

        | ``webhook`` | ``url`` | ``email_address``, ``label``,
        ``slack_channel_id``, ``teams_workflow_url`` |

        | ``slack`` | ``slack_channel_id``, ``label`` | ``email_address``,
        ``url``, ``teams_workflow_url`` |

        | ``teams`` | ``label`` (+ ``teams_workflow_url`` on create) |
        ``email_address``, ``url``, ``slack_channel_id`` |


        **Identity.** Without ``id`` the channel is keyed on its type's natural
        identity, so

        repeating the same call is idempotent rather than creating duplicates:
        personal types by

        (organization, type, caller), ``email`` by (organization,
        ``email_address``), ``webhook`` by

        (organization, ``url``), ``slack`` by (organization,
        ``slack_channel_id``). With ``id`` it is

        an authoritative edit of that row — the only way to change a ``teams``
        channel, whose

        natural identity is a secret URL you never hold back.
      examples:
        - enabled: true
          subscriptions:
            - alert_type: break_glass_used
            - alert_type: policy_changed
          type: webhook
          url: https://hooks.example.com/barndoor
        - email_address: security-ops@example.com
          subscriptions:
            - alert_type: connection_broken
          type: email
        - enabled: false
          id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
          subscriptions:
            - alert_type: break_glass_used
          type: webhook
          url: https://hooks.example.com/barndoor
    ChannelResponse:
      properties:
        id:
          type: string
          format: uuid
          title: Id
          description: >-
            Server-assigned channel id. Stable across updates; pass it as `id`
            to edit by id.
        type:
          $ref: '#/components/schemas/ChannelType'
          description: The kind of destination. See `ChannelType`.
        enabled:
          type: boolean
          title: Enabled
          description: Whether the channel currently delivers.
        user_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: User Id
          description: >-
            Owning user for a personal channel (`in_app`, `user_email`). `null`
            for the org-wide types, which belong to the organization rather than
            one user.
        email_address:
          anyOf:
            - type: string
            - type: 'null'
          title: Email Address
          description: >-
            Destination address for `type: email`, normalized. `null` for every
            other type.
        url:
          anyOf:
            - type: string
            - type: 'null'
          title: Url
          description: >-
            Destination endpoint for `type: webhook`. `null` for every other
            type.
        label:
          anyOf:
            - type: string
            - type: 'null'
          title: Label
          description: >-
            Human-readable name, set for `slack` and `teams`. `null` for the
            other types.
        slack_channel_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Slack Channel Id
          description: 'Slack channel id for `type: slack`. `null` for every other type.'
        subscriptions:
          items:
            $ref: '#/components/schemas/ChannelSubscription'
          type: array
          title: Subscriptions
          description: >-
            The alert types this channel currently delivers. Empty means it
            delivers nothing.
        created_at:
          type: string
          format: date-time
          title: Created At
          description: When the channel was created (UTC).
        updated_at:
          type: string
          format: date-time
          title: Updated At
          description: When the channel or its subscriptions last changed (UTC).
        has_signing_secret:
          type: boolean
          title: Has Signing Secret
          description: >-
            Whether a `webhook` channel has a stored signing secret — always
            true for a webhook channel, false otherwise. The secret itself is
            never readable back; use `POST
            /channels/{channel_id}/regenerate-secret` to obtain a fresh one.
          default: false
        has_workflow_url:
          type: boolean
          title: Has Workflow Url
          description: >-
            Whether a `teams` channel has a stored Workflows URL. The URL is a
            secret and is never returned; this flag is the only readable signal
            that one is configured.
          default: false
        signing_secret:
          anyOf:
            - type: string
            - type: 'null'
          title: Signing Secret
          description: >-
            One-time reveal of a newly generated webhook signing secret. Present
            **only** on the response to the request that created it — the `PUT`
            that first creates a `webhook` channel — and `null` on every list or
            subsequent read. Store it when you receive it; the only way to
            recover from losing it is to rotate.
      type: object
      required:
        - id
        - type
        - enabled
        - user_id
        - email_address
        - url
        - label
        - slack_channel_id
        - subscriptions
        - created_at
        - updated_at
      title: ChannelResponse
      description: >-
        A channel and its subscriptions. Secrets (webhook signing secret, slack
        bot token) live

        in OpenBao and are never surfaced here except as a one-time reveal via
        ``signing_secret``,

        populated only on the response to the request that just generated it
        (webhook create or

        regenerate); slack channels expose only the non-secret ``label`` +
        ``slack_channel_id``.
    ChannelType:
      type: string
      enum:
        - in_app
        - user_email
        - email
        - webhook
        - slack
        - teams
      title: ChannelType
      description: >-
        What kind of destination a channel delivers to. The type is the
        discriminator for the

        whole channel model: it decides which destination field is required,
        whether the channel is

        personal (owned by one user) or org-wide (managed by an admin), and
        which natural-identity

        key an upsert without ``id`` dedupes on.


        Personal — owner is always the authenticated caller, never taken from
        the request body:


        - ``in_app``: the caller's notification inbox in the platform UI. No
        destination field.

        - ``user_email``: the caller's own verified account email. No
        destination field.


        Org-wide — admin/superadmin managed, one set per organization:


        - ``email``: an arbitrary address (a team alias, a ticketing inbox).
        Requires
          ``email_address``; deduped on it.
        - ``webhook``: an HTTPS endpoint you own, signed with a Standard
        Webhooks
          ``whsec_`` secret. Requires ``url``; deduped on it.
        - ``slack``: a channel in the organization's connected Slack workspace.
        Requires
          ``slack_channel_id`` + ``label``; needs the Slack app installed first (an interactive
          consent flow that is not part of this API).
        - ``teams``: a Microsoft Teams channel behind a Workflows
        incoming-webhook URL. Requires
          ``label``, plus ``teams_workflow_url`` on create.
    ChannelSubscriptionInput:
      properties:
        alert_type:
          $ref: '#/components/schemas/AlertType'
          description: >-
            The alert type to deliver to this channel. Must be one of the values
            returned by `GET /public/v1/channels/options`; subscribing to a type
            your organization is not admitted to is accepted but inert.
          examples:
            - mcp_server_added
      type: object
      required:
        - alert_type
      title: ChannelSubscriptionInput
      description: >-
        One alert type the channel matches on.


        Named ``Channel``-first deliberately: these schemas are merged by name
        into the unified

        public OpenAPI document (scripts/generate-public-openapi.py), where a
        bare

        ``SubscriptionResponse`` collided with identity-service's *payment*
        subscription schema and

        silently lost the tie. Keep the prefix. A channel receives an alert only
        if its

        subscription set contains that alert's type — an empty set means the
        channel exists but

        delivers nothing.
    ChannelSubscription:
      properties:
        alert_type:
          $ref: '#/components/schemas/AlertType'
          description: The subscribed alert type.
      type: object
      required:
        - alert_type
      title: ChannelSubscription
      description: One alert type this channel is subscribed to.
    AlertType:
      type: string
      enum:
        - llm_threshold_exceeded
        - connection_broken
        - mcp_server_added
        - mcp_server_deleted
        - policy_changed
        - policy_impact
        - admin_role_change
        - break_glass_used
        - llm_budget_exhausted
        - llm_provider_unavailable
        - llm_provider_key_rejected
        - api_key_revoked
        - llm_api_key_expiring
        - llm_api_key_expired
        - mcp_server_tooling_changed
      title: AlertType
      description: >-
        A discrete, notification-worthy event. This is what publishers emit and
        what channels

        subscribe to — a channel is delivered an alert when its subscription set
        contains that

        alert's type. Severity and category are intrinsic to the type (see
        ``ALERT_TAXONOMY``), so

        they are never set per subscription.


        Treat this enum as open: values are added over time, and the set your
        organization may

        actually subscribe to is gated by feature flags. Read

        ``GET /public/v1/channels/options`` for the live vocabulary rather than
        hardcoding these

        names — subscribing to a type your organization is not admitted to is
        silently inert.
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: >-
        A Barndoor API key, sent as `Authorization: Bearer <key>`. Keys are
        prefixed `bdai_` and are opaque — do not attempt to decode one.
        Interchangeable with an OAuth 2.0 access token.
    OAuth2:
      type: oauth2
      x-issuer: https://auth.barndoor.ai/realms/barndoor
      description: >-
        An OAuth 2.0 access token from Barndoor's identity provider, sent as
        `Authorization: Bearer <token>`. Public clients must use PKCE.
        Interchangeable with an API key.
      flows:
        authorizationCode:
          authorizationUrl: >-
            https://auth.barndoor.ai/realms/barndoor/protocol/openid-connect/auth
          tokenUrl: >-
            https://auth.barndoor.ai/realms/barndoor/protocol/openid-connect/token
          scopes:
            openid: Issue an OIDC ID token identifying the caller.
            profile: Read the caller's basic profile claims.
            email: Read the caller's email address.
            offline_access: Issue a refresh token. Optional — no operation requires it.
        clientCredentials:
          tokenUrl: >-
            https://auth.barndoor.ai/realms/barndoor/protocol/openid-connect/token
          scopes:
            openid: Issue an OIDC ID token identifying the caller.
            profile: Read the caller's basic profile claims.
            email: Read the caller's email address.
            offline_access: Issue a refresh token. Optional — no operation requires it.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.