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

# Reconcile Hub Content

> Materialize ``content_nodes`` for a hub's provenance-scoped playlists (D4).

A bodyless POST (or a body with ``attributes`` omitted / ``playlist_ids``
omitted) derives the playlist set from this hub's OWN
``HubTemplateApplication`` history — the common case, and the intended
default usage for both an operator running this after a data issue and
``mio-cli`` re-running it after its own step sequence. Passing an
explicit ``playlist_ids`` list is for the one case provenance cannot
cover (a hub with no application record at all — see the plan's §6 open
question); every id in that list must belong to THIS hub.

Idempotent by construction: ``ContentService.ensure_nodes`` hash-first
resolves every spec (D6), so calling this twice in a row with the SAME
resolved playlist set adopts everything the first call created rather
than creating it again — zero duplicate rows, zero duplicate INSERT
attempts on the second call.

Returns 200 with one result per resolved ``NodeSpec``, in the full D7
outcome vocabulary (created / adopted / adopted_not_member_visible /
skipped_*) — nothing is log-only, so a caller can tell exactly what
happened without diffing the hub.

Error codes
-----------
404 ``hub_not_found``          — ``hub_id`` does not exist, or belongs to
                                  a different team than ``team_id``
                                  (enumeration-safe: both cases 404
                                  identically).
422 ``no_playlist_provenance`` — the hub has no ``HubTemplateApplication``
                                  row carrying any playlist ids, AND no
                                  ``playlist_ids`` were supplied in the
                                  request body.
422 ``playlist_not_in_hub``    — one or more ids in an explicit
                                  ``playlist_ids`` list do not resolve to
                                  a Playlist scoped to THIS hub
                                  (``meta.invalid_playlist_ids`` lists
                                  which).
429                            — rate limit (60/hr/IP) exceeded.

RAISE, NEVER RETURN: every rejection above is an ``AppException``
subclass (``NotFoundError`` / ``ValidationError``,
``app/infrastructure/exceptions.py``) raised out of
``HubContentReconcileService.reconcile`` and left to propagate uncaught
— never caught here and turned into a hand-rolled ``JSONResponse``. They
are mapped by the centrally-registered ``app_exception_handler``
(``app/main.py``), which runs AFTER ``get_db()``'s except-branch has
already rolled back anything ``ensure_nodes`` flushed before the
rejection. A handler that instead caught one of these and RETURNED an
error response would return NORMALLY from this route, sending
``get_db()`` down its commit branch instead — durably persisting
whatever partial work happened before the error. See
``app/hub_scaffold/reconcile_service.py``'s module docstring for the
same contract stated from the service side.



## OpenAPI

````yaml /openapi/mio-openapi.json post /api/v1/teams/{team_id}/hubs/{hub_id}/content/reconcile
openapi: 3.1.0
info:
  description: >-
    Production API reference for Membership.io. Use the authored guides for
    workflows and the generated endpoint pages for exact parameters and schemas.
  title: Membership.io API
  version: 0.1.0
servers:
  - description: Production
    url: https://api.member.dev
security:
  - bearerAuth: []
paths:
  /api/v1/teams/{team_id}/hubs/{hub_id}/content/reconcile:
    post:
      tags:
        - hubs-admin
      summary: Reconcile Hub Content
      description: >-
        Materialize ``content_nodes`` for a hub's provenance-scoped playlists
        (D4).


        A bodyless POST (or a body with ``attributes`` omitted /
        ``playlist_ids``

        omitted) derives the playlist set from this hub's OWN

        ``HubTemplateApplication`` history — the common case, and the intended

        default usage for both an operator running this after a data issue and

        ``mio-cli`` re-running it after its own step sequence. Passing an

        explicit ``playlist_ids`` list is for the one case provenance cannot

        cover (a hub with no application record at all — see the plan's §6 open

        question); every id in that list must belong to THIS hub.


        Idempotent by construction: ``ContentService.ensure_nodes`` hash-first

        resolves every spec (D6), so calling this twice in a row with the SAME

        resolved playlist set adopts everything the first call created rather

        than creating it again — zero duplicate rows, zero duplicate INSERT

        attempts on the second call.


        Returns 200 with one result per resolved ``NodeSpec``, in the full D7

        outcome vocabulary (created / adopted / adopted_not_member_visible /

        skipped_*) — nothing is log-only, so a caller can tell exactly what

        happened without diffing the hub.


        Error codes

        -----------

        404 ``hub_not_found``          — ``hub_id`` does not exist, or belongs
        to
                                          a different team than ``team_id``
                                          (enumeration-safe: both cases 404
                                          identically).
        422 ``no_playlist_provenance`` — the hub has no
        ``HubTemplateApplication``
                                          row carrying any playlist ids, AND no
                                          ``playlist_ids`` were supplied in the
                                          request body.
        422 ``playlist_not_in_hub``    — one or more ids in an explicit
                                          ``playlist_ids`` list do not resolve to
                                          a Playlist scoped to THIS hub
                                          (``meta.invalid_playlist_ids`` lists
                                          which).
        429                            — rate limit (60/hr/IP) exceeded.


        RAISE, NEVER RETURN: every rejection above is an ``AppException``

        subclass (``NotFoundError`` / ``ValidationError``,

        ``app/infrastructure/exceptions.py``) raised out of

        ``HubContentReconcileService.reconcile`` and left to propagate uncaught

        — never caught here and turned into a hand-rolled ``JSONResponse``. They

        are mapped by the centrally-registered ``app_exception_handler``

        (``app/main.py``), which runs AFTER ``get_db()``'s except-branch has

        already rolled back anything ``ensure_nodes`` flushed before the

        rejection. A handler that instead caught one of these and RETURNED an

        error response would return NORMALLY from this route, sending

        ``get_db()`` down its commit branch instead — durably persisting

        whatever partial work happened before the error. See

        ``app/hub_scaffold/reconcile_service.py``'s module docstring for the

        same contract stated from the service side.
      operationId: >-
        hubs_admin.post_hubs_admin_post_teams_by_team_id_hubs_by_hub_id_content_reconcile
      parameters:
        - in: path
          name: team_id
          required: true
          schema:
            title: Team Id
            type: string
        - in: path
          name: hub_id
          required: true
          schema:
            title: Hub Id
            type: string
      requestBody:
        content:
          application/vnd.api+json:
            schema:
              anyOf:
                - $ref: '#/components/schemas/HubContentReconcileEnvelope'
                - type: 'null'
              title: Envelope
      responses:
        '200':
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/HubContentReconcileResponse'
          description: Successful Response
        '422':
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      security:
        - HTTPBearer: []
components:
  schemas:
    HubContentReconcileEnvelope:
      additionalProperties: false
      properties:
        data:
          $ref: '#/components/schemas/HubContentReconcileResource'
      required:
        - data
      title: HubContentReconcileEnvelope
      type: object
    HubContentReconcileResponse:
      properties:
        data:
          $ref: '#/components/schemas/HubContentReconcileResultResource'
      required:
        - data
      title: HubContentReconcileResponse
      type: object
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          title: Detail
          type: array
      title: HTTPValidationError
      type: object
    HubContentReconcileResource:
      additionalProperties: false
      properties:
        attributes:
          $ref: '#/components/schemas/HubContentReconcileRequestAttributes'
        type:
          const: content_node_reconciliations
          title: Type
          type: string
      required:
        - type
      title: HubContentReconcileResource
      type: object
    HubContentReconcileResultResource:
      properties:
        attributes:
          $ref: '#/components/schemas/HubContentReconcileResultAttributes'
        id:
          title: Id
          type: string
        type:
          const: content_node_reconciliations
          default: content_node_reconciliations
          title: Type
          type: string
      required:
        - id
        - attributes
      title: HubContentReconcileResultResource
      type: object
    ValidationError:
      properties:
        ctx:
          title: Context
          type: object
        input:
          title: Input
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          title: Location
          type: array
        msg:
          title: Message
          type: string
        type:
          title: Error Type
          type: string
      required:
        - loc
        - msg
        - type
      title: ValidationError
      type: object
    HubContentReconcileRequestAttributes:
      additionalProperties: false
      description: >-
        The one caller-supplied override: an explicit playlist set (D4).


        ``playlist_ids`` OMITTED (or the whole request body omitted — see the

        router, which accepts a bodyless POST) means "derive it from this hub's

        ``HubTemplateApplication`` provenance" — the common case, and the whole

        reason this endpoint exists as a bodyless heal action. Supplying it

        explicitly is for the ONE case provenance cannot cover: a hub with no

        ``HubTemplateApplication`` row at all (a pages-only scaffold, or an

        external ``mio-cli`` run that skipped this backend op — see the plan's

        §6 open question). An explicitly EMPTY list (``[]``) is rejected by

        ``min_length=1`` rather than silently treated as "no override" — a
        caller

        that bothered to send the key meant something by it.
      properties:
        playlist_ids:
          anyOf:
            - items:
                maxLength: 36
                minLength: 1
                type: string
              maxItems: 200
              minItems: 1
              type: array
            - type: 'null'
          title: Playlist Ids
      title: HubContentReconcileRequestAttributes
      type: object
    HubContentReconcileResultAttributes:
      properties:
        hub_id:
          title: Hub Id
          type: string
        results:
          items:
            $ref: '#/components/schemas/ContentNodeReconciliationEntry'
          title: Results
          type: array
      required:
        - hub_id
        - results
      title: HubContentReconcileResultAttributes
      type: object
    ContentNodeReconciliationEntry:
      description: >-
        One ``EnsureNodeResult`` (D7), verbatim on the wire — nothing is

        log-only. ``outcome`` is the full D7 vocabulary

        (``app/content/node_spec.py::EnsureOutcome``): ``created`` / ``adopted``
        /

        ``adopted_not_member_visible`` / ``skipped_duplicate_media`` /

        ``skipped_ambiguous_media`` / ``skipped_tombstoned`` /

        ``skipped_parent_tombstoned`` / ``skipped_parent_unresolved`` /

        ``skipped_slug_conflict``.
      properties:
        legacy_hash:
          title: Legacy Hash
          type: string
        member_visible:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Member Visible
        node_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Node Id
        node_type:
          title: Node Type
          type: string
        outcome:
          title: Outcome
          type: string
        reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Reason
      required:
        - legacy_hash
        - node_type
        - outcome
      title: ContentNodeReconciliationEntry
      type: object
  securitySchemes:
    bearerAuth:
      bearerFormat: JWT or mio_sk API key
      description: >-
        Send platform JWTs, contact JWTs, or team API keys as `Authorization:
        Bearer <token>`.
      scheme: bearer
      type: http
    HTTPBearer:
      scheme: bearer
      type: http

````