openapi: 3.1.0
info:
  title: P4A REST API
  version: '1.0.0'
  description: |
    The P4A REST API lets an external client or script act on your behalf using a
    [personal access token](/docs/api/personal-access-tokens). Every endpoint
    documented here is reachable with a token; admin-only endpoints are not part
    of this surface.

    ## Authentication

    Send your token as a Bearer credential on every request:

    ```
    Authorization: Bearer p4a_xxxxxxxxxxxxxxxxxxxxxxxx
    ```

    A token acts as you: requests are attributed to your account and limited to
    what you can already access. The same data scoping that applies in the
    dashboard applies to the API.

    ## Scopes

    Each endpoint requires one scope, chosen by HTTP method:

    - **`api:read`** — `GET` requests (discover policies, read your own resources).
    - **`api:write`** — `POST`, `PATCH`, `PUT`, `DELETE` requests (submit policies,
      trigger deployments, manage workspaces and connections).

    A request whose token is missing the required scope is rejected with `403`.
    A missing, invalid, expired, or revoked token is rejected with `401`.
servers:
  - url: '{origin}'
    description: >-
      P4A API origin. Defaults to production; override the `origin` variable in
      the playground's server dialog to target a non-production environment
      (e.g. a preview deploy or `http://localhost:3000`).
    variables:
      origin:
        default: https://www.p4a.ai
        description: Scheme + host of the P4A instance to call (no trailing slash).
security:
  - bearerAuth: []
tags:
  - name: Policies
    description: Discover catalog policies and manage policies you own.
  - name: Submissions
    description: Submit policies for review and track their status.
  - name: Deployments
    description: Deploy policies to your Anypoint organizations and manage releases.
  - name: Connections
    description: Manage Anypoint connections and inspect their reach.
  - name: Workspaces
    description: Create workspaces, manage members, and share connections.
  - name: Transfers
    description: Transfer policy ownership between users.
  - name: Tokens
    description: List and revoke your own personal access tokens.
  - name: Account
    description: Read and update your own profile.

paths:
  # ---------------------------------------------------------------- Policies
  /api/policies:
    get:
      tags: [Policies]
      summary: List catalog policies
      description: |
        Returns the policy catalog. Anonymous and read-token callers see approved
        policies; an authenticated caller additionally sees their own
        under-review or private policies.

        With `source=mulesoft`, returns the read-only catalog of MuleSoft
        out-of-the-box (Omni/Flex Gateway) policies synced from
        docs.mulesoft.com instead. Those entries are summary rows
        (`MuleSoftPolicySummary`, keyed by `slug`, no `body_md`); fetch one
        policy's full documentation body via
        `GET /api/mulesoft-policies/{slug}`.
      operationId: listPolicies
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: category
          in: query
          required: false
          schema: { type: string }
          description: Filter by policy category.
        - name: q
          in: query
          required: false
          schema: { type: string }
          description: Free-text search over name and description (name and summary for `source=mulesoft`).
        - name: source
          in: query
          required: false
          schema:
            type: string
            enum: [community, mulesoft]
            default: community
          description: |
            Which catalog to list. `community` (default) = user-submitted P4A
            policies. `mulesoft` = MuleSoft out-of-the-box Omni/Flex Gateway
            policies (read-only, `MuleSoftPolicySummary` items).
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum: [inbound, outbound]
          description: |
            `source=mulesoft` only. Filter by enforcement leg — `inbound`
            (incoming request, most policies) or `outbound` (upstream/response
            leg, e.g. `tls-outbound`). Ignored for the community catalog.
      responses:
        '200':
          description: The matching policies.
          content:
            application/json:
              schema:
                type: object
                properties:
                  policies:
                    type: array
                    description: |
                      `Policy` items for the community catalog;
                      `MuleSoftPolicySummary` items when `source=mulesoft`.
                    items:
                      oneOf:
                        - $ref: '#/components/schemas/Policy'
                        - $ref: '#/components/schemas/MuleSoftPolicySummary'
  /api/mulesoft-policies/{slug}:
    parameters:
      - name: slug
        in: path
        required: true
        schema: { type: string }
        description: The MuleSoft policy slug (from a `source=mulesoft` list entry).
    get:
      tags: [Policies]
      summary: Get a MuleSoft out-of-the-box policy
      description: |
        Returns one MuleSoft out-of-the-box (Omni/Flex Gateway) policy including
        its full documentation body (`body_md`, synced from docs.mulesoft.com).
        Public: the MuleSoft catalog is guest-readable. Slugs that are no longer
        present in the docs index return 404.
      operationId: getMuleSoftPolicy
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: The MuleSoft policy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  policy: { $ref: '#/components/schemas/MuleSoftPolicy' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/policies/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: Get a policy
      operationId: getPolicy
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: The policy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  policy: { $ref: '#/components/schemas/Policy' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Policies]
      summary: Update a policy you own
      description: Requires `api:write`. Only the policy owner may edit these fields.
      operationId: updatePolicy
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PolicyPatch' }
      responses:
        '200':
          description: The updated policy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  policy: { $ref: '#/components/schemas/Policy' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/policies/{id}/branches:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: List the policy's repository branches
      operationId: listPolicyBranches
      responses:
        '200':
          description: Branch information for the policy's source repository.
          content:
            application/json:
              schema: { type: object }
  /api/policies/{id}/download:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Policies]
      summary: Record a policy download
      description: Requires `api:write`. Records that you fetched the install command.
      operationId: recordPolicyDownload
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /api/policies/{id}/history:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: List a policy's lifecycle history
      operationId: listPolicyHistory
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer }
        - name: before
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          description: History entries, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  entries:
                    type: array
                    items: { type: object }
                  nextCursor:
                    type: [string, 'null']
  /api/policies/{id}/issues-enabled:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: Live re-check of whether the policy repo accepts issues
      description: >-
        Live-verifies (against GitHub, cached ~1h) whether the policy's upstream
        repository currently accepts issues, for the "Report an issue"
        affordance (#66). Corrects the persisted `github_has_issues` baseline
        when an author disabled the tracker after publish. Visible to any caller
        who can read the policy (404 otherwise, same as `GET /api/policies/{id}`).
      operationId: getPolicyIssuesEnabled
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: >-
            The live issue-tracker state. `enabled` is `null` when GitHub
            couldn't be read (private/gone repo, rate limit, network) — the
            caller should keep trusting the persisted `github_has_issues`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                    nullable: true
        '404': { $ref: '#/components/responses/NotFound' }
  /api/policies/{id}/like:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: Whether you have liked this policy
      operationId: getPolicyLike
      responses:
        '200':
          description: Your like state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  liked: { type: boolean }
    post:
      tags: [Policies]
      summary: Toggle your like on a policy
      description: Requires `api:write`.
      operationId: togglePolicyLike
      responses:
        '200':
          description: Your like state after toggling.
          content:
            application/json:
              schema:
                type: object
                properties:
                  liked: { type: boolean }
  /api/policies/{id}/comments:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: List the discussion comments on a policy
      description: >-
        Public but RLS-scoped: anonymous callers see comments on published
        policies; a signed-in caller additionally sees comments on their own
        unpublished policies. Returns a flat, oldest-first list — replies carry
        a non-null `parentId` (single-level threading) and the client rebuilds
        the tree. Soft-deleted comments are returned with `deleted: true` and
        empty `body`/`authorName`/`authorId` so reply chains stay intact.
      operationId: listPolicyComments
      responses:
        '200':
          description: The policy's comments, oldest-first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  comments:
                    type: array
                    items: { $ref: '#/components/schemas/Comment' }
    post:
      tags: [Policies]
      summary: Post a comment or single-level reply on a policy
      description: >-
        Requires `api:write`. Pass `parentId` to reply to a top-level comment;
        replies are limited to a single level (you cannot reply to a reply).
        Per-day quota-checked (HTTP 429 when exceeded).
      operationId: createPolicyComment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CommentInput' }
      responses:
        '201':
          description: The created comment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  comment: { $ref: '#/components/schemas/Comment' }
        '404':
          description: Policy or parent comment not found (or not published).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Invalid body, or reply nesting/parent-policy invariant violated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429':
          description: Daily comment quota exceeded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /api/comments/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    patch:
      tags: [Policies]
      summary: Edit a comment
      description: >-
        Requires `api:write`. Author-only (a platform admin may also edit).
        A no-op when the body is unchanged.
      operationId: updateComment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string, maxLength: 10000 }
      responses:
        '200':
          description: The updated comment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  comment: { $ref: '#/components/schemas/Comment' }
        '403':
          description: You are neither the author nor an admin.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: Comment not found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: The comment has been deleted and cannot be edited.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    delete:
      tags: [Policies]
      summary: Delete a comment
      description: >-
        Requires `api:write`. The author soft-deletes (the row is kept so
        replies don't orphan; the body is blanked and rendered as a
        placeholder). A platform admin may hard-delete with `?hard=true`.
      operationId: deleteComment
      parameters:
        - name: hard
          in: query
          required: false
          description: Admin-only. `true` permanently removes the row and its replies.
          schema: { type: boolean }
      responses:
        '200':
          description: The comment was deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        '403':
          description: You are neither the author nor an admin (or hard-delete requested by a non-admin).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: Comment not found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /api/policies/{id}/orgs:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: List Anypoint orgs eligible for this policy
      operationId: listPolicyOrgs
      parameters:
        - name: connectionId
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: The eligible organizations for the given connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  orgs:
                    type: array
                    items: { $ref: '#/components/schemas/AnypointOrg' }
  /api/policies/{id}/reviewers/me:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: Whether you are a reviewer of this policy
      operationId: getPolicyReviewerSelf
      responses:
        '200':
          description: Your reviewer state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  isReviewer: { type: boolean }
  /api/policies/{id}/transfers:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Transfers]
      summary: Initiate a policy ownership transfer
      description: Requires `api:write`. Offers ownership of the policy to another user by email.
      operationId: initiatePolicyTransfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recipientEmail]
              properties:
                recipientEmail: { type: string, format: email }
      responses:
        '201':
          description: The created transfer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transfer:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      to_user_id: { type: string, format: uuid }
  /api/policies/{id}/revisions:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Policies]
      summary: List your proposed edits for this policy
      description: >-
        Lists the edit revisions you have proposed for this policy's About text
        or source repository. Scoped to your own revisions.
      operationId: listPolicyRevisions
      responses:
        '200':
          description: Your revisions for this policy, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  revisions:
                    type: array
                    items: { $ref: '#/components/schemas/PolicyRevision' }
    post:
      tags: [Policies]
      summary: Propose an edit to a published policy
      description: >-
        Requires `api:write`. As the policy's owner, propose a new value for one
        field — the About text (`description`) or the source repository
        (`github_url`). The change does not go live immediately: it is stored as
        a pending revision and an administrator re-reviews it before it replaces
        the published value. A `github_url` change is re-validated for repository
        shape before it is accepted. Only one pending edit per field is allowed
        at a time.
      operationId: submitPolicyRevision
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [field, value]
                  properties:
                    field: { type: string, enum: [description] }
                    value:
                      type: string
                      minLength: 10
                      maxLength: 20000
                      description: New Markdown About text.
                - type: object
                  required: [field, value]
                  properties:
                    field: { type: string, enum: [github_url] }
                    value:
                      type: string
                      format: uri
                      description: New source repository URL (re-validated for shape).
      responses:
        '201':
          description: The created pending revision.
          content:
            application/json:
              schema:
                type: object
                properties:
                  revision: { $ref: '#/components/schemas/PolicyRevision' }
                  policy_name: { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/BadRequest' }

  # ------------------------------------------------------------- Submissions
  /api/submissions:
    get:
      tags: [Submissions]
      summary: List your submissions
      operationId: listSubmissions
      responses:
        '200':
          description: Your submissions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  submissions:
                    type: array
                    items: { $ref: '#/components/schemas/Submission' }
    post:
      tags: [Submissions]
      summary: Submit a policy for review
      description: Requires `api:write`. Accepts a unified or split (definition + implementation) project.
      operationId: createSubmission
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubmissionInput' }
      responses:
        '201':
          description: The created submission and repository validation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  submission: { $ref: '#/components/schemas/Submission' }
                  validation: { type: object }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/submissions/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    patch:
      tags: [Submissions]
      summary: Update or resubmit a submission
      description: Requires `api:write`. Resubmit a rejected policy or edit its docs metadata.
      operationId: updateSubmission
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200':
          description: The updated submission.
          content:
            application/json:
              schema:
                type: object
                properties:
                  submission: { $ref: '#/components/schemas/Submission' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/submissions/{id}/history:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Submissions]
      summary: List a submission's review history
      operationId: listSubmissionHistory
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: Review history entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  entries:
                    type: array
                    items: { type: object }
  /api/submissions/validate:
    post:
      tags: [Submissions]
      summary: Validate a submission's GitHub repositories
      description: Requires `api:write`. Checks repo shape before you submit.
      operationId: validateSubmission
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                projectType: { type: string, enum: [unified, split] }
                githubUrl: { type: string, format: uri }
                implementationGithubUrl: { type: string, format: uri }
      responses:
        '200':
          description: The validation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  validation: { type: object }

  # ------------------------------------------------------------- Deployments
  /api/deployments:
    get:
      tags: [Deployments]
      summary: List your deployments
      operationId: listDeployments
      responses:
        '200':
          description: Your deployments, paginated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deployments:
                    type: array
                    items: { $ref: '#/components/schemas/Deployment' }
                  pagination: { type: object }
    post:
      tags: [Deployments]
      summary: Enqueue a deployment
      description: |
        Requires `api:write`. Fans out one deployment per target organization,
        optionally publishing and releasing.
      operationId: createDeployment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeploymentInput' }
      responses:
        '201':
          description: All target organizations were enqueued successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/DeploymentResult' }
        '207':
          description: >-
            Partial success — some target organizations were enqueued and
            others failed. Inspect each entry in `results`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/DeploymentResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/deployments/delete-failed:
    post:
      tags: [Deployments]
      summary: Delete all your failed deployments
      description: |
        Requires `api:write`. Permanently removes every **failed** deployment
        row you own (cascading its build jobs and logs), optionally narrowed to
        a single policy. Only your own rows are touched.
      operationId: deleteFailedDeployments
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                policyId:
                  type: string
                  format: uuid
                  description: Only delete failed deployments of this policy. Omit to clear all of yours.
      responses:
        '200':
          description: How many failed deployments were deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedCount: { type: integer }
                  deletedDeploymentIds:
                    type: array
                    items: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/deployments/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    delete:
      tags: [Deployments]
      summary: Delete a failed deployment
      description: |
        Requires `api:write`. Permanently removes a single **failed**
        deployment row (cascading its build jobs and logs). Returns `409` if
        the deployment is in any status other than `failed`.
      operationId: deleteDeployment
      responses:
        '200':
          description: The deleted deployment id.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedDeploymentId: { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /api/deployments/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Deployments]
      summary: Cancel an in-flight deployment
      description: Requires `api:write`.
      operationId: cancelDeployment
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /api/deployments/{id}/delete-release:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Deployments]
      summary: Delete a released deployment
      description: Requires `api:write`. Enqueues removal of a published release.
      operationId: deleteDeploymentRelease
      responses:
        '201':
          description: The enqueued cleanup job, if one was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: { type: [object, 'null'] }
  /api/deployments/{id}/dev-version:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Deployments]
      summary: Check whether a deployment's DEV asset still exists
      description: |
        Requires `api:read`. Probes Anypoint for this Publish-mode
        deployment's `-dev` asset version. Read-only reconnaissance (no
        status change, unlike `sync`) — the portal calls it lazily per row
        to decide whether to offer the "Delete DEV version" control.

        `dev_version_exists`:
          * `true` — at least one `-dev` peer is still present.
          * `false` — every tracked `-dev` peer is gone.
          * `null` — not a Publish-mode row, nothing to probe, or a
            transient Anypoint lookup failure.
      operationId: getDeploymentDevVersion
      responses:
        '200':
          description: The DEV-version probe result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dev_version_exists: { type: [boolean, 'null'] }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/deployments/{id}/history:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Deployments]
      summary: Get a deployment's job history and logs
      operationId: getDeploymentHistory
      responses:
        '200':
          description: The deployment's jobs and most-recent logs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items: { type: object }
                  job: { type: [object, 'null'] }
                  logs:
                    type: array
                    items: { type: object }
  /api/deployments/{id}/logs:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Deployments]
      summary: Stream a deployment's build logs
      description: Returns a Server-Sent Events stream of log chunks.
      operationId: streamDeploymentLogs
      responses:
        '200':
          description: A `text/event-stream` of log chunks.
          content:
            text/event-stream:
              schema: { type: string }
  /api/deployments/{id}/sync:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Deployments]
      summary: Reconcile a deployment's status with Anypoint
      description: Requires `api:write`.
      operationId: syncDeployment
      responses:
        '200':
          description: The reconciliation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  result: { type: string, enum: [in_sync, deleted, unknown] }
                  status: { type: string }
  /api/deployments/{id}/unpublish:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Deployments]
      summary: Unpublish a deployment
      description: Requires `api:write`. Enqueues removal of the published asset.
      operationId: unpublishDeployment
      responses:
        '201':
          description: The enqueued unpublish job, if one was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: { type: [object, 'null'] }

  # ------------------------------------------------------------- Connections
  /api/connections:
    get:
      tags: [Connections]
      summary: List your Anypoint connections
      operationId: listConnections
      responses:
        '200':
          description: Your connections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connections:
                    type: array
                    items: { $ref: '#/components/schemas/Connection' }
    post:
      tags: [Connections]
      summary: Create an Anypoint connection
      description: Requires `api:write`. The client secret is encrypted at rest and never returned.
      operationId: createConnection
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConnectionInput' }
      responses:
        '200':
          description: The created connection and credential validation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection: { $ref: '#/components/schemas/Connection' }
                  validation: { type: object }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/connections/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Connections]
      summary: Get a connection
      operationId: getConnection
      responses:
        '200':
          description: The connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection: { $ref: '#/components/schemas/Connection' }
                  is_owner: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Connections]
      summary: Update a connection
      description: Requires `api:write`. Credential fields are all-or-none.
      operationId: updateConnection
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConnectionPatch' }
      responses:
        '200':
          description: The updated connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection: { $ref: '#/components/schemas/Connection' }
        '400': { $ref: '#/components/responses/BadRequest' }
    delete:
      tags: [Connections]
      summary: Delete a connection
      description: Requires `api:write`.
      operationId: deleteConnection
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /api/connections/{id}/sync:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Connections]
      summary: Re-validate a connection's credentials
      description: Requires `api:write`.
      operationId: syncConnection
      responses:
        '200':
          description: The connection and validation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection: { $ref: '#/components/schemas/Connection' }
                  validation: { type: object }
  /api/connections/{id}/orgs:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Connections]
      summary: List the connection's Anypoint organizations
      operationId: listConnectionOrgs
      responses:
        '200':
          description: The organizations visible through the connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  orgs:
                    type: array
                    items: { $ref: '#/components/schemas/AnypointOrg' }
  /api/connections/{id}/workspaces:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Connections]
      summary: List workspaces the connection is shared with
      operationId: listConnectionWorkspaces
      responses:
        '200':
          description: The workspace shares for this connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  shared:
                    type: array
                    items: { type: object }
  /api/connections/{id}/impact:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Connections]
      summary: Summarize what depends on a connection
      operationId: getConnectionImpact
      responses:
        '200':
          description: Counts of dependents, for confirmation before deletion.
          content:
            application/json:
              schema:
                type: object
                properties:
                  active_deployments: { type: integer }
                  historical_deployments: { type: integer }
                  workspace_links: { type: integer }
                  has_credential: { type: boolean }
  /api/connections/validate:
    post:
      tags: [Connections]
      summary: Validate Anypoint credentials without saving
      description: Requires `api:write`.
      operationId: validateConnection
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConnectionCredentials' }
      responses:
        '200':
          description: Whether the credentials authenticate.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  error: { type: string }

  # -------------------------------------------------------------- Workspaces
  /api/workspaces:
    get:
      tags: [Workspaces]
      summary: List your workspace memberships
      operationId: listWorkspaces
      responses:
        '200':
          description: The workspaces you belong to.
          content:
            application/json:
              schema:
                type: object
                properties:
                  memberships:
                    type: array
                    items: { type: object }
    post:
      tags: [Workspaces]
      summary: Create a workspace
      description: Requires `api:write`.
      operationId: createWorkspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                slug: { type: string, minLength: 2, maxLength: 48 }
      responses:
        '201':
          description: The created workspace.
          content:
            application/json:
              schema:
                type: object
                properties:
                  workspace: { $ref: '#/components/schemas/Workspace' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/workspaces/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Workspaces]
      summary: Get a workspace with members and shares
      operationId: getWorkspace
      responses:
        '200':
          description: The workspace and its related data.
          content:
            application/json:
              schema: { type: object }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Workspaces]
      summary: Rename a workspace
      description: Requires `api:write`.
      operationId: updateWorkspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
      responses:
        '200':
          description: The updated workspace.
          content:
            application/json:
              schema:
                type: object
                properties:
                  workspace: { $ref: '#/components/schemas/Workspace' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/workspaces/{id}/archive:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Workspaces]
      summary: Archive a workspace
      description: Requires `api:write`.
      operationId: archiveWorkspace
      responses:
        '200':
          description: The new archive timestamp.
          content:
            application/json:
              schema:
                type: object
                properties:
                  archived_at: { type: [string, 'null'] }
    delete:
      tags: [Workspaces]
      summary: Unarchive a workspace
      description: Requires `api:write`.
      operationId: unarchiveWorkspace
      responses:
        '200':
          description: The cleared archive timestamp.
          content:
            application/json:
              schema:
                type: object
                properties:
                  archived_at: { type: [string, 'null'] }
  /api/workspaces/{id}/members:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Workspaces]
      summary: List workspace members
      operationId: listWorkspaceMembers
      responses:
        '200':
          description: The members of the workspace.
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items: { type: object }
    post:
      tags: [Workspaces]
      summary: Invite a member to a workspace
      description: Requires `api:write`.
      operationId: inviteWorkspaceMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                role: { type: string, enum: [admin, member] }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
  /api/workspaces/{id}/members/{userId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - name: userId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Workspaces]
      summary: Change a member's role or re-enable them
      description: Requires `api:write`.
      operationId: updateWorkspaceMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role: { type: string, enum: [admin, member] }
                disabled: { type: boolean, enum: [false] }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
    delete:
      tags: [Workspaces]
      summary: Remove a member from a workspace
      description: Requires `api:write`.
      operationId: removeWorkspaceMember
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /api/workspaces/{id}/members/{userId}/impact:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - name: userId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Workspaces]
      summary: Summarize what depends on a member
      operationId: getWorkspaceMemberImpact
      responses:
        '200':
          description: Counts of resources owned or authored by the member.
          content:
            application/json:
              schema:
                type: object
                properties:
                  role: { type: string, enum: [owner, admin, member] }
                  disabled_at: { type: [string, 'null'] }
                  owned_shared_connections: { type: integer }
                  deployments_authored: { type: integer }
  /api/workspaces/{id}/connections:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Workspaces]
      summary: Share a connection into a workspace
      description: Requires `api:write`.
      operationId: shareWorkspaceConnection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [connection_id]
              properties:
                connection_id: { type: string, format: uuid }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
    patch:
      tags: [Workspaces]
      summary: Re-enable a shared connection
      description: Requires `api:write`.
      operationId: updateWorkspaceConnection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [connection_id]
              properties:
                connection_id: { type: string, format: uuid }
                disabled: { type: boolean, enum: [false] }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
    delete:
      tags: [Workspaces]
      summary: Unshare a connection from a workspace
      description: Requires `api:write`.
      operationId: unshareWorkspaceConnection
      parameters:
        - name: connection_id
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /api/workspaces/{id}/connections/{connectionId}/impact:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - name: connectionId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Workspaces]
      summary: Summarize a shared connection's reach in a workspace
      operationId: getWorkspaceConnectionImpact
      responses:
        '200':
          description: Counts describing the shared connection's visibility and use.
          content:
            application/json:
              schema:
                type: object
                properties:
                  is_disabled: { type: boolean }
                  disabled_reason: { type: [string, 'null'] }
                  members_visible: { type: integer }
                  deployments_authored: { type: integer }

  # --------------------------------------------------------------- Transfers
  /api/transfers/{id}/accept:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Transfers]
      summary: Accept a policy transfer offered to you
      description: Requires `api:write`.
      operationId: acceptTransfer
      responses:
        '200':
          description: The accepted transfer and the policy now owned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transfer: { type: object }
                  policyId: { type: string }
  /api/transfers/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Transfers]
      summary: Cancel a transfer you initiated
      description: Requires `api:write`.
      operationId: cancelTransfer
      responses:
        '200':
          description: The cancelled transfer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transfer: { type: object }
  /api/transfers/{id}/reject:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Transfers]
      summary: Reject a policy transfer offered to you
      description: Requires `api:write`.
      operationId: rejectTransfer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                feedback: { type: string, maxLength: 4000 }
      responses:
        '200':
          description: The rejected transfer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transfer: { type: object }

  # ------------------------------------------------------------------ Tokens
  /api/tokens:
    get:
      tags: [Tokens]
      summary: List your personal access tokens
      operationId: listTokens
      responses:
        '200':
          description: Your tokens (secrets are never returned).
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens:
                    type: array
                    items: { $ref: '#/components/schemas/Token' }
    post:
      tags: [Tokens]
      summary: Create a personal access token
      description: |
        Requires a signed-in **browser session** — a token cannot mint another
        token, so this endpoint rejects personal-access-token (bearer)
        authentication with `403`. The full secret is returned exactly once in
        the response. This is the one operation in this API that is **not**
        PAT-eligible; create tokens from the dashboard Settings page, not the
        interactive playground.
      operationId: createToken
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scopes]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                scopes:
                  type: array
                  minItems: 1
                  items: { type: string, enum: [api:read, api:write] }
                expiresInDays:
                  type: [integer, 'null']
                  enum: [30, 90, 365, null]
      responses:
        '201':
          description: The new token's secret (shown once) and its record.
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  record: { $ref: '#/components/schemas/Token' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/tokens/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    delete:
      tags: [Tokens]
      summary: Revoke a personal access token
      description: Requires `api:write`. Revocation is immediate and cannot be undone.
      operationId: revokeToken
      responses:
        '200':
          description: The revoked token's record.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  record: { $ref: '#/components/schemas/Token' }

  # ----------------------------------------------------------------- Account
  /api/me:
    get:
      tags: [Account]
      summary: Get your profile
      operationId: getMe
      responses:
        '200':
          description: Your profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
    patch:
      tags: [Account]
      summary: Update your profile
      description: Requires `api:write`.
      operationId: updateMe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                company: { type: string, minLength: 1, maxLength: 120 }
      responses:
        '200':
          description: Your updated profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user: { $ref: '#/components/schemas/User' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/me/transfers:
    get:
      tags: [Account]
      summary: List transfers involving you
      operationId: listMyTransfers
      parameters:
        - name: role
          in: query
          required: false
          schema: { type: string, enum: [recipient, initiator] }
      responses:
        '200':
          description: Transfers where you are the recipient or initiator.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transfers:
                    type: array
                    items: { type: object }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: p4a_<token>
      description: 'A personal access token, sent as `Authorization: Bearer p4a_…`.'
    cookieAuth:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: >-
        A signed-in **browser session** cookie. Used only by `POST /api/tokens`,
        which mints new tokens and therefore rejects personal access tokens — a
        token cannot mint another token. This operation cannot be exercised from
        the interactive playground; create tokens from the dashboard Settings page.
  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: The resource's UUID.
  responses:
    Ok:
      description: Success.
      content:
        application/json:
          schema:
            type: object
            properties:
              ok: { type: boolean, const: true }
    BadRequest:
      description: The request body failed validation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: The token is missing the required scope, or the action is not allowed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: The resource does not exist or is not visible to you.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: The resource is in a state that does not allow this action.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
      example:
        error: 'Not authenticated'
    Policy:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        category: { type: string }
        status: { type: string }
        liked:
          type: boolean
          description: >-
            Whether the current viewer has liked this policy. Present on
            `GET /api/policies/{id}` and reflects the authenticated caller
            (false for anonymous callers). Folded into the policy payload so
            the catalog detail page need not make a separate like-state
            round-trip (#666).
        github_has_issues:
          type: boolean
          nullable: true
          description: >-
            Whether the policy's upstream GitHub repository currently accepts
            issues, captured at submission/validation time. Drives the "Report
            an issue" affordance on the detail page (#66): a prefilled
            `/issues/new` handoff is offered unless this is explicitly `false`.
            `null` means unknown (pre-#66 rows or a repo that couldn't be
            re-read) and fails open. The live re-check that can correct a
            tracker disabled after publish is exposed at
            `GET /api/policies/{id}/issues-enabled`.
      example:
        id: '3f1c2b4a-1e2d-4c3b-9a8f-0d1e2f3a4b5c'
        name: 'Rate limit'
        description: 'Throttle requests per agent.'
        category: 'Traffic management'
        status: 'approved'
        liked: false
        github_has_issues: true
    MuleSoftPolicySummary:
      type: object
      description: >-
        A MuleSoft out-of-the-box (Omni/Flex Gateway) policy as returned by
        `GET /api/policies?source=mulesoft`. Summary shape — no documentation
        body; fetch that via `GET /api/mulesoft-policies/{slug}`.
      properties:
        slug: { type: string, description: 'Stable identifier from the docs URL.' }
        name: { type: string }
        summary: { type: string }
        category: { type: string }
        direction:
          type: string
          enum: [inbound, outbound]
          description: >-
            Enforcement leg derived from the policy slug: `outbound` for slugs
            ending in `-outbound`/`-response`, else `inbound`.
        first_version:
          type: string
          nullable: true
          description: 'First Omni/Flex Gateway version the policy was available in, if known.'
        doc_url:
          type: string
          format: uri
          description: 'Canonical docs.mulesoft.com page for the policy.'
        synced_at:
          type: string
          format: date-time
          nullable: true
          description: 'When the row was last synced from docs.mulesoft.com.'
      example:
        slug: 'rate-limiting'
        name: 'Rate Limiting'
        summary: 'Monitors access to an API.'
        category: 'Quality of Service'
        direction: 'inbound'
        first_version: 'v1.2.0'
        doc_url: 'https://docs.mulesoft.com/gateway/latest/policies-included-rate-limiting'
        synced_at: '2026-07-08T15:00:00Z'
    MuleSoftPolicy:
      allOf:
        - $ref: '#/components/schemas/MuleSoftPolicySummary'
        - type: object
          description: >-
            Full MuleSoft policy including the documentation body rendered on
            the in-portal detail page.
          properties:
            body_md:
              type: string
              description: >-
                The policy documentation synced from docs.mulesoft.com —
                markdown with embedded (sanitizable) HTML tables. Relative doc
                links are rewritten to absolute docs.mulesoft.com URLs.
    PolicyPatch:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        category: { type: string }
        videoTutorialUrl: { type: string }
        examplesUrl: { type: string }
        githubUrl: { type: string, format: uri }
        implementationGithubUrl: { type: string }
        iconUrl: { type: string }
        iconDisabled: { type: boolean }
    PolicyRevision:
      type: object
      description: >-
        An owner-proposed edit to a single published-policy field, pending an
        administrator's re-review before it goes live.
      properties:
        id: { type: string, format: uuid }
        policy_id: { type: string, format: uuid }
        field: { type: string, enum: [description, github_url] }
        proposed_value: { type: string }
        previous_value:
          type: string
          nullable: true
          description: The live value at the time the edit was proposed.
        status: { type: string, enum: [pending, approved, rejected] }
        feedback:
          type: string
          nullable: true
          description: Reviewer note left on approval or rejection.
        submitted_at: { type: string, format: date-time }
        reviewed_at: { type: string, format: date-time, nullable: true }
    Comment:
      type: object
      description: >-
        A discussion comment on a policy. Top-level comments have a null
        `parentId`; single-level replies point at a top-level comment. A
        soft-deleted comment has `deleted: true` with empty `body` and null
        `authorId`/`authorName` so its live replies stay intact.
      properties:
        id: { type: string, format: uuid }
        policyId: { type: string, format: uuid }
        parentId:
          type: string
          format: uuid
          nullable: true
          description: The top-level comment this replies to, or null for a top-level comment.
        authorId:
          type: string
          format: uuid
          nullable: true
          description: Null on a soft-deleted comment.
        authorName:
          type: string
          nullable: true
          description: The author's display name; null on a soft-deleted comment.
        body:
          type: string
          description: Markdown source. Empty string on a soft-deleted comment.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        deleted:
          type: boolean
          description: True when the comment has been (soft-)deleted.
      example:
        id: 'c1d2e3f4-a5b6-7089-90ab-cdef01234567'
        policyId: '3f1c2b4a-1e2d-4c3b-9a8f-0d1e2f3a4b5c'
        parentId: null
        authorId: 'a1b2c3d4-5e6f-7081-92a3-b4c5d6e7f809'
        authorName: 'Jane Doe'
        body: 'How do I configure the token bucket size?'
        createdAt: '2026-07-02T10:15:00Z'
        updatedAt: '2026-07-02T10:15:00Z'
        deleted: false
    CommentInput:
      type: object
      required: [body]
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 10000
          description: Markdown source of the comment.
        parentId:
          type: string
          format: uuid
          description: >-
            Optional. The id of the top-level comment to reply to. Omit for a
            top-level comment. Replies are limited to a single level.
    Submission:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        status: { type: string }
        githubUrl: { type: string, format: uri }
      example:
        id: 'a1b2c3d4-5e6f-7081-92a3-b4c5d6e7f809'
        name: 'Rate limit'
        description: 'Throttle requests per agent based on a token bucket.'
        status: 'pending'
        githubUrl: 'https://github.com/acme/rate-limit-policy'
    SubmissionInput:
      type: object
      required: [name, description, githubUrl]
      properties:
        projectType: { type: string, enum: [unified, split] }
        name: { type: string, minLength: 2, maxLength: 120 }
        description: { type: string, minLength: 10, maxLength: 20000 }
        githubUrl: { type: string, format: uri }
        implementationGithubUrl:
          type: string
          format: uri
          description: Required when `projectType` is `split`.
        category: { type: string }
        videoTutorialUrl: { type: string }
        examplesUrl: { type: string }
        iconUrl: { type: string }
        iconDisabled: { type: boolean }
        deliversIdeaId:
          type: string
          format: uuid
          description: >-
            Optional. The id of an approved Policy Idea this submission delivers. Must
            reference an idea that is `approved` and visible to you, otherwise the
            request is rejected. This records the *declared intent* to deliver the
            idea while the policy is under review; the idea flips to `delivered`
            (and its original submitter is notified) only once the policy is
            approved and published.
      example:
        projectType: 'unified'
        name: 'Rate limit'
        description: 'Throttle requests per agent based on a token bucket algorithm.'
        githubUrl: 'https://github.com/acme/rate-limit-policy'
        category: 'Traffic management'
    Deployment:
      type: object
      properties:
        id: { type: string, format: uuid }
        policy_id: { type: string, format: uuid }
        connection_id: { type: string, format: uuid }
        workspace_id: { type: string, format: uuid }
        status: { type: string }
        anypoint_org_id: { type: string }
        definition_asset_version:
          type: [string, 'null']
          description: 'Version the definition asset was published at. Distinct from the implementation peer only for split-model policies.'
        implementation_asset_version:
          type: [string, 'null']
          description: 'Version the implementation asset was published at.'
        home_doc_status:
          type: [string, 'null']
          enum: [published, skipped, failed, null]
          description: |
            Outcome of the best-effort documentation page published to the
            Exchange asset after a successful deploy. `failed` means the deploy
            succeeded but the Exchange listing carries no docs page. `null` =
            not attempted (legacy or simulated deploys).
    DeploymentResult:
      type: object
      description: |
        One per-target outcome from a fan-out enqueue. On a timed-out request
        the deployment may still have been queued — poll `GET /api/deployments`
        with the returned `deployment.id` before retrying. A retry of the same
        target within ~5 minutes is de-duplicated: it returns the original
        deployment (with `deployment.deduped: true`) instead of queuing a
        duplicate.
      properties:
        ok: { type: boolean }
        organizationId: { type: string }
        organizationName: { type: [string, 'null'] }
        error: { type: string, description: 'Set when this target failed to enqueue.' }
        deployment:
          type: object
          description: 'The queued (or de-duplicated) deployment row; absent on a failed target.'
          properties:
            id: { type: string, format: uuid }
            status: { type: string }
            deduped:
              type: boolean
              description: 'True when an in-window retry matched an existing deployment instead of creating a new one.'
    DeploymentInput:
      type: object
      required: [policyId, connectionId, workspaceId, targetOrganizationIds]
      properties:
        policyId: { type: string, format: uuid }
        connectionId: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        targetOrganizationIds:
          type: array
          minItems: 1
          maxItems: 50
          items: { type: string }
        doPublish: { type: boolean }
        doRelease: { type: boolean }
        cleanupDev: { type: boolean }
        ref: { type: string }
        implementationRef: { type: string }
      example:
        policyId: '3f1c2b4a-1e2d-4c3b-9a8f-0d1e2f3a4b5c'
        connectionId: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'
        workspaceId: '9f8e7d6c-5b4a-3210-fedc-ba9876543210'
        targetOrganizationIds: ['a0000000-0000-0000-0000-000000000001']
        doPublish: true
        doRelease: false
    Connection:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        host: { type: string, enum: [us, eu, custom] }
        client_id: { type: string, description: 'Masked; the secret is never returned.' }
        organization_id: { type: string }
      example:
        id: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'
        name: 'Acme production'
        host: 'us'
        client_id: 'abcd…'
        organization_id: 'a0000000-0000-0000-0000-000000000001'
    ConnectionCredentials:
      type: object
      required: [host, clientId, clientSecret, organizationId]
      properties:
        host: { type: string, enum: [us, eu, custom] }
        customDomain: { type: [string, 'null'], format: uri }
        clientId: { type: string, minLength: 4 }
        clientSecret: { type: string, minLength: 4 }
        organizationId: { type: string, minLength: 4 }
    ConnectionInput:
      allOf:
        - $ref: '#/components/schemas/ConnectionCredentials'
        - type: object
          required: [name]
          properties:
            name: { type: string, minLength: 2, maxLength: 80 }
      example:
        name: 'Acme production'
        host: 'us'
        clientId: '1234abcd'
        clientSecret: 's3cr3t-value'
        organizationId: 'a0000000-0000-0000-0000-000000000001'
    ConnectionPatch:
      type: object
      description: 'Credential fields (clientId, clientSecret, organizationId) are all-or-none.'
      properties:
        name: { type: string }
        host: { type: string, enum: [us, eu, custom] }
        customDomain: { type: [string, 'null'], format: uri }
        clientId: { type: string, minLength: 4 }
        clientSecret: { type: string, minLength: 4 }
        organizationId: { type: string, minLength: 4 }
    AnypointOrg:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        isPrimary: { type: boolean }
        fullPath: { type: string }
    Workspace:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
      example:
        id: '9f8e7d6c-5b4a-3210-fedc-ba9876543210'
        name: 'Platform team'
        slug: 'platform-team'
    Token:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        token_prefix: { type: string }
        scopes:
          type: array
          items: { type: string, enum: [api:read, api:write] }
        expires_at: { type: [string, 'null'], format: date-time }
        last_used_at: { type: [string, 'null'], format: date-time }
        revoked_at: { type: [string, 'null'], format: date-time }
        created_at: { type: string, format: date-time }
      example:
        id: '11112222-3333-4444-5555-666677778888'
        name: "My laptop's CLI"
        token_prefix: 'p4a_abcd'
        scopes: ['api:read']
        expires_at: '2026-09-22T00:00:00.000Z'
        last_used_at: null
        revoked_at: null
        created_at: '2026-06-24T12:00:00.000Z'
    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        company: { type: [string, 'null'] }
        is_admin: { type: boolean }
      example:
        id: 'deadbeef-0000-1111-2222-333344445555'
        email: 'you@example.com'
        company: 'Acme'
        is_admin: false
