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

# Register a webhook endpoint

> Register an endpoint to receive signed event deliveries.

The response contains the signing secret. It is shown once, here, and is
never retrievable again: store it before you close the response. Verify
each delivery by recomputing
`HMAC_SHA256(secret, "<t>.<raw body>")` and comparing it to the `v1`
value in the `X-Exo-Signature` header.

The url must be https and must resolve to a public address: private,
loopback, link-local, CGNAT, reserved and tunnelled ranges are rejected at
registration and again at every delivery, and redirects are never followed.
Unknown event types are rejected so a typo fails here rather than silently
never firing; a subscribed type that no emitter publishes yet comes back in
`inactiveEventTypes` for the same reason.

An organization may register at most 20 endpoints (409
`webhook_limit_reached` beyond that). A retried `Idempotency-Key`
returns the original registration but with the `secret` field replaced by
a marker: the plaintext is never persisted anywhere, including the replay
store.

WHAT AN ENDPOINT RECEIVES. Registering does not widen what the key behind it
can see. Events about the ORGANIZATION (`insight.proposed`,
`brain.trained`) are delivered to every endpoint. Events about a PERSON
(`job.completed`, `basin.shifted`, `contradiction.detected`,
`belief.superseded`) are delivered only to endpoints registered by that
person, mirroring the API, where another member's job returns 404 and
identity reads are self-or-admin. Events about a PROVISIONED SUBJECT are
delivered to every endpoint, because any key in the organization can already
read that subject's data with `X-Exo-Subject`.

## Authorization

This route requires the `write` scope.

A missing or invalid credential returns 401 [`authentication_error`](/errors/authentication_error). A valid credential without the scope returns 403 [`permission_denied`](/errors/permission_denied), and the problem body names the exact scope required.

## Headers

These are request conventions the contract does not declare as parameters, so they do not appear in the schema tables below.

| Header            | Applies        | What it does                                                                                                                                         |
| ----------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Exo-API-Key`   | Required       | Your Exo API key, `exo_<env>_<token>`. `Authorization: Bearer exo_...` is accepted as an alternate.                                                  |
| `X-Exo-Subject`   | Not applicable | This route is not subject-scoped.                                                                                                                    |
| `Idempotency-Key` | Honoured       | Makes a retry safe. The same key with the same body replays the original response instead of acting twice. See [Idempotency](/platform/idempotency). |
| `X-Request-Id`    | Response       | Returned on every response, including `204`s. Quote it in a support request.                                                                         |

## Success responses

| Status | Meaning             |
| ------ | ------------------- |
| `201`  | Successful Response |

## Errors

| Status | Code                                                   | When                                                           |
| ------ | ------------------------------------------------------ | -------------------------------------------------------------- |
| `400`  | [`invalid_request`](/errors/invalid_request)           | Malformed request, for example an invalid cursor.              |
| `401`  | [`authentication_error`](/errors/authentication_error) | Missing or invalid credentials.                                |
| `403`  | [`permission_denied`](/errors/permission_denied)       | Missing scope, or an unprovisioned subject selector.           |
| `404`  | [`not_found`](/errors/not_found)                       | The resource does not exist, or is not visible to this caller. |
| `409`  | [`conflict`](/errors/conflict)                         | A conflict with the current state of the resource.             |
| `422`  | [`validation_error`](/errors/validation_error)         | The request was understood but failed field validation.        |
| `429`  | [`rate_limit_exceeded`](/errors/rate_limit_exceeded)   | Rate limit exceeded. Retry after the Retry-After interval.     |
| `503`  | [`service_degraded`](/errors/service_degraded)         | A dependency is temporarily unavailable. Safe to retry.        |

Beyond the shared statuses, this route can answer with [`webhook_limit_reached`](/errors/webhook_limit_reached), [`idempotency_in_flight`](/errors/idempotency_in_flight), [`idempotency_key_reuse`](/errors/idempotency_key_reuse). Every problem body names its own `code`, and the `type` URI always resolves to the matching page.

Every error is an RFC 9457 `application/problem+json` body carrying a stable `code`, a `requestId`, and a `suggestedAction` where Exo has one. See [Errors](/platform/errors).

## Notes

* **The signing secret is in this response and nowhere else.** No route returns it again. Store it before you close the connection.
* **A replayed `Idempotency-Key` returns the registration with the secret redacted**, not in the clear. The replay still proves the endpoint was created once.
* **The URL must be https and must resolve to a public address.** Private, loopback, link-local, CGNAT, reserved and tunnelled ranges are rejected at registration and again at every delivery, and redirects are never followed.
* **Unknown event types are rejected here** so a typo fails at registration rather than silently never firing.


## OpenAPI

````yaml openapi.json POST /v1/webhooks
openapi: 3.1.0
info:
  contact:
    name: Exo
    url: https://get-exo.com/
  description: >-
    The Exo public developer API: ingest content, retrieve identity-conditioned
    context, and read the knowledge graph. Authenticate every request with an
    Exo API key, either as the X-Exo-API-Key header or as Authorization: Bearer
    exo_... . Errors follow RFC 9457 problem+json with a stable snake_case code
    on every body.
  title: Exo API
  version: 1.0.0
servers:
  - url: https://api.get-exo.com
security:
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: retrieve
    x-group: Retrieval
  - name: search
    x-group: Search
  - name: ingest
    x-group: Ingestion
  - name: jobs
    x-group: Jobs
  - name: subjects
    x-group: Subjects
  - name: recall
    x-group: Recall
  - name: graph
    x-group: Graph
  - name: insights
    x-group: Contradictions and proposals
  - name: condition
    x-group: Condition
  - name: brain
    x-group: Brain
  - name: sessions
    x-group: Coding sessions
  - name: connectors
    x-group: Connectors
  - name: events
    x-group: Events and webhooks
  - name: meta
    x-group: Identity
  - name: keys
    x-group: Keys and sandbox
  - name: usage
    x-group: Usage
  - name: settings
    x-group: Settings
  - name: exports
    x-group: Exports
  - name: team
    x-group: Team
  - name: admin
    x-group: Account and purge
paths:
  /v1/webhooks:
    post:
      tags:
        - events
      summary: Register a webhook endpoint
      description: >-
        Register an endpoint to receive signed event deliveries.


        The response contains the signing secret. It is shown ONCE, here, and is

        never retrievable again: store it before you close the response. Verify

        each delivery by recomputing

        ``HMAC_SHA256(secret, "<t>.<raw body>")`` and comparing it to the ``v1``

        value in the ``X-Exo-Signature`` header.


        The url must be https and must resolve to a public address: private,

        loopback, link-local, CGNAT, reserved and tunnelled ranges are rejected
        at

        registration and again at every delivery, and redirects are never
        followed.

        Unknown event types are rejected so a typo fails here rather than
        silently

        never firing; a subscribed type that no emitter publishes yet comes back
        in

        ``inactiveEventTypes`` for the same reason.


        An organization may register at most 20 endpoints (409

        ``webhook_limit_reached`` beyond that). A retried ``Idempotency-Key``

        returns the original registration but with the ``secret`` field replaced
        by

        a marker: the plaintext is never persisted anywhere, including the
        replay

        store.


        WHAT AN ENDPOINT RECEIVES. Registering does not widen what the key
        behind it

        can see. Events about the ORGANIZATION (``insight.proposed``,

        ``brain.trained``) are delivered to every endpoint. Events about a
        PERSON

        (``job.completed``, ``basin.shifted``, ``contradiction.detected``,

        ``belief.superseded``) are delivered only to endpoints registered by
        that

        person, mirroring the API, where another member's job returns 404 and

        identity reads are self-or-admin. Events about a PROVISIONED SUBJECT are

        delivered to every endpoint, because any key in the organization can
        already

        read that subject's data with ``X-Exo-Subject``.
      operationId: createWebhook
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreateRequest'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookWithSecret'
          description: Successful Response
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Malformed request, for example an invalid cursor.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing or invalid credentials
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing scope or unprovisioned subject
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The requested resource does not exist.
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: This organization is at its webhook endpoint limit
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The url or event types are not acceptable
        '429':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Rate limit exceeded
        '503':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: A dependency is unavailable
components:
  schemas:
    WebhookCreateRequest:
      description: Body for POST /v1/webhooks.
      properties:
        eventTypes:
          description: Event types to subscribe to, or ['*'] for every event.
          items:
            type: string
          maxItems: 20
          minItems: 1
          title: Eventtypes
          type: array
        url:
          description: HTTPS endpoint that will receive signed event deliveries.
          maxLength: 2048
          title: Url
          type: string
      required:
        - url
        - eventTypes
      title: WebhookCreateRequest
      type: object
    WebhookWithSecret:
      description: 'Registration response: the ONLY place the signing secret is returned.'
      properties:
        consecutiveFailures:
          title: Consecutivefailures
          type: integer
        createdAt:
          format: date-time
          title: Createdat
          type: string
        disabled:
          title: Disabled
          type: boolean
        disabledReason:
          anyOf:
            - type: string
            - type: 'null'
          title: Disabledreason
        eventTypes:
          items:
            type: string
          title: Eventtypes
          type: array
        id:
          title: Id
          type: string
        inactiveEventTypes:
          description: >-
            Subscribed event types that no emitter publishes yet, so nothing
            will be delivered for them. The subscription is kept and starts
            delivering as soon as the emitter ships; this field exists so you
            never wait on an event silently.
          items:
            type: string
          title: Inactiveeventtypes
          type: array
        lastDeliveryAt:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Lastdeliveryat
        lastSuccessAt:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Lastsuccessat
        secret:
          description: >-
            The signing secret. Shown once, at registration, and never
            retrievable again -- not by a read route and not by an idempotent
            replay, which returns a marker in its place. Store it now; if it is
            lost, register a new endpoint.
          title: Secret
          type: string
        updatedAt:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Updatedat
        url:
          title: Url
          type: string
      required:
        - id
        - url
        - eventTypes
        - disabled
        - consecutiveFailures
        - createdAt
        - secret
      title: WebhookWithSecret
      type: object
    Problem:
      description: |-
        RFC 9457 problem+json envelope (spec D9), documented in OpenAPI.

        Invariant: ``code`` equals the tail of the ``type`` URI
        (``https://docs.get-exo.com/errors/<code>``).
      properties:
        code:
          title: Code
          type: string
        detail:
          title: Detail
          type: string
        documentationUrl:
          anyOf:
            - type: string
            - type: 'null'
          title: Documentationurl
        errors:
          anyOf:
            - items:
                $ref: '#/components/schemas/FieldViolation'
              type: array
            - type: 'null'
          title: Errors
        requestId:
          default: ''
          title: Requestid
          type: string
        status:
          title: Status
          type: integer
        suggestedAction:
          anyOf:
            - type: string
            - type: 'null'
          title: Suggestedaction
        title:
          title: Title
          type: string
        type:
          title: Type
          type: string
      required:
        - type
        - title
        - status
        - detail
        - code
      title: Problem
      type: object
    FieldViolation:
      description: A single field-level violation inside a Problem ``errors[]`` list.
      properties:
        code:
          title: Code
          type: string
        field:
          title: Field
          type: string
        message:
          title: Message
          type: string
        pointer:
          title: Pointer
          type: string
      required:
        - pointer
        - field
        - code
        - message
      title: FieldViolation
      type: object
  securitySchemes:
    ApiKeyHeader:
      description: An Exo API key (exo_...) sent as the X-Exo-API-Key header.
      in: header
      name: X-Exo-API-Key
      type: apiKey
    BearerAuth:
      description: The same Exo API key (exo_...) sent as an Authorization bearer token.
      scheme: bearer
      type: http

````