openapi: 3.1.0
info:
  title: FixedMark API
  version: 1.0.0
  description: |
    The FixedMark API. Every customer route takes a bearer token: an API key
    (`fm_` plus an 8 character lookup prefix plus the secret) or a dashboard
    session. A key acts only in its own org, only with the scopes it holds,
    and an app-scoped key only on its app.

    Access to an app is the union of the caller's org role, a direct grant on
    the app, and a grant to a team the caller is in. An app the caller cannot
    reach answers 404, the same as an app that does not exist.

    Every error answers one body: `{"error": {"code", "message", "request_id"}}`.
    The codes are listed under `ErrorCode`. `request_id` is also in the
    `X-Request-Id` header and in every log line of the request.

    Each API key has a token bucket of 600 requests a minute, and 60 a minute
    on the routes marked `x-fm-ratelimit: create`. Every answer carries
    `RateLimit-Limit` and `RateLimit-Remaining`; over the limit the answer is
    429 `rate_limited` with `Retry-After`. The policy test route is metered
    the same way for a dashboard session, per user. Requests with no token
    or a token that is refused share a bucket of 60 a minute per source
    address, with the same headers and the same 429.

    Tokens travel in headers and bodies, never in paths or query strings.

    Every usage row carries `direction` and every period object carries
    `product`. Both are `egress` or `ingress`. Egress is the only product in
    v1; ingress rows appear when the inbound product ships.
servers:
- url: https://api.fixedmark.com
  description: Production
security:
- bearer: []
tags:
- name: me
- name: orgs
- name: members
- name: invitations
- name: teams
- name: app members
- name: apps
- name: ips
- name: credentials
- name: connectors
- name: policy
- name: jobs
- name: usage
- name: logs
- name: api keys
- name: audit
- name: sso
- name: billing
- name: plans
- name: webhooks
- name: vercel
- name: health
paths:
  /healthz:
    get:
      tags:
      - health
      operationId: getHealthz
      summary: Answers 200 while the process serves at all.
      security: []
      responses:
        '200':
          description: The process is up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
              example:
                status: ok
                version: 5295b4f
  /readyz:
    get:
      tags:
      - health
      operationId: getReadyz
      summary: Answers 200 only when Postgres answers and the job client runs.
      security: []
      responses:
        '200':
          description: Ready.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
              example:
                status: ready
        '503':
          description: Not ready. The reason is db or jobs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
              example:
                status: not ready
                reason: db
  /v1/me:
    get:
      tags:
      - me
      operationId: getMe
      summary: 'Who the token is: the user or key, its org, role, and scopes.'
      x-fm-action: none
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/me/orgs:
    get:
      tags:
      - me
      operationId: listMyOrgs
      summary: Every org the signed-in user belongs to, and the active one.
      x-fm-action: none
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/State'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs:
    post:
      tags:
      - orgs
      operationId: createOrg
      summary: Create an org owned by the caller.
      x-fm-action: none
      x-fm-ratelimit: create
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrgBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Org'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}:
    get:
      tags:
      - orgs
      operationId: getOrg
      summary: The org.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    patch:
      tags:
      - orgs
      operationId: updateOrg
      summary: Rename the org or set its country and billing email.
      x-fm-action: org:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrgPatch'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - orgs
      operationId: deleteOrg
      summary: Delete the org. The owner only.
      description: Revokes every API key and proxy credential, ends the org's entitlements, puts every IP in grace so it stops serving at the next lifecycle tick, cancels the subscription at its channel now, mails the owners, and purges the org's operational rows 30 days later. Until then the org reads as deleting and refuses every write with org.deleting. A second call answers the same 202. An org a Heroku or Vercel subscription bills answers 409 org.marketplace_managed.
      x-fm-action: org:delete
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: Deleting.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgDeletion'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/export:
    post:
      tags:
      - orgs
      operationId: exportOrg
      summary: Export the org's data. The owner only.
      description: Queues one JSON file of the org's members, apps, IPs, credential and key metadata without hashes, usage rows with their direction, invoices, and audit log, and mails a link that works for 7 days to every owner. One export per org per day; a second call answers 200 with the export already made.
      x-fm-action: org:delete
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The export already made today.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '202':
          description: Queued.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/Unavailable'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/transfer:
    post:
      tags:
      - orgs
      operationId: transferOrg
      summary: Make another member the owner.
      x-fm-action: org:delete
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferReply'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/members:
    get:
      tags:
      - members
      operationId: listMembers
      summary: The members with their roles, teams, and app grants.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MemberView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/members/{user}:
    patch:
      tags:
      - members
      operationId: updateMemberRole
      summary: Change a member's role.
      x-fm-action: member:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoleBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoleBody'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - members
      operationId: removeMember
      summary: Remove a member and revoke the keys they made.
      x-fm-action: member:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveReply'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/invitations:
    get:
      tags:
      - invitations
      operationId: listInvitations
      summary: The open invitations.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Invitation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - invitations
      operationId: createInvitation
      summary: Invite an email to the org, or to one app. The token travels only in the mailed link.
      x-fm-action: member:invite
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Issued'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/invitations/{invite}:
    delete:
      tags:
      - invitations
      operationId: revokeInvitation
      summary: Revoke an open invitation.
      x-fm-action: member:invite
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: invite
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/invitations/{invite}/resend:
    post:
      tags:
      - invitations
      operationId: resendInvitation
      summary: Send the invitation again with a fresh link.
      x-fm-action: member:invite
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: invite
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Issued'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/invitations/lookup:
    post:
      tags:
      - invitations
      operationId: lookupInvitation
      summary: What an invitation token invites the caller to.
      x-fm-action: none
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              properties:
                token:
                  type: string
                  description: The token from the invitation link. It travels in the body, never in a path or query.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preview'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/invitations/accept:
    post:
      tags:
      - invitations
      operationId: acceptInvitation
      summary: Join the org or app an invitation token names.
      x-fm-action: none
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              properties:
                token:
                  type: string
                  description: The token from the invitation link. It travels in the body, never in a path or query.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Joined'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/invitations/decline:
    post:
      tags:
      - invitations
      operationId: declineInvitation
      summary: Decline an invitation.
      x-fm-action: none
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              properties:
                token:
                  type: string
                  description: The token from the invitation link. It travels in the body, never in a path or query.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/teams:
    get:
      tags:
      - teams
      operationId: listTeams
      summary: The org's teams.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TeamView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - teams
      operationId: createTeam
      summary: Create a team.
      x-fm-action: team:manage
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamCreateBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/teams/{id}:
    get:
      tags:
      - teams
      operationId: getTeam
      summary: A team with its people and app grants.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamDetailView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    patch:
      tags:
      - teams
      operationId: updateTeam
      summary: Rename a team or link an SSO group. Linking a group to a team with hand-added members answers 409 team.idp_confirm naming them, until the request sends confirm true.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamPatchBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - teams
      operationId: deleteTeam
      summary: Delete a team and its grants.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/teams/{id}/members/{user}:
    put:
      tags:
      - teams
      operationId: addTeamMember
      summary: Add an org member to the team.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - teams
      operationId: removeTeamMember
      summary: Remove a member from the team.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/teams/{id}/apps/{app}:
    put:
      tags:
      - teams
      operationId: setTeamGrant
      summary: Grant the team a role on an app.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: app
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppRoleBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamGrantView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - teams
      operationId: removeTeamGrant
      summary: Remove the team's grant on an app.
      x-fm-action: team:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: app
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/members:
    get:
      tags:
      - app members
      operationId: listAppMembers
      summary: 'Who reaches the app: direct grants and team grants.'
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppMembersView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/members/{user}:
    put:
      tags:
      - app members
      operationId: setAppMember
      summary: Grant a user, or with kind=team a team, a role on the app.
      x-fm-action: app:members
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: kind
        in: query
        required: false
        schema:
          type: string
          enum:
          - user
          - team
        description: team when {user} is a team id, granting the team; default user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppRoleBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/AppGrantView'
                - $ref: '#/components/schemas/TeamGrantView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - app members
      operationId: removeAppMember
      summary: Remove a user's, or with kind=team a team's, grant on the app.
      x-fm-action: app:members
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: kind
        in: query
        required: false
        schema:
          type: string
          enum:
          - user
          - team
        description: team when {user} is a team id, granting the team; default user.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Status'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/seats:
    get:
      tags:
      - billing
      operationId: getSeats
      summary: Seats used and paid for.
      x-fm-action: billing:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeatsView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/sso:
    get:
      tags:
      - sso
      operationId: getSSO
      summary: The org's SSO setup and domains.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/View'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - sso
      operationId: enableSSO
      summary: Turn SSO on for verified domains.
      x-fm-action: sso:configure
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnableInput'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Enabled'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    patch:
      tags:
      - sso
      operationId: updateSSO
      summary: Enforce SSO or stop enforcing it.
      x-fm-action: sso:configure
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enforced:
                  type: boolean
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/View'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - sso
      operationId: disableSSO
      summary: Turn SSO off.
      x-fm-action: sso:configure
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/View'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/sso/portal:
    post:
      tags:
      - sso
      operationId: ssoPortalLink
      summary: A short-lived link to the identity provider setup portal.
      x-fm-action: sso:configure
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PortalBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required:
                - link
                - intent
                properties:
                  link:
                    type: string
                  intent:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/audit:
    get:
      tags:
      - audit
      operationId: listAudit
      summary: The org's audit log, newest first.
      x-fm-action: audit:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 100
        description: Rows per page.
      - name: before
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
        description: next_cursor from the page before.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/api-keys:
    get:
      tags:
      - api keys
      operationId: listAPIKeys
      summary: The org's API keys. Never the secret.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/APIKeyView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - api keys
      operationId: createAPIKey
      summary: Create an API key. The answer holds the key once; it is never shown again.
      x-fm-action: token:rotate
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateKeyBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedKeyReply'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/api-keys/{key}:
    delete:
      tags:
      - api keys
      operationId: revokeAPIKey
      summary: Revoke an API key.
      x-fm-action: token:rotate
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: key
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIKeyView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps:
    get:
      tags:
      - apps
      operationId: listApps
      summary: 'The apps the caller can see: every app for an owner, admin, or developer, and the apps its grants name for anyone else.'
      x-fm-action: app:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AppSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - apps
      operationId: createApp
      summary: Create an app in the caller's org.
      x-fm-action: app:create
      x-fm-ratelimit: create
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAppBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}:
    get:
      tags:
      - apps
      operationId: getApp
      summary: The app and the IPs it holds.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - apps
      operationId: deleteApp
      summary: Delete an app that holds no IPs.
      description: Marks the app deleted, drops its grants, and revokes its scoped keys, open invitations, and proxy credentials. An app that still holds IPs, backs a marketplace add-on, or has linked Vercel projects answers 409.
      x-fm-action: app:delete
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppDeleted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/ips:
    post:
      tags:
      - ips
      operationId: createIP
      summary: Provision a dedicated IP for the app. Answers the job to watch.
      x-fm-action: ip:allocate
      x-fm-ratelimit: create
      security:
      - bearer: []
      - internal: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: Idempotency-Key
        in: header
        required: true
        schema:
          type: string
          maxLength: 255
        description: A repeat with the same key answers the first job.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateIPBody'
      responses:
        '202':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/ips:
    get:
      tags:
      - ips
      operationId: listIPs
      summary: The IPs the caller can see, in pages. With app, one app's; an app the caller cannot read answers 404.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: app
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: Only this app's IPs.
      - name: after
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: next_cursor of the page before.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 100
        description: IPs per page.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IPPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/ips/{id}:
    get:
      tags:
      - ips
      operationId: getIP
      summary: An IP with its listeners and routes.
      x-fm-action: app:read
      security:
      - bearer: []
      - internal: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IP'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/ips/{id}/reactivate:
    post:
      tags:
      - ips
      operationId: reactivateIP
      summary: Put a suspended or quarantined IP back on a node under the same address.
      description: The IP's routes that the suspension left with no placement go back on the ports they held, and read pending with state_reason ip_retired until the node binds them. When another live route now holds one of those ports, the reply is 409 ip.not_reactivatable naming both routes, and no job starts.
      x-fm-action: ip:allocate
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/apps/{id}/connectors:
    get:
      tags:
      - connectors
      operationId: listConnectors
      summary: The app's connectors, each with the sessions nodes report for it. No reply after the create holds a token.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - $ref: '#/components/parameters/AppID'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - connectors
      operationId: createConnector
      summary: Add a connector to the app. Answers the connector, its token once, and the command that installs fixedmarkd with it.
      description: 402 connector.not_entitled answers a plan without inbound_tunnel, with feature inbound_tunnel. connector.quota answers an org that holds every connector its plan's connectors quota allows. The token is fmc_ and 52 lowercase base32 characters; the server keeps only its sha256 and the 8 characters after fmc_.
      x-fm-action: route:edit
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - $ref: '#/components/parameters/AppID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConnectorBody'
      responses:
        '201':
          description: Created.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Refused. code is connector.name_invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/connectors/{cid}:
    get:
      tags:
      - connectors
      operationId: getConnector
      summary: One connector of the app, with its sessions.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: cid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connector'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - connectors
      operationId: deleteConnector
      summary: Delete a connector. A connector a live route names answers 409 connector.in_use with the route ids.
      x-fm-action: route:edit
      security:
      - bearer: []
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: cid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '204':
          description: Deleted. Its token stops working at once.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Refused. connector.in_use names the live routes that name the connector.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorInUse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/connectors/{cid}/rotate:
    post:
      tags:
      - connectors
      operationId: rotateConnector
      summary: Give a connector a new token. The old token stops working at once, and a session it opened closes with token_rotated.
      x-fm-action: route:edit
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: cid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK. The token shows this once.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorCreated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/connector/plan:
    get:
      tags:
      - connectors
      operationId: getConnectorPlan
      summary: The plan fixedmarkd serves, for the connector whose token is the bearer. Metered at 60 a minute per token prefix.
      description: Version 2. Each endpoint is an IP a live route of the connector is placed on, by its public address, on port 443 with SNI connector.fixedmark.net, those serving a route first. Each route lists its placements, active and healthy first. acme is present when a route terminates TLS in fixedmarkd. fixedmarkd refuses any other version and exits 4. Fetch it again after refresh_after_s seconds.
      x-fm-action: none
      security:
      - connector: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorPlan'
        '401':
          description: connector.token_invalid. The token is malformed, unknown, rotated, or its connector deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: connector.disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/connector/acme/dns01:
    post:
      tags:
      - connectors
      operationId: createConnectorChallenge
      summary: Publish a DNS-01 challenge TXT at _acme-challenge.<hostname> for the connector whose token is the bearer.
      description: The hostname must be a live http route the connector serves, the same routes its plan lists. The record is exact, never a wildcard, at TTL 60, and holds at most 2 values. A hostname gets 10 challenges an hour and a connector 60. A challenge is a renewal only when the control plane certified the hostname within 90 days, on a valid report that followed a challenge of the same connector turning visible within 2 hours; any other is a first issue. First issues are capped per 7 days across FixedMark and per org. A repeat of a live challenge answers 200 with its id. The sweep removes a challenge after an hour, or within a minute once its route is disabled, deleted, or unplaced, its org blocked, or its connector deleted or disabled. Metered at 120 calls a minute per token prefix across the relay routes.
      x-fm-action: none
      security:
      - connector: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectorChallengeCreate'
      responses:
        '201':
          description: Created. visible turns true once every authoritative nameserver answers the value.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorChallenge'
        '200':
          description: The live challenge with the same value.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorChallenge'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: connector.token_invalid. The token is malformed, unknown, rotated, or its connector deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: connector.disabled, or acme.hostname_not_route for a hostname that is not a live http route the connector serves.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: acme.value_invalid. The value is not exactly 43 base64url characters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: acme.rate_limited past the per-name, per-hostname, or per-connector limit, acme.budget_exhausted when the 7-day budget of first issues is spent, acme.org_budget_exhausted when the org's share of it is, or rate_limited past the call bucket. Retry after Retry-After seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: acme.value_in_use. Another connector's live challenge holds the value at this name, as after the route moved connectors. Start a new order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: acme.dns_unavailable. The tunnel zone is not written on this control plane.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/connector/acme/dns01/{id}:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The challenge.
    get:
      tags:
      - connectors
      operationId: getConnectorChallenge
      summary: Whether the connector's challenge is visible on every authoritative nameserver of the tunnel zone.
      x-fm-action: none
      security:
      - connector: []
      responses:
        '200':
          description: OK. A deleted or swept challenge is never visible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorChallenge'
        '401':
          description: connector.token_invalid. The token is malformed, unknown, rotated, or its connector deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: connector.disabled, or acme.hostname_not_route for a hostname that is not a live http route the connector serves.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: acme.challenge_unknown. The connector has no challenge with that id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - connectors
      operationId: deleteConnectorChallenge
      summary: Remove the connector's challenge and its value from the zone. A challenge already gone answers the same.
      x-fm-action: none
      security:
      - connector: []
      responses:
        '204':
          description: Deleted.
        '401':
          description: connector.token_invalid. The token is malformed, unknown, rotated, or its connector deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: connector.disabled, or acme.hostname_not_route for a hostname that is not a live http route the connector serves.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: acme.challenge_unknown. The connector has no challenge with that id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/connector/certs/{hostname}:
    put:
      tags:
      - connectors
      operationId: reportConnectorCert
      summary: Record the certificate state fixedmarkd holds for the hostname of a live http route it serves.
      description: A report that names no certificate keeps the last not_after, serial, and issuer reported. A valid report with a serial and a not_after within 100 days certifies the hostname, so its next challenges are renewals, only when a challenge of this connector for it turned visible within the 2 hours before.
      x-fm-action: none
      security:
      - connector: []
      parameters:
      - name: hostname
        in: path
        required: true
        schema:
          type: string
        description: The route's hostname.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectorCertReport'
      responses:
        '204':
          description: Recorded.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: connector.token_invalid. The token is malformed, unknown, rotated, or its connector deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: connector.disabled, or acme.hostname_not_route for a hostname that is not a live http route the connector serves.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: cert.state_invalid. The state is not one of the five, or not_after is not RFC 3339.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/routes:
    get:
      tags:
      - apps
      operationId: listRoutes
      summary: The app's live inbound routes, each with its hostname, its placements, and the state its nodes report.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The app.
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - apps
      operationId: createRoute
      summary: Add an inbound route to the app. A shared route is placed on two shared ingress IPs of its region and reached at <name>.<org-slug>.<zone>, TCP on a port from 20000 to 29999 or TLS on 443 by its hostname. A dedicated route goes on one IP of the app. Answers the route, pending, and the job that pushes it to its nodes.
      description: A shared placement and an http route need a connector origin (route.kind_origin). An sni_pattern or a chosen port needs a dedicated placement. The first route with a hostname fixes the org's slug, from org_slug or the org's name; a later org_slug is ignored, and GET /v1/orgs/{id} shows the slug in tunnel.slug. 402 route.tunnel_not_entitled answers a plan without inbound_tunnel, and 402 route.dedicated_not_entitled a dedicated placement on a plan without inbound_ip, each with feature. route.hostname_quota carries the quota in limit. route.pool_full is 503, retried after the pool grows. The job's stream lists validating_origin, writing_route, pushing_to_node, and waiting_for_listener.
      x-fm-action: route:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The app.
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
          maxLength: 255
        description: A repeat with the same key answers the first route and job.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRouteBody'
      responses:
        '202':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature, inbound_tunnel for a route, inbound_ip for a dedicated placement, or access for an access route.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Refused. code says why, such as route.name_invalid, route.kind_origin, or route.origin_private.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          description: The shared ingress pool of the region is full (route.pool_full), or the placement could not be written (route.placement_store). Retry in a few minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/apps/{id}/routes/{route_id}:
    get:
      tags:
      - apps
      operationId: getRoute
      summary: One live route of the app.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The app.
      - name: route_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Route'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - apps
      operationId: deleteRoute
      summary: End an inbound route. Its partner policies go with it, its placements are released, and its nodes drop it.
      x-fm-action: route:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The app.
      - name: route_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '204':
          description: Deleted.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/routes/{route_id}/placement:
    put:
      tags:
      - apps
      operationId: setRoutePlacement
      summary: Move the route between the shared pool and a dedicated IP of the app. The route keeps its id, name, hostname, and partners; a TCP route keeps its port when it is free on the new IPs. Audited as route:placement.
      description: 'Every placement needs what a create needs: inbound_tunnel (402 route.tunnel_not_entitled), or access for an access route. {mode: dedicated, ip_id} also needs inbound_ip (402 route.dedicated_not_entitled) and an active dedicated IP of the app (409 route.ip_not_dedicated). {mode: shared} needs a connector origin (422 route.kind_origin) and no sni_pattern, and answers 503 route.pool_full when the region has no room. A route already in the placement asked for answers 200 with changed false and does not move: a shared route stays in its group, and only a shared route with no live placement, such as one a drain parked, is placed again. Partners dial a tcp route at its IP and port, so moving a tcp route with partners needs confirm true, else 409 route.placement_unconfirmed.'
      x-fm-action: route:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The app.
      - name: route_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetRoutePlacementBody'
      responses:
        '200':
          description: Unchanged. The route is already in that placement; changed is false.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoutePlacementChanged'
        '202':
          description: Moved. Its new nodes serve it once they report.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoutePlacementChanged'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature, inbound_tunnel for a route, inbound_ip for a dedicated placement, or access for an access route.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Refused. code says why, such as route.kind_origin or route.placement_invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          description: The shared ingress pool of the region is full (route.pool_full).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/apps/{id}/routes/{route_id}/partners:
    put:
      tags:
      - apps
      operationId: setRoutePartners
      summary: Replace the route's whole partner list. The node's firewall and proxy take the new list from one push.
      description: 1 to 256 IPv4 addresses or CIDRs. A repeat is dropped; a CIDR inside another is refused. 0.0.0.0/0 admits every source and stands alone; it, or any CIDR wider than a /8, needs allow_any, and the answer then carries warning open_to_all. A refused list answers 422 with every problem in error.problems, such as route.partners_too_many and route.partner_invalid, and changes nothing. The change is audited as route:partners with the before and after lists.
      x-fm-action: route:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: route_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetRoutePartnersBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoutePartners'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Refused. error.code is the first problem and error.problems lists them all.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnersRefusal'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/access/config:
    get:
      tags:
      - access
      operationId: getAccessConfig
      summary: What fm-access needs before it signs in. 404 access.disabled while access is off.
      x-fm-action: none
      security: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessConfig'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/access/authorizations:
    post:
      tags:
      - access
      operationId: createAccessAuthorization
      summary: Approve one fm-access sign-in for the signed-in user. Returns a one-time code valid for 120 seconds.
      description: The caller must be a user signed in to org_id and a member of it. redirect_uri is http://127.0.0.1:<port>/callback or http://[::1]:<port>/callback (access.redirect_uri_invalid), and the challenge is S256 (access.pkce_method). Only the code's sha256 is stored.
      x-fm-action: none
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessAuthorizeBody'
      responses:
        '201':
          description: Created.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessAuthorization'
        '400':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/access/token:
    post:
      tags:
      - access
      operationId: accessToken
      summary: Exchange a sign-in code for a session, or refresh a session's credential. Metered at 30 a minute per address and 60 a minute per session (access.rate_limited).
      description: grant_type authorization_code takes code, code_verifier, redirect_uri, and device_name, and returns a refresh_token once. grant_type refresh_token takes refresh_token and returns a new credential with the user's current teams. The credential lives an hour at most and never past the session's end.
      x-fm-action: none
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessTokenBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessToken'
        '400':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/access/routes:
    get:
      tags:
      - access
      operationId: listAccessRoutes
      summary: The access routes the credential's user reaches in its org, directly or through a team.
      description: Each endpoint is one placement of the route, its IP by its public address on 443 with the access SNI. A credential that does not verify, or whose session was revoked or ended, answers 401 access.credential_invalid.
      x-fm-action: none
      security:
      - access: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRouteList'
        '401':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/access/logout:
    post:
      tags:
      - access
      operationId: accessLogout
      summary: End the session the refresh token names and revoke it on nodes. An unknown token answers 204 too.
      x-fm-action: none
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessLogoutBody'
      responses:
        '204':
          description: Signed out.
        '400':
          description: Refused. error.code names why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{org_id}/access/sessions:
    get:
      tags:
      - access
      operationId: listAccessSessions
      summary: The org's live access sessions. No token is shown, only each refresh token's prefix.
      x-fm-action: access:manage
      security:
      - bearer: []
      parameters:
      - name: org_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessSessionList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{org_id}/access/sessions/{session_id}/revoke:
    post:
      tags:
      - access
      operationId: revokeAccessSession
      summary: End one access session. Nodes close its live connections within seconds. Audited as access:session_revoke.
      x-fm-action: access:manage
      security:
      - bearer: []
      parameters:
      - name: org_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: session_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRevoked'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{org_id}/access/users/{user}/revoke:
    post:
      tags:
      - access
      operationId: revokeAccessUser
      summary: End every access session of a member and revoke every credential they hold. user is a user id or an email. Audited as access:user_revoke.
      x-fm-action: access:manage
      security:
      - bearer: []
      parameters:
      - name: org_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: user
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessRevoked'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/routes/{route_id}/access:
    get:
      tags:
      - ips
      operationId: getRouteAccess
      summary: The users and teams an access route admits.
      description: route_id is the route's id or the access route's name. A partner route answers 409 access.route_not_access, and a route of another organization 404 access.route_not_found.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: route_id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessPolicy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    put:
      tags:
      - ips
      operationId: setRouteAccess
      summary: Replace an access route's whole policy. Nodes take the change in their next access push.
      description: A user who is not a member of the route's organization, or a team of another organization, answers 422 access.grant_principal_unknown and changes nothing. More than 100 principals answers 422 access.grant_limit. The change is audited as access:grant with the added and removed ids.
      x-fm-action: access:grant
      security:
      - bearer: []
      parameters:
      - name: route_id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetAccessPolicyBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessPolicy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Refused. error.code is access.grant_principal_unknown or access.grant_limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/regions:
    get:
      tags:
      - ips
      operationId: listRegions
      summary: The regions IPs can be created in.
      x-fm-action: org:read
      security:
      - bearer: []
      - internal: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RegionView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials:
    get:
      tags:
      - credentials
      operationId: listCredentials
      summary: The app's proxy credentials. Prefixes only.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CredentialView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - credentials
      operationId: createCredential
      summary: Create a proxy credential. The answer holds the token once.
      x-fm-action: token:rotate
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCredentialBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedCredential'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials/{cred}/rotate:
    post:
      tags:
      - credentials
      operationId: rotateCredential
      summary: Replace a credential's token. The old one works until its grace ends.
      x-fm-action: token:rotate
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotatedCredential'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials/{cred}/policy:
    get:
      tags:
      - policy
      operationId: listCredentialPolicy
      summary: The credential's destination allowlist. No rules means every destination the node allows.
      x-fm-action: app:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - policy
      operationId: addCredentialPolicyRule
      summary: Add one allowlist rule. Nodes enforce it within seconds. Needs a plan with egress_firewall, else 402 plan.feature_required; existing rules stay in force and can still be deleted and tested without it.
      x-fm-action: policy:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyRuleBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials/{cred}/policy/test:
    post:
      tags:
      - policy
      operationId: testCredentialPolicy
      summary: Whether the credential's rules allow a destination, and by which rule.
      x-fm-action: app:read
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyTestBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyDecision'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials/{cred}/policy/{rule}:
    delete:
      tags:
      - policy
      operationId: deleteCredentialPolicyRule
      summary: Delete one allowlist rule. Deleting the last allows every destination again.
      x-fm-action: policy:edit
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: rule
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/credentials/{cred}:
    delete:
      tags:
      - credentials
      operationId: revokeCredential
      summary: Revoke a credential.
      x-fm-action: token:rotate
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: cred
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/jobs/{id}/events:
    get:
      tags:
      - ips
      operationId: getJobEvents
      summary: The provisioning job's steps as server-sent events.
      description: |
        Takes a session or an org API key, which needs `app:read` in the
        job's org, or the FM_INTERNAL_TOKEN bearer as the host. The stream
        sends one `steps` event with the step names, one `step` event per
        provisioning_events row, and a `done` event with the end state.
        Last-Event-ID resumes after an event id.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        example: 3f0c9d2a-5b7e-4e61-8a4d-1c2b3a4d5e6f
      - name: Last-Event-ID
        in: header
        required: false
        schema:
          type: string
      responses:
        '200':
          description: The event stream.
          content:
            text/event-stream:
              schema:
                type: string
              example: |
                event: steps
                data: {"steps":["validating","ensuring_network"]}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: app:read
      security:
      - bearer: []
      - internal: []
  /v1/jobs/{id}:
    get:
      tags:
      - jobs
      operationId: getJob
      summary: A provisioning job's state.
      x-fm-action: app:read
      security:
      - bearer: []
      - internal: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/apps/{id}/logs:
    get:
      tags:
      - logs
      operationId: getAppLogs
      summary: One page of the app's connection logs, newest first.
      description: |
        One row per closed connection through the app's IPs, inside the window
        the org's plan keeps (`log_retention_days`: 1 day on trial and
        Starter, 7 on Pro, 30 on Business). A row holds who connected, where
        to, and how it ended; no payload, header, path, or token is ever
        stored. The caller needs `logs:read` on the app, through its org
        role, a direct app grant, or a team grant. An app the caller cannot
        reach answers 404, as an unknown app does, and the deny is audited.
        Pass `next_cursor` back as `cursor` with the same filters for the
        next page. An unknown or repeated query key is refused.
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: since
        in: query
        required: false
        description: RFC 3339, inclusive. Raised to the oldest time the plan keeps.
        schema:
          type: string
          format: date-time
        example: '2026-10-24T00:00:00Z'
      - name: until
        in: query
        required: false
        description: RFC 3339, exclusive. Defaults to an hour past now.
        schema:
          type: string
          format: date-time
      - name: direction
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/Direction'
      - name: dest
        in: query
        required: false
        description: A destination host, host:port, or `*.suffix`, matched without case.
        schema:
          type: string
          maxLength: 255
        example: '*.example.com:443'
      - name: result
        in: query
        required: false
        description: How the connection ended, such as `ok`, `denied_by_policy`, or `probe` for fm's startup check.
        schema:
          type: string
          pattern: ^[a-z0-9_]{1,64}$
        example: denied_by_policy
      - name: egress_ip
        in: query
        required: false
        description: The app's FixedMark IP the connection left from.
        schema:
          type: string
        example: 203.0.113.7
      - name: peer
        in: query
        required: false
        description: The client address, or a CIDR of them with no host bits set. For an inbound row it is the partner.
        schema:
          type: string
        example: 198.51.100.9/32
      - name: listener_port
        in: query
        required: false
        description: The port the connection reached on the FixedMark IP, such as 20000 for an inbound TCP route.
        schema:
          type: integer
          minimum: 1
          maximum: 65535
      - name: route_id
        in: query
        required: false
        description: One inbound route's rows. `filters.routes` lists the app's routes.
        schema:
          type: string
          format: uuid
      - name: user_id
        in: query
        required: false
        description: One user's access rows.
        schema:
          type: string
          format: uuid
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
      - name: cursor
        in: query
        required: false
        description: The `next_cursor` of the page before.
        schema:
          type: string
          maxLength: 64
      responses:
        '200':
          description: The page. `next_cursor` is null on the last one.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionLogPage'
              example:
                rows:
                - id: 41
                  at: '2026-10-24T12:00:01.123Z'
                  direction: egress
                  peer_ip: 192.0.2.44
                  listener: http_forward
                  listener_port: 8080
                  dest_host: api.example.com
                  dest_port: 443
                  sni: api.example.com
                  bytes_up: 517
                  bytes_down: 4096
                  duration_ms: 41
                  result: ok
                  reason: null
                  egress_ip: 203.0.113.7
                  credential_id: 9b1f6c2e-3d4a-4e5b-8c7d-1a2b3c4d5e6f
                  route_id: null
                next_cursor: MTc5MjgzNTEyNTcwMTEyMy40MQ
                retention_days: 7
                oldest: '2026-10-17T12:00:00Z'
                filters:
                  directions:
                  - value: egress
                    label: Outbound
                  - value: ingress
                    label: Inbound
                  routes:
                  - id: 5f0c2b1e-7a3d-4e9b-8c21-3d4e5f6a7b8c
                    label: 203.0.113.7:20000
                    kind: tcp
                    port: 20000
                    deleted: false
        '400':
          description: The query is refused with `logs.bad_query`; message says which parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: logs:read
      security:
      - bearer: []
  /v1/apps/{id}/logs/stream:
    get:
      tags:
      - logs
      operationId: streamAppLogs
      summary: The app's new connection log rows as server-sent events.
      description: |
        A live tail. The stream sends one `ready` event, then one `row` event
        per new row the `direction`, `dest`, `result`, `egress_ip`, `peer`,
        `listener_port`, and `route_id` filters match. Each event id is a point at or below which the client
        already has every row, so Last-Event-ID resumes there and misses no
        row; a row can come again, so skip a row id already seen. A row can
        arrive after one with a higher id. Access is checked again every minute, and the
        stream ends after 10 minutes; reconnect with Last-Event-ID. The
        caller needs `logs:read` on the app. An org holds at most 10 open
        tails; past that the answer is 503 `logs.busy`.
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: direction
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/Direction'
      - name: dest
        in: query
        required: false
        schema:
          type: string
          maxLength: 255
      - name: result
        in: query
        required: false
        schema:
          type: string
          pattern: ^[a-z0-9_]{1,64}$
      - name: egress_ip
        in: query
        required: false
        schema:
          type: string
      - name: peer
        in: query
        required: false
        schema:
          type: string
      - name: listener_port
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 65535
      - name: route_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
      - name: user_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
      - name: Last-Event-ID
        in: header
        required: false
        schema:
          type: string
      responses:
        '200':
          description: The event stream.
          content:
            text/event-stream:
              schema:
                type: string
              example: |
                event: ready
                data: {"after":40,"retention_days":7}

                id: 41
                event: row
                data: {"id":41,"at":"2026-10-24T12:00:01.123Z","direction":"egress","peer_ip":"192.0.2.44","listener_port":8080,"result":"ok"}
        '400':
          description: The query or Last-Event-ID is refused with `logs.bad_query`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          description: Too many tails are open, `logs.busy`. Retry shortly.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-fm-action: logs:read
      security:
      - bearer: []
  /v1/apps/{id}/usage:
    get:
      tags:
      - usage
      operationId: getAppUsage
      summary: The app's usage by bucket and direction.
      description: |
        Sums the app's usage_hourly rows, its routes' bytes on shared IPs
        included, one row per bucket per direction that has bytes, in bucket
        then direction order. A row counts when its hour is in [from, to).
        Buckets are UTC hours or UTC days. direction narrows the rows to one
        direction; the default, all, answers both. The caller needs `logs:read` on the app's org. An app
        in another org answers 404, as an unknown app does. Hours less than
        three hours old may still grow; the ETag changes when they do.
        GET /v1/apps/{id}/usage/series answers the same rows as one series
        per direction with the dashboard's labels.
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: from
        in: query
        required: true
        description: RFC 3339, inclusive.
        schema:
          type: string
          format: date-time
        example: '2026-10-01T00:00:00Z'
      - name: to
        in: query
        required: true
        description: RFC 3339, exclusive. At most 92 days after from.
        schema:
          type: string
          format: date-time
        example: '2026-10-02T00:00:00Z'
      - name: granularity
        in: query
        required: false
        schema:
          type: string
          enum:
          - hour
          - day
          default: hour
        example: hour
      - name: direction
        in: query
        required: false
        schema:
          type: string
          enum:
          - egress
          - ingress
          - all
          default: all
        example: all
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          description: The rows. An empty array when the window has none.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Vary:
              $ref: '#/components/headers/Vary'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UsageRow'
              examples:
                hourly:
                  summary: Two hours of egress and one of ingress
                  value:
                  - bucket: '2026-10-01T00:00:00Z'
                    direction: egress
                    bytes_up: 2001
                    bytes_down: 4001
                    conns: 2
                    reqs: 4
                  - bucket: '2026-10-01T01:00:00Z'
                    direction: egress
                    bytes_up: 4001
                    bytes_down: 8001
                    conns: 4
                    reqs: 8
                  - bucket: '2026-10-01T01:00:00Z'
                    direction: ingress
                    bytes_up: 70
                    bytes_down: 80
                    conns: 1
                    reqs: 0
        '304':
          $ref: '#/components/responses/NotModified'
        '400':
          description: The query is refused. A window wider than 92 days names max_days.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: logs:read
      security:
      - bearer: []
      - internal: []
  /v1/apps/{id}/usage/series:
    get:
      tags:
      - usage
      operationId: getAppUsageSeries
      summary: The app's usage by bucket, one series per direction.
      description: |
        Sums the app's usage_hourly rows, its routes' bytes on shared IPs
        included, and answers one series per direction, egress first, each
        with the label the dashboard shows (Outbound, Inbound) and its rows
        in bucket order.
        With direction=all, the default, a direction with no rows has no
        series; with egress or ingress, that one series is always there. A
        row counts when its hour is in [from, to). Buckets are UTC hours or
        UTC days. The caller needs `logs:read` on the app's org. An app in
        another org answers 404, as an unknown app does. Hours less than
        three hours old may still grow; the ETag changes when they do.
        GET /v1/apps/{id}/usage answers the same rows as one flat list.
      parameters:
      - $ref: '#/components/parameters/AppID'
      - name: from
        in: query
        required: true
        description: RFC 3339, inclusive.
        schema:
          type: string
          format: date-time
        example: '2026-10-01T00:00:00Z'
      - name: to
        in: query
        required: true
        description: RFC 3339, exclusive. At most 92 days after from.
        schema:
          type: string
          format: date-time
        example: '2026-10-02T00:00:00Z'
      - name: granularity
        in: query
        required: false
        schema:
          type: string
          enum:
          - hour
          - day
          default: hour
        example: hour
      - name: direction
        in: query
        required: false
        schema:
          type: string
          enum:
          - egress
          - ingress
          - all
          default: all
        example: all
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          description: The series. An empty list when the window has no rows.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Vary:
              $ref: '#/components/headers/Vary'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageView'
              examples:
                hourly:
                  summary: Two hours of egress and one of ingress
                  value:
                    direction: all
                    granularity: hour
                    series:
                    - direction: egress
                      label: Outbound
                      rows:
                      - bucket: '2026-10-01T00:00:00Z'
                        direction: egress
                        bytes_up: 2001
                        bytes_down: 4001
                        conns: 2
                        reqs: 4
                      - bucket: '2026-10-01T01:00:00Z'
                        direction: egress
                        bytes_up: 4001
                        bytes_down: 8001
                        conns: 4
                        reqs: 8
                    - direction: ingress
                      label: Inbound
                      rows:
                      - bucket: '2026-10-01T01:00:00Z'
                        direction: ingress
                        bytes_up: 70
                        bytes_down: 80
                        conns: 1
                        reqs: 0
        '304':
          $ref: '#/components/responses/NotModified'
        '400':
          description: The query is refused. A window wider than 92 days names max_days.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: logs:read
      security:
      - bearer: []
      - internal: []
  /v1/orgs/{id}/usage/period:
    get:
      tags:
      - usage
      operationId: getOrgUsagePeriod
      summary: The org's current billing period, one object per product.
      description: |
        The latest usage_period of each product that started on or before
        today in UTC: egress, and ingress once the plan holds an inbound
        allowance. The caller needs
        `logs:read` in the org; another org answers 404.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        example: 0b7e0c0e-1d2a-4c3b-9a5e-6f1a2b3c4d5e
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          description: The periods. An empty array before the first period starts.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/CacheControl'
            Vary:
              $ref: '#/components/headers/Vary'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Period'
              examples:
                throttled:
                  value:
                  - product: egress
                    label: Outbound
                    period_start: '2026-10-01'
                    bytes_total: 1050000000000
                    allowance_bytes: 1000000000000
                    percent: 105
                    state: throttled
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: logs:read
      security:
      - bearer: []
      - internal: []
  /v1/plans:
    get:
      tags:
      - plans
      operationId: listPlans
      summary: The active plans with every quota and feature, priced on the caller's channel and currency.
      description: |
        Answers without a token on the default channel, as the pricing page
        calls it. With a token the org's own channel and currency price the
        plans, and an org on a closed plan also sees its plan with
        `status: closed` and its successor.
        Each plan lists the active add-ons it may buy with the price of one
        unit, and the ingress packs it sells with their one-time price. An
        add-on that needs a feature, such as `inbound_ip`, is listed only on
        plans with that feature.
      security:
      - {}
      - bearer: []
      responses:
        '200':
          description: The plans, cheapest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanList'
              example:
                channel: paddle
                currency: USD
                plans:
                - id: pro
                  name: Pro
                  status: active
                  version: 1
                  features:
                    dedicated_ips: true
                    ip_pair: true
                    log_retention_days: 7
                    webhooks: true
                  quotas:
                    bytes: 107374182400
                    ips: 2
                    regions: 1
                    concurrent_connections: 500
                    connections_per_second: 100
                  packs_allowed: true
                  price:
                    currency: USD
                    amount_minor: 2900
                    interval: month
                  current: false
                  addons:
                  - id: inbound_ip
                    unit: ip_month
                    price:
                      currency: USD
                      amount_minor: 900
                      interval: month
                  packs_ingress:
                  - id: pro-ingress-100gb
                    bytes: 107374182400
                    price:
                      currency: USD
                      amount_minor: 1000
                      interval: once
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/Internal'
      x-fm-action: org:read
  /v1/orgs/{id}/webhooks:
    get:
      tags:
      - webhooks
      operationId: listWebhooks
      summary: The org's webhook endpoints.
      x-fm-action: logs:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Endpoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - webhooks
      operationId: createWebhook
      summary: Add a webhook endpoint. The answer holds the signing secret once. Needs a plan with webhooks, else 402 plan.feature_required.
      x-fm-action: org:manage
      x-fm-ratelimit: create
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedWebhookReply'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/webhooks/{endpoint}:
    patch:
      tags:
      - webhooks
      operationId: updateWebhook
      summary: Change an endpoint's URL, events, or enabled flag. Without the webhooks feature only {"enabled":false} is taken; any other change is 402 plan.feature_required.
      x-fm-action: org:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: endpoint
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Endpoint'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    delete:
      tags:
      - webhooks
      operationId: deleteWebhook
      summary: Delete an endpoint.
      x-fm-action: org:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: endpoint
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '204':
          description: Done. No body.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/webhooks/{endpoint}/deliveries:
    get:
      tags:
      - webhooks
      operationId: listWebhookDeliveries
      summary: An endpoint's recent deliveries and their attempts.
      x-fm-action: logs:read
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: endpoint
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeliveryView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/orgs/{id}/webhooks/{endpoint}/deliveries/{delivery}/replay:
    post:
      tags:
      - webhooks
      operationId: replayWebhookDelivery
      summary: Send a delivery again. Needs a plan with webhooks, else 402 plan.feature_required.
      x-fm-action: org:manage
      security:
      - bearer: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: endpoint
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: delivery
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReplayReply'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. The plan does not include the feature the error names in feature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/webhooks/events:
    get:
      tags:
      - webhooks
      operationId: listWebhookEvents
      summary: The event names an endpoint can subscribe to.
      x-fm-action: none
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing:
    get:
      tags:
      - billing
      operationId: getBilling
      summary: The org's plan, subscription, quotas, and usage this period.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/channel:
    get:
      tags:
      - billing
      operationId: getBillingChannel
      summary: The payment channels the org may use and the one it uses.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    put:
      tags:
      - billing
      operationId: setBillingChannel
      summary: Choose the payment channel before the first subscription.
      x-fm-action: billing:manage
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/checkout:
    post:
      tags:
      - billing
      operationId: createCheckout
      summary: Start a checkout for a plan.
      x-fm-action: billing:manage
      x-fm-ratelimit: create
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/billing/manage:
    get:
      tags:
      - billing
      operationId: getBillingPortal
      summary: A link to the payment provider's customer portal.
      x-fm-action: billing:manage
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required:
                - url
                properties:
                  url:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/billing/gst:
    get:
      tags:
      - billing
      operationId: getGST
      summary: The org's GST details.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GSTView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    put:
      tags:
      - billing
      operationId: setGST
      summary: Set the org's GSTIN and legal name.
      x-fm-action: billing:manage
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GSTView'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GSTView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/invoices:
    get:
      tags:
      - billing
      operationId: listInvoices
      summary: The org's invoices.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required:
                - invoices
                properties:
                  invoices:
                    type: array
                    items:
                      $ref: '#/components/schemas/InvoiceView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/invoices/{invoice}/pdf:
    get:
      tags:
      - billing
      operationId: getInvoicePDF
      summary: An invoice as PDF.
      x-fm-action: org:read
      security:
      - bearer: []
      parameters:
      - name: invoice
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The PDF.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/packs:
    get:
      tags:
      - billing
      operationId: listPacks
      summary: Bandwidth packs on the org's plan, the ones applied this period, and auto purchase.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PacksView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
    post:
      tags:
      - billing
      operationId: buyPack
      summary: Buy a bandwidth pack.
      x-fm-action: billing:manage
      x-fm-ratelimit: create
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PackBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PackCheckoutView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/billing/auto-pack:
    patch:
      tags:
      - billing
      operationId: setAutoPack
      summary: Turn automatic pack purchase on or off.
      x-fm-action: billing:manage
      security:
      - bearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoPackBody'
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required:
                - auto_pack
                properties:
                  auto_pack:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/addons:
    get:
      tags:
      - billing
      operationId: listAddons
      summary: The add-ons on the org's plan and channel.
      x-fm-action: billing:manage
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/billing/addons/preview:
    get:
      tags:
      - billing
      operationId: previewAddons
      summary: What the next invoice charges for add-ons.
      x-fm-action: billing:manage
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/vercel/installation:
    get:
      tags:
      - vercel
      operationId: getVercelInstallation
      summary: The org's Vercel installation and linked projects.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstallationView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
  /v1/vercel/installation/projects:
    get:
      tags:
      - vercel
      operationId: listVercelProjects
      summary: The Vercel projects the installation can see.
      x-fm-action: org:read
      security:
      - bearer: []
      responses:
        '200':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required:
                - projects
                properties:
                  projects:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/vercel/installation/projects/{project}:
    put:
      tags:
      - vercel
      operationId: linkVercelProject
      summary: Link a Vercel project to an app and write its proxy env vars.
      x-fm-action: token:rotate
      security:
      - bearer: []
      parameters:
      - name: project
        in: path
        required: true
        schema:
          type: string
        description: The Vercel project id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LinkBody'
      responses:
        '201':
          description: OK.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
      - vercel
      operationId: unlinkVercelProject
      summary: Unlink a Vercel project and remove its env vars.
      x-fm-action: token:rotate
      security:
      - bearer: []
      parameters:
      - name: project
        in: path
        required: true
        schema:
          type: string
        description: The Vercel project id.
      responses:
        '204':
          description: Done. No body.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestID'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '502':
          description: Refused. code says why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: An API key (fm_ plus an 8 character lookup prefix plus the secret) or a dashboard session token.
    internal:
      type: http
      scheme: bearer
      description: The operator's token on the control-plane host only, FM_INTERNAL_TOKEN on /internal and FM_STAFF_HOST_TOKEN on /v1/admin.
    connector:
      type: http
      scheme: bearer
      description: A connector token, fmc_ and 52 lowercase base32 characters, as fixedmarkd holds it.
    access:
      type: http
      scheme: bearer
      description: An access credential, fma1. and the signed claims, as fm-access holds it.
  parameters:
    AppID:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      example: 7d3c5a2e-8f41-4b6a-9c0d-2e5f7a1b3c9d
    IfNoneMatch:
      name: If-None-Match
      in: header
      required: false
      description: An ETag from an earlier answer. A match answers 304.
      schema:
        type: string
  headers:
    ETag:
      description: A strong validator over the response body.
      schema:
        type: string
      example: '"5f2b9c0e7d1a4b3c8e6f0a9d2c4b7e1f"'
    CacheControl:
      schema:
        type: string
        enum:
        - private, max-age=60
    Vary:
      description: The answer depends on the caller's key, so a cache keys it by Authorization.
      schema:
        type: string
        enum:
        - Authorization
    RetryAfter:
      description: Seconds until the bucket has a token again.
      schema:
        type: integer
        minimum: 1
    RateLimitLimit:
      description: Requests a minute the route's bucket allows this key.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the route's bucket now.
      schema:
        type: integer
        minimum: 0
    RequestID:
      description: The request id, also in the error body and in every log line of the request.
      schema:
        type: string
  responses:
    BadRequest:
      description: The request is not valid. code names the field or rule; bad_request is a body that does not parse.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: No token, a wrong token, or a revoked or expired key. Never a hint whether the key exists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The caller lacks the route's action, through its org role, a direct app grant, or a team grant, or the key lacks the scope. The deny is written to audit_log.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Nothing with that id exists for this caller. Another org's resource and an app the caller cannot see answer the same 404.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: The request conflicts with the current state. code says which rule.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: 'The key spent its bucket: 600 requests a minute, and 60 a minute on create routes. Retry after Retry-After seconds.'
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Internal:
      description: Something failed on our side. The request_id is in the log line that says what.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unavailable:
      description: The control plane is read-only, or a dependency is not configured.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotModified:
      description: The If-None-Match tag matches. No body.
      headers:
        ETag:
          $ref: '#/components/headers/ETag'
        Cache-Control:
          $ref: '#/components/headers/CacheControl'
        Vary:
          $ref: '#/components/headers/Vary'
    Gone:
      description: The invitation was already accepted, revoked, or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    APIKeyView:
      type: object
      required:
      - id
      - prefix
      - name
      - scopes
      - created_by
      - created_at
      - last_used_at
      - expires_at
      - revoked_at
      - status
      - app_id
      properties:
        id:
          type: string
          format: uuid
        prefix:
          type: string
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        created_by:
          type:
          - string
          - 'null'
          format: uuid
        created_by_email:
          type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type:
          - string
          - 'null'
          format: date-time
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        status:
          type: string
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_name:
          type: string
    AppDeleted:
      type: object
      required:
      - id
      - status
      - removed_grants
      - revoked_keys
      - revoked_credentials
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        removed_grants:
          type: integer
        revoked_keys:
          type: integer
        revoked_credentials:
          type: integer
    AppDetail:
      type: object
      required:
      - id
      - name
      - status
      - grace_ends_at
      - ips
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
        grace_ends_at:
          type:
          - string
          - 'null'
          format: date-time
        ips:
          type: array
          items:
            $ref: '#/components/schemas/AppIPDetail'
    AppGrantView:
      type: object
      required:
      - app_id
      - user_id
      - app_role
      - from
      properties:
        app_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        app_role:
          type: string
        from:
          type:
          - string
          - 'null'
    AppIPDetail:
      type: object
      required:
      - id
      - address
      - region
      - provider
      - provider_name
      - city
      - node_id
      - status
      - status_changed_at
      - grace_ends_at
      - retire_after
      properties:
        id:
          type: string
        address:
          type: string
        region:
          type: string
        provider:
          type: string
        provider_name:
          type: string
        city:
          type: string
        node_id:
          type:
          - string
          - 'null'
        status:
          type: string
        status_changed_at:
          type: string
          format: date-time
        grace_ends_at:
          type:
          - string
          - 'null'
          format: date-time
        retire_after:
          type:
          - string
          - 'null'
          format: date-time
    AppMemberView:
      type: object
      required:
      - user_id
      - email
      - name
      - org_role
      - app_role
      - team_role
      - teams
      - source
      - implicit
      - added_by
      - granted_at
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
        name:
          type: string
        org_role:
          type: string
        app_role:
          type:
          - string
          - 'null'
        team_role:
          type:
          - string
          - 'null'
        teams:
          type: array
          items:
            type: string
        source:
          type: string
        implicit:
          type: boolean
        added_by:
          type:
          - string
          - 'null'
          format: uuid
        granted_at:
          type:
          - string
          - 'null'
          format: date-time
    AppMembersView:
      type: object
      required:
      - app_id
      - app_name
      - can_manage
      - members
      - teams
      properties:
        app_id:
          type: string
          format: uuid
        app_name:
          type: string
        can_manage:
          type: boolean
        members:
          type: array
          items:
            $ref: '#/components/schemas/AppMemberView'
        teams:
          type: array
          items:
            $ref: '#/components/schemas/AppTeamView'
    AppRoleBody:
      type: object
      required:
      - role
      properties:
        role:
          type: string
        kind:
          type: string
    AppSummary:
      type: object
      required:
      - id
      - name
      - status
      - ips
      - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        status:
          type: string
        ips:
          type: integer
        created_at:
          type: string
          format: date-time
    AppTeamView:
      type: object
      required:
      - team_id
      - name
      - slug
      - app_role
      - members
      - managed
      - source
      - granted_at
      properties:
        team_id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        app_role:
          type: string
        members:
          type: integer
        managed:
          type: boolean
        source:
          type: string
        granted_at:
          type: string
          format: date-time
    AppView:
      type: object
      required:
      - id
      - org_id
      - name
      - status
      - created_at
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        status:
          type: string
        created_at:
          type: string
          format: date-time
    AppliedPackView:
      type: object
      required:
      - id
      - pack_id
      - product
      - bytes
      - price_minor
      - currency
      - source
      - applied_at
      properties:
        id:
          type: string
          format: uuid
        pack_id:
          type: string
        product:
          type: string
        bytes:
          type: integer
        price_minor:
          type: integer
        currency:
          type: string
        source:
          type: string
        applied_at:
          type: string
          format: date-time
    AttemptView:
      type: object
      required:
      - attempt
      - status
      - duration_ms
      - error
      - response
      - at
      properties:
        attempt:
          type: integer
        status:
          type:
          - integer
          - 'null'
        duration_ms:
          type: integer
        error:
          type:
          - string
          - 'null'
          description: Why no 2xx came back, such as timeout or tls. plan_feature_required is a delivery dropped unsent because the org's plan no longer includes webhooks.
        response:
          type:
          - string
          - 'null'
        at:
          type: string
          format: date-time
    AuditPage:
      type: object
      required:
      - items
      - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AuditView'
        next_cursor:
          type:
          - string
          - 'null'
    AuditView:
      type: object
      required:
      - id
      - at
      - principal
      - principal_label
      - action
      - resource
      - resource_label
      - result
      - detail
      properties:
        id:
          type: integer
        at:
          type: string
          format: date-time
        principal:
          type: string
        principal_label:
          type: string
        action:
          type: string
        resource:
          type: string
        resource_label:
          type: string
        result:
          type: string
        detail:
          description: Free-form JSON.
    AutoPackBody:
      type: object
      required:
      - enabled
      properties:
        enabled:
          type:
          - boolean
          - 'null'
    BillingView:
      type: object
      required:
      - plan
      - status
      - quotas
      - dedicated
      - regions_included
      - can_manage
      - plans
      - features
      properties:
        plan:
          oneOf:
          - $ref: '#/components/schemas/PlanView'
          - type: 'null'
        status:
          type: string
        channel:
          type: string
        period_end:
          type:
          - string
          - 'null'
          format: date-time
        trial_ends_at:
          type:
          - string
          - 'null'
          format: date-time
        ends_at:
          type:
          - string
          - 'null'
          format: date-time
        quotas:
          type: object
          additionalProperties:
            type: integer
        dedicated:
          type: boolean
        regions_included:
          type: integer
        can_manage:
          type: boolean
        plans:
          type: array
          items:
            $ref: '#/components/schemas/PlanView'
        features:
          description: Free-form JSON.
    Capabilities:
      type: object
      required:
      - display_name
      - checkout
      - proration
      - partial_refunds
      - discount_codes
      - metered_usage
      - upi_mandates
      - merchant_of_record
      - hosted_portal
      - tax_handling
      - managed_dunning
      - marketplace
      - addon_charges
      - currencies
      properties:
        display_name:
          type: string
        checkout:
          type: boolean
        proration:
          type: boolean
        partial_refunds:
          type: boolean
        discount_codes:
          type: boolean
        metered_usage:
          type: boolean
        upi_mandates:
          type: boolean
        merchant_of_record:
          type: boolean
        hosted_portal:
          type: boolean
        tax_handling:
          type: boolean
        managed_dunning:
          type: boolean
        marketplace:
          type: boolean
        addon_charges:
          type: boolean
        currencies:
          type:
          - array
          - 'null'
          items:
            type: string
        client:
          oneOf:
          - $ref: '#/components/schemas/ClientScript'
          - type: 'null'
    CatalogPlan:
      type: object
      required:
      - id
      - name
      - status
      - version
      - features
      - quotas
      - packs_allowed
      - price
      - current
      - addons
      - packs_ingress
      properties:
        id:
          type: string
        name:
          type: string
        status:
          $ref: '#/components/schemas/PlanStatus'
        version:
          type: integer
        features:
          $ref: '#/components/schemas/Features'
        quotas:
          $ref: '#/components/schemas/Quotas'
        packs_allowed:
          type: boolean
        price:
          oneOf:
          - $ref: '#/components/schemas/CatalogPrice'
          - type: 'null'
        current:
          type: boolean
        successor:
          type: string
        addons:
          type: array
          items:
            $ref: '#/components/schemas/CatalogAddon'
        packs_ingress:
          type: array
          items:
            $ref: '#/components/schemas/CatalogPack'
    CatalogAddon:
      type: object
      description: An add-on the plan may buy and the price of one unit. An `ip_month` is one IP for 730 hours, billed by the hour.
      required:
      - id
      - unit
      - price
      properties:
        id:
          type: string
        unit:
          type: string
        price:
          $ref: '#/components/schemas/CatalogPrice'
    CatalogPack:
      type: object
      description: An ingress pack the plan sells, the bytes it adds to one period's inbound allowance, and its one-time price, null where the channel has none.
      required:
      - id
      - bytes
      - price
      properties:
        id:
          type: string
        bytes:
          type: integer
          format: int64
        price:
          oneOf:
          - $ref: '#/components/schemas/CatalogPrice'
          - type: 'null'
    CatalogPrice:
      type: object
      required:
      - currency
      - amount_minor
      - interval
      properties:
        currency:
          type: string
        amount_minor:
          type: integer
          format: int64
        interval:
          type: string
          enum:
          - month
          - year
          - once
    ChannelBody:
      type: object
      required:
      - channel
      properties:
        channel:
          type: string
    ChannelView:
      type: object
      required:
      - id
      - capabilities
      properties:
        id:
          type: string
        currency:
          type: string
        capabilities:
          $ref: '#/components/schemas/Capabilities'
    ChannelsView:
      type: object
      required:
      - current
      - checkout
      - options
      properties:
        current:
          oneOf:
          - $ref: '#/components/schemas/ChannelView'
          - type: 'null'
        checkout:
          oneOf:
          - $ref: '#/components/schemas/ChannelView'
          - type: 'null'
        options:
          type: array
          items:
            $ref: '#/components/schemas/ChannelView'
        reason:
          type: string
    CheckoutBody:
      type: object
      required:
      - plan_id
      - channel
      - kind
      - discount_code
      properties:
        plan_id:
          type: string
        channel:
          type: string
        kind:
          type: string
        discount_code:
          type: string
    CheckoutView:
      type: object
      required:
      - changed
      - plan_id
      - channel
      properties:
        url:
          type: string
        changed:
          type: boolean
        plan_id:
          type: string
        channel:
          type: string
    ClientScript:
      type: object
      required:
      - script
      - token
      - environment
      properties:
        script:
          type: string
        token:
          type: string
        environment:
          type: string
    Confirm:
      type: object
      required:
      - sso_amount_minor
      - sso_user_amount_minor
      - currency
      properties:
        sso_amount_minor:
          type:
          - integer
          - 'null'
        sso_user_amount_minor:
          type:
          - integer
          - 'null'
        currency:
          type: string
    ConnectionLogRow:
      type: object
      additionalProperties: false
      required:
      - id
      - at
      - direction
      - peer_ip
      - listener
      - listener_port
      - dest_host
      - dest_port
      - sni
      - bytes_up
      - bytes_down
      - duration_ms
      - result
      - reason
      - egress_ip
      - credential_id
      - route_id
      properties:
        id:
          type: integer
          format: int64
        at:
          type: string
          format: date-time
          description: When the connection closed.
        direction:
          $ref: '#/components/schemas/Direction'
        peer_ip:
          type: string
          description: The client address the proxy saw.
        listener:
          type: string
          description: The listener kind, such as http_forward or socks5.
        listener_port:
          type: integer
          minimum: 1
          maximum: 65535
        dest_host:
          type:
          - string
          - 'null'
        dest_port:
          type:
          - integer
          - 'null'
        sni:
          type:
          - string
          - 'null'
        bytes_up:
          type: integer
          format: int64
          minimum: 0
          description: Bytes the FixedMark IP sent.
        bytes_down:
          type: integer
          format: int64
          minimum: 0
          description: Bytes the FixedMark IP received.
        duration_ms:
          type: integer
          format: int64
          minimum: 0
        result:
          type: string
        reason:
          type:
          - string
          - 'null'
        egress_ip:
          type:
          - string
          - 'null'
          description: The app's public FixedMark IP, never a bind address.
        credential_id:
          type:
          - string
          - 'null'
          description: The id of the credential the connection authenticated with. Never the token.
        route_id:
          type:
          - string
          - 'null'
          description: The inbound route an ingress connection reached. Null for egress.
        origin_kind:
          type:
          - string
          - 'null'
          enum:
          - host
          - connector
          - null
          description: Where an ingress connection went. Null for egress.
        connector_id:
          type:
          - string
          - 'null'
          description: The connector a connector origin's stream went through.
        access:
          type: object
          required:
          - user_id
          - email
          - session_id
          description: Set on an access connection whose credential verified. The email is read from the user's account when the row is read; it is never stored in the log.
          properties:
            user_id:
              type: string
              format: uuid
            email:
              type:
              - string
              - 'null'
            session_id:
              type:
              - string
              - 'null'
    ConnectionLogPage:
      type: object
      additionalProperties: false
      required:
      - rows
      - next_cursor
      - retention_days
      - oldest
      - filters
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/ConnectionLogRow'
        next_cursor:
          type:
          - string
          - 'null'
        retention_days:
          type: integer
          minimum: 0
        oldest:
          type: string
          format: date-time
          description: The oldest time the org's plan keeps.
        filters:
          $ref: '#/components/schemas/ConnectionLogFilters'
    ConnectionLogFilters:
      type: object
      additionalProperties: false
      required:
      - directions
      - routes
      description: The options the logs page's filters offer, so no client names a direction or a route of its own.
      properties:
        directions:
          type: array
          items:
            type: object
            required:
            - value
            - label
            properties:
              value:
                $ref: '#/components/schemas/Direction'
              label:
                type: string
        routes:
          type: array
          description: The app's live routes, and those deleted inside the window the plan keeps.
          items:
            type: object
            required:
            - id
            - label
            - kind
            - port
            - deleted
            properties:
              id:
                type: string
                format: uuid
              label:
                type: string
              kind:
                type: string
              port:
                type: integer
              deleted:
                type: boolean
    CreateAppBody:
      type: object
      required:
      - name
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
    CreateCredentialBody:
      type: object
      required:
      - label
      properties:
        label:
          type: string
    CreateIPBody:
      type: object
      required:
      - region
      properties:
        region:
          type: string
    CreateKeyBody:
      type: object
      required:
      - name
      - scopes
      properties:
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        app_id:
          type:
          - string
          - 'null'
          format: uuid
    CreateOrgBody:
      type: object
      required:
      - name
      - country
      properties:
        name:
          type: string
        country:
          type: string
    CreatedCredential:
      type: object
      required:
      - id
      - app_id
      - prefix
      - label
      - created_at
      - expires_at
      - revoked_at
      - status
      - token
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        prefix:
          type: string
        label:
          type: string
        created_at:
          type: string
          format: date-time
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        status:
          type: string
        token:
          type: string
    CreatedKeyReply:
      type: object
      required:
      - id
      - prefix
      - name
      - scopes
      - created_by
      - created_at
      - last_used_at
      - expires_at
      - revoked_at
      - status
      - app_id
      - key
      properties:
        id:
          type: string
          format: uuid
        prefix:
          type: string
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        created_by:
          type:
          - string
          - 'null'
          format: uuid
        created_by_email:
          type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type:
          - string
          - 'null'
          format: date-time
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        status:
          type: string
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_name:
          type: string
        key:
          type: string
    CreatedWebhookReply:
      type: object
      required:
      - id
      - url
      - events
      - enabled
      - disabled_reason
      - disabled_at
      - created_at
      - secret
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
        events:
          type: array
          items:
            type: string
        enabled:
          type: boolean
        disabled_reason:
          type:
          - string
          - 'null'
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        secret:
          type: string
    CredentialView:
      type: object
      required:
      - id
      - app_id
      - prefix
      - label
      - created_at
      - expires_at
      - revoked_at
      - status
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        prefix:
          type: string
        label:
          type: string
        created_at:
          type: string
          format: date-time
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        status:
          type: string
    DeliveryView:
      type: object
      required:
      - id
      - event
      - event_id
      - state
      - attempts
      - next_at
      - payload
      properties:
        id:
          type: integer
        event:
          type: string
        event_id:
          type: string
        state:
          type: string
        attempts:
          type: array
          items:
            $ref: '#/components/schemas/AttemptView'
        next_at:
          type:
          - string
          - 'null'
          format: date-time
        payload:
          description: Free-form JSON.
    Direction:
      type: string
      enum:
      - egress
      - ingress
      description: Which way the bytes flowed through the customer's IP.
    DomainView:
      type: object
      required:
      - domain
      - verified
      - verified_at
      properties:
        domain:
          type: string
        verified:
          type: boolean
        verified_at:
          type:
          - string
          - 'null'
          format: date-time
    EnableInput:
      type: object
      required:
      - domains
      - confirm
      properties:
        domains:
          type: array
          items:
            type: string
        confirm:
          $ref: '#/components/schemas/Confirm'
    Enabled:
      type: object
      required:
      - org_id
      - status
      - enforced
      - default_role
      - domains
      - prices
      - users_this_period
      - period_start
      - enabled_at
      - disabled_at
      - links
      properties:
        org_id:
          type: string
          format: uuid
        status:
          type: string
        enforced:
          type: boolean
        default_role:
          type: string
        domains:
          type: array
          items:
            $ref: '#/components/schemas/DomainView'
        prices:
          $ref: '#/components/schemas/Object'
        users_this_period:
          type: integer
        period_start:
          type: string
          format: date-time
        enabled_at:
          type:
          - string
          - 'null'
          format: date-time
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
        refusal:
          type: string
        links:
          $ref: '#/components/schemas/Links'
    Endpoint:
      type: object
      required:
      - id
      - url
      - events
      - enabled
      - disabled_reason
      - disabled_at
      - created_at
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
        events:
          type: array
          items:
            type: string
        enabled:
          type: boolean
        disabled_reason:
          type:
          - string
          - 'null'
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    Error:
      type: object
      required:
      - error
      description: The one error body every route answers. Some codes add fields beside code and message, named in the route.
      properties:
        error:
          type: object
          required:
          - code
          - message
          - request_id
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
            request_id:
              type: string
              description: The X-Request-Id of the request.
            feature:
              type: string
              description: With plan.feature_required, connector.not_entitled, and the route feature refusals, the catalog feature the org's plan lacks, such as webhooks, inbound_tunnel, or inbound_ip.
            limit:
              type: integer
              description: With route.hostname_quota, the plan's hostnames quota.
    ErrorCode:
      type: string
      enum:
      - access.code_expired
      - access.code_invalid
      - access.code_used
      - access.config_invalid
      - access.credential_invalid
      - access.disabled
      - access.grant_limit
      - access.grant_principal_unknown
      - access.not_member
      - access.pkce_method
      - access.pkce_mismatch
      - access.rate_limited
      - access.redirect_uri_invalid
      - access.request_invalid
      - access.route_not_access
      - access.route_not_found
      - access.session_expired
      - access.session_invalid
      - access.session_not_found
      - access.session_revoked
      - access.too_many_teams
      - access.user_not_found
      - account_deactivated
      - acme.budget_exhausted
      - acme.challenge_unknown
      - acme.dns_unavailable
      - acme.hostname_not_route
      - acme.org_budget_exhausted
      - acme.rate_limited
      - acme.value_in_use
      - acme.value_invalid
      - addon_charge_not_failed
      - addon_invalid
      - addon_missing_price
      - addon_not_found
      - admin.busy
      - admin.email_taken
      - admin.inbound_staging
      - admin.job_not_cancelable
      - admin.job_not_retryable
      - admin.no_agent
      - admin.no_identity
      - admin.no_image
      - admin.push_failed
      - admin.reason_required
      - admin.reserved_ip
      - admin.state
      - admin.target_forbidden
      - admin.unavailable
      - admin.unsupported
      - admin.workos_failed
      - app.has_ips
      - app.has_routes
      - app.marketplace_managed
      - app.no_grant
      - app.not_found
      - app.not_org_member
      - app.vercel_linked
      - auth.app_key_scope
      - auth.email_conflict
      - auth.email_unverified
      - auth.forbidden
      - auth.invalid_token
      - auth.key_expired
      - auth.key_revoked
      - auth.not_member
      - auth.org_inactive
      - auth.org_suspended
      - auth.password_reset_required
      - auth.role_ceiling
      - auth.scope_ceiling
      - auth.second_factor_first
      - auth.session_revoked
      - auth.sso_required
      - auth.terms_required
      - auth.unknown_scope
      - auth.user_blocked
      - bad_request
      - billing.bad_payload
      - billing.bad_signature
      - billing.channel_locked
      - billing.channel_unavailable
      - billing.checkout_closed
      - billing.checkout_in_progress
      - billing.discounts_unsupported
      - billing.gst_details_invalid
      - billing.gstin_invalid
      - billing.invoice_not_found
      - billing.no_channel
      - billing.no_price
      - billing.no_subscription
      - billing.pack_not_on_plan
      - billing.packs_unavailable
      - billing.provider_rejected
      - billing.provider_unavailable
      - billing.same_plan
      - billing.seat_confirm_required
      - billing.seat_needs_admin
      - billing.subscription_started
      - billing.unknown_channel
      - billing.unknown_pack
      - billing.unknown_plan
      - billing.unsupported
      - billing.unsupported_kind
      - billing.webhook_failed
      - cert.state_invalid
      - confirm_mismatch
      - conflict
      - connector.disabled
      - connector.in_use
      - connector.name_invalid
      - connector.name_taken
      - connector.not_entitled
      - connector.not_found
      - connector.quota
      - connector.state_invalid
      - connector.token_invalid
      - credential.inactive
      - frozen
      - heroku.sso_refused
      - idempotency_key_required
      - internal
      - invite.accepted
      - invite.app_deleted
      - invite.email_mismatch
      - invite.expired
      - invite.not_found
      - invite.revoked
      - ip.not_reactivatable
      - ip.shared_not_forceable
      - ip_not_retired
      - kyc_required
      - logs.bad_query
      - logs.busy
      - method_not_allowed
      - mfa.factor_mismatch
      - mfa.identity_mismatch
      - mfa.locked
      - mfa.verified
      - mfa_required
      - node_holds_ips
      - not_found
      - not_suspended
      - org.already_member
      - org.country_locked
      - org.deleting
      - org.last_owner
      - org.limit
      - org.marketplace_managed
      - org.pool_protected
      - org.seat_limit
      - org.seat_marketplace
      - org.transfer_to_self
      - org_blocked
      - pack_below_floor
      - pack_invalid
      - plan.feature_required
      - plan_closed
      - plan_exists
      - plan_has_checkouts
      - plan_has_subscribers
      - plan_invalid
      - plan_missing_price
      - plan_not_found
      - plan_successor_invalid
      - plan_transition_refused
      - policy.cidr
      - policy.destination
      - policy.destination_ipv6
      - policy.duplicate
      - policy.limit
      - policy.pattern
      - policy.port
      - policy.target
      - rate_limited
      - read_only
      - readonly
      - region.unavailable
      - route.access_disabled
      - route.access_limits_invalid
      - route.access_name_invalid
      - route.access_name_taken
      - route.access_not_entitled
      - route.access_partners
      - route.conflict
      - route.idempotency_key_reused
      - route.ip_unavailable
      - route.kind_invalid
      - route.limit
      - route.tunnel_not_entitled
      - route.dedicated_not_entitled
      - route.not_found
      - route.inbound_ips_quota
      - route.quota
      - route.origin_fleet
      - route.origin_invalid
      - route.origin_link_local
      - route.origin_loopback
      - route.origin_metadata
      - route.origin_multicast
      - route.origin_port_blocked
      - route.origin_private
      - route.origin_reserved
      - route.origin_unresolvable
      - route.connector_disabled
      - route.connector_not_found
      - route.origin_conflict
      - route.origin_kind_invalid
      - route.proxy_protocol_host_only
      - route.target_invalid
      - route.partner_invalid
      - route.partner_open_to_all
      - route.partner_required
      - route.partners_too_many
      - route.port_invalid
      - route.ports_exhausted
      - route.sni_invalid
      - route.sni_reserved
      - route.placement_store
      - route.group_unavailable
      - route.port_taken
      - route.ip_not_dedicated
      - route.pool_full
      - route.region_unknown
      - route.placement_invalid
      - route.placement_unconfirmed
      - route.kind_origin
      - route.hostname_quota
      - route.hostname_taken
      - route.slug_taken
      - route.slug_reserved
      - route.slug_invalid
      - route.name_taken
      - route.name_reserved
      - route.name_invalid
      - route.tunnel_disabled
      - sso.already_enabled
      - sso.bad_domain
      - sso.channel_unsupported
      - sso.domain_taken
      - sso.not_configured
      - sso.not_enabled
      - sso.not_monthly
      - sso.not_ready
      - sso.plan_excluded
      - sso.price_changed
      - sso.provider_failed
      - sso.seat_limit
      - sso.trial
      - sso.unavailable
      - team.exists
      - team.idp_confirm
      - team.idp_group_taken
      - team.idp_managed
      - team.limit
      - team.no_grant
      - team.not_found
      - team.not_member
      - team.not_org_member
      - trial_org
      - tunnel.region_unknown
      - unavailable
      - upstream_failed
      - validation
      - vercel.billed_elsewhere
      - vercel.identity_taken
      - vercel.install_refused
      - vercel.installation_exists
      - vercel.installation_removed
      - vercel.installation_revoked
      - vercel.no_account
      - vercel.not_installed
      - vercel.not_owner
      - vercel.org_installed
      - vercel.plan_unknown
      - vercel.project_linked
      - vercel.project_refused
      - vercel.region_required
      - vercel.sign_in_refused
      - vercel.unavailable
      - webhook.bad_payload
      - webhook.bad_signature
      - webhook.bad_url
      - webhook.disabled
      - webhook.failed
      - webhook.limit
      - webhook.unknown_event
      description: Every code an error body can carry. The Go list in internal/api/errorcodes.gen.go is generated from this one.
    Features:
      type: object
      description: Every feature registry key. A bool, a whole number, or for regions_included a list of region ids where empty means every region.
      additionalProperties:
        oneOf:
        - type: boolean
        - type: integer
        - type: array
          items:
            type: string
    GSTView:
      type: object
      required:
      - gstin
      - legal_name
      - place_of_supply
      properties:
        gstin:
          type: string
        legal_name:
          type: string
        place_of_supply:
          type: string
        states:
          type: object
          additionalProperties:
            type: string
        seller_state:
          type: string
    Health:
      type: object
      required:
      - status
      properties:
        status:
          type: string
        version:
          type: string
        reason:
          type: string
    IP:
      type: object
      required:
      - id
      - address
      - region
      - provider
      - city
      - status
      - listeners
      - routes
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        address:
          type: string
        region:
          type: string
        provider:
          type: string
        city:
          type: string
        status:
          type: string
        listeners:
          type: array
          items:
            $ref: '#/components/schemas/Listener'
        routes:
          type: array
          items:
            $ref: '#/components/schemas/Route'
    IPPage:
      type: object
      required:
      - items
      - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IPSummary'
        next_cursor:
          type:
          - string
          - 'null'
          description: Pass as after for the next page; null on the last page.
    IPSummary:
      type: object
      required:
      - id
      - address
      - app_id
      - region
      - provider
      - city
      - status
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        address:
          type: string
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        region:
          type: string
        provider:
          type: string
        city:
          type: string
        status:
          type: string
          enum:
          - reserved
          - active
          - suspended
    InstallationView:
      type: object
      required:
      - id
      - plan
      - state
      - projects
      properties:
        id:
          type: string
        team_id:
          type: string
        plan:
          type: string
        state:
          type: string
        projects:
          type: array
          items:
            $ref: '#/components/schemas/LinkView'
    Invitation:
      type: object
      required:
      - id
      - org_id
      - email
      - role
      - invited_by
      - expires_at
      - created_at
      - expired
      - app_id
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        email:
          type: string
        role:
          type: string
        invited_by:
          type:
          - string
          - 'null'
          format: uuid
        invited_by_email:
          type: string
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        expired:
          type: boolean
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_role:
          type: string
        app_name:
          type: string
    InviteBody:
      type: object
      required:
      - email
      - role
      - confirm_extra_seat
      properties:
        email:
          type: string
        role:
          type: string
        confirm_extra_seat:
          type: boolean
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_role:
          type: string
    InvoiceView:
      type: object
      required:
      - id
      - number
      - legal_name
      - place_of_supply
      - description
      - currency
      - taxable
      - cgst
      - sgst
      - igst
      - total
      - issued_at
      - pdf_url
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
        legal_name:
          type: string
        gstin:
          type: string
        place_of_supply:
          type: string
        description:
          type: string
        currency:
          type: string
        taxable:
          type: integer
        cgst:
          type: integer
        sgst:
          type: integer
        igst:
          type: integer
        total:
          type: integer
        issued_at:
          type: string
          format: date-time
        pdf_url:
          type: string
    Issued:
      type: object
      required:
      - id
      - org_id
      - email
      - role
      - invited_by
      - expires_at
      - created_at
      - expired
      - app_id
      - existing
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        email:
          type: string
        role:
          type: string
        invited_by:
          type:
          - string
          - 'null'
          format: uuid
        invited_by_email:
          type: string
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        expired:
          type: boolean
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_role:
          type: string
        app_name:
          type: string
        link:
          type: string
        existing:
          type: boolean
    Job:
      type: object
      required:
      - job_id
      properties:
        job_id:
          type: string
          format: uuid
    JobView:
      type: object
      required:
      - id
      - app_id
      - state
      - done
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        state:
          type: string
          description: 'The provisioning state: a step name while running, then active, failed, or rejected.'
        done:
          type: boolean
          description: True once the job will not change again.
        error:
          type: string
    Joined:
      type: object
      required:
      - org_id
      - org_name
      - role
      properties:
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        role:
          type: string
        app_id:
          type:
          - string
          - 'null'
          format: uuid
        app_role:
          type: string
    LinkBody:
      type: object
      required:
      - app_id
      properties:
        app_id:
          type: string
        region:
          type: string
    LinkView:
      type: object
      required:
      - project_id
      - app_id
      - env
      - linked_at
      properties:
        project_id:
          type: string
        app_id:
          type: string
          format: uuid
        env:
          type: array
          items:
            type: string
        linked_at:
          type: string
          format: date-time
    Links:
      type: object
      properties:
        setup:
          type: string
        verify_domains:
          type: string
    Listener:
      type: object
      required:
      - address
      - port
      - kind
      additionalProperties: false
      description: One way to reach the IP. The address the proxy binds never leaves the server.
      properties:
        address:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
        kind:
          type: string
          enum:
          - http_forward
          - socks5
          - tls_front
          - tls_socks
          - inbound_tcp
          - inbound_sni
    MemberAccess:
      type: object
      required:
      - all
      - apps
      - teams
      properties:
        all:
          type: boolean
        apps:
          type: array
          items:
            $ref: '#/components/schemas/MemberApp'
        teams:
          type: array
          items:
            $ref: '#/components/schemas/MemberTeam'
    MemberApp:
      type: object
      required:
      - id
      - name
      - role
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        role:
          type: string
    MemberTeam:
      type: object
      required:
      - id
      - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    MemberView:
      type: object
      required:
      - user_id
      - email
      - name
      - role
      - mfa_enabled
      - active_keys
      - joined_at
      - access
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
        name:
          type: string
        role:
          type: string
        mfa_enabled:
          type: boolean
        active_keys:
          type: integer
        joined_at:
          type: string
          format: date-time
        access:
          $ref: '#/components/schemas/MemberAccess'
    Membership:
      type: object
      required:
      - org_id
      - org_name
      - org_status
      - role
      properties:
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        org_status:
          type: string
        role:
          type: string
    NextPrice:
      type: object
      required:
      - amount_minor
      - currency
      - interval
      - effective_from
      properties:
        amount_minor:
          type: integer
        currency:
          type: string
        interval:
          type: string
        effective_from:
          type: string
          format: date-time
    Object:
      type: object
      description: A JSON object; its fields are described on the route.
    OptString:
      type:
      - string
      - 'null'
    Org:
      type: object
      required:
      - id
      - name
      - country
      - role
      - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        country:
          type: string
        role:
          type: string
        created_at:
          type: string
          format: date-time
    OrgDeletion:
      type: object
      required:
      - id
      - status
      - purge_after
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          const: deleting
        purge_after:
          type: string
          format: date-time
          description: When org.purge removes the org's operational rows. Invoices, payments, and the audit log stay.
    OrgPatch:
      type: object
      properties:
        name:
          type:
          - string
          - 'null'
        country:
          type:
          - string
          - 'null'
        mfa_required:
          type:
          - boolean
          - 'null'
    OrgView:
      type: object
      required:
      - id
      - name
      - country
      - status
      - mfa_required
      - has_subscription
      - members_without_mfa
      - created_at
      - delete_requested_at
      - purge_after
      - tunnel
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        country:
          type: string
        tunnel:
          $ref: '#/components/schemas/OrgTunnel'
        status:
          type: string
          description: active, suspended, suspended_for_abuse, blocked, or closed; deleting once an owner asked to delete the org.
        mfa_required:
          type: boolean
        has_subscription:
          type: boolean
        members_without_mfa:
          type: integer
        created_at:
          type: string
          format: date-time
        delete_requested_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the deletion was asked, while status is deleting.
        purge_after:
          type:
          - string
          - 'null'
          format: date-time
          description: When org.purge removes the org's operational rows, while status is deleting.
    OrgTunnel:
      type: object
      required:
      - slug
      - zone
      description: Where the org's hostnames live. A hostname is <name>.<slug>.<zone>.
      properties:
        slug:
          type:
          - string
          - 'null'
          description: The org's slug, fixed by its first hostname route; null until then. A later route's org_slug is ignored.
        zone:
          type: string
          description: The tunnel zone, FM_TUNNEL_ZONE, such as fmtunnel.test in dev. Empty when tunnels are off.
    PackBody:
      type: object
      required:
      - pack_id
      properties:
        pack_id:
          type: string
    PackCheckoutView:
      type: object
      required:
      - url
      - pack_id
      - channel
      properties:
        url:
          type: string
        pack_id:
          type: string
        channel:
          type: string
    PackOffer:
      type: object
      required:
      - id
      - product
      - bytes
      - currency
      - amount_minor
      properties:
        id:
          type: string
        product:
          type: string
        bytes:
          type: integer
        currency:
          type: string
        amount_minor:
          type: integer
    PacksView:
      type: object
      required:
      - available
      - product
      - period_start
      - allowance_bytes
      - plan_allowance_bytes
      - used_bytes
      - state
      - packs
      - applied
      - auto_pack
      - can_manage
      properties:
        available:
          type: boolean
        reason:
          type: string
        plan_id:
          type: string
        plan_name:
          type: string
        product:
          type: string
        period_start:
          type: string
        allowance_bytes:
          type: integer
        plan_allowance_bytes:
          type: integer
        used_bytes:
          type: integer
        state:
          type: string
        packs:
          type: array
          items:
            $ref: '#/components/schemas/PackOffer'
        applied:
          type: array
          items:
            $ref: '#/components/schemas/AppliedPackView'
        auto_pack:
          type: boolean
        can_manage:
          type: boolean
        products:
          type: array
          description: Each product the plan meters, egress first, with the label the dashboard shows. An ingress entry appears only when the plan's inbound_bytes quota is above 0.
          items:
            $ref: '#/components/schemas/ProductAllowance'
    ProductAllowance:
      type: object
      required:
      - product
      - label
      - allowance_bytes
      - plan_allowance_bytes
      - used_bytes
      - state
      properties:
        product:
          $ref: '#/components/schemas/Product'
        label:
          type: string
          description: Outbound for egress, Inbound for ingress.
        allowance_bytes:
          type: integer
        plan_allowance_bytes:
          type: integer
        used_bytes:
          type: integer
        state:
          type: string
    PendingInvitation:
      type: object
      required:
      - id
      - org_id
      - org_name
      - role
      - expires_at
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        role:
          type: string
        invited_by_email:
          type: string
        expires_at:
          type: string
          format: date-time
    Period:
      type: object
      additionalProperties: false
      required:
      - product
      - label
      - period_start
      - bytes_total
      - allowance_bytes
      - percent
      - state
      properties:
        product:
          $ref: '#/components/schemas/Product'
        label:
          type: string
          description: Outbound for egress, Inbound for ingress.
        period_start:
          type: string
          format: date
        bytes_total:
          type: integer
          format: int64
          minimum: 0
        allowance_bytes:
          type: integer
          format: int64
          minimum: 0
        percent:
          type: integer
          format: int64
          minimum: 0
          description: bytes_total of allowance_bytes in whole percent, rounded down, the figure the quota thresholds compare.
        state:
          type: string
          enum:
          - ok
          - soft
          - throttled
          - blocked
    PlanList:
      type: object
      required:
      - plans
      properties:
        channel:
          type: string
        currency:
          type: string
        plans:
          type: array
          items:
            $ref: '#/components/schemas/CatalogPlan'
    PlanStatus:
      type: string
      enum:
      - draft
      - active
      - closed
      - retired
    PlanView:
      type: object
      required:
      - id
      - name
      - current
      - dedicated
      - bytes
      - regions
      properties:
        id:
          type: string
        name:
          type: string
        currency:
          type: string
        amount_minor:
          type: integer
        current:
          type: boolean
        dedicated:
          type: boolean
        bytes:
          type: integer
        regions:
          type: integer
    PolicyDecision:
      type: object
      required:
      - allowed
      - reason
      - message
      properties:
        allowed:
          type: boolean
        reason:
          type: string
          enum:
          - no_rules
          - match
          - no_match
          - resolve_failed
          - reserved_address
        rule:
          $ref: '#/components/schemas/PolicyRule'
        message:
          type: string
    PolicyList:
      type: object
      required:
      - rules
      - max_rules
      properties:
        rules:
          type:
          - array
          - 'null'
          items:
            $ref: '#/components/schemas/PolicyRule'
        max_rules:
          type: integer
    PolicyRule:
      type: object
      required:
      - id
      - created_by
      - created_at
      description: One allowlist rule. Exactly one of host_pattern and cidr is present, and a missing port matches every port.
      properties:
        id:
          type: string
          format: uuid
        host_pattern:
          type: string
        cidr:
          type: string
        port:
          type: integer
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
    PolicyRuleBody:
      type: object
      additionalProperties: false
      properties:
        host_pattern:
          type: string
          description: An exact host, or *. and a suffix that matches every name under it.
        cidr:
          type: string
          description: An IPv4 CIDR; a bare address is its /32.
        port:
          type: integer
          minimum: 0
          maximum: 65535
          description: 0 or absent matches every port.
    PolicyTestBody:
      type: object
      additionalProperties: false
      required:
      - destination
      properties:
        destination:
          type: string
          description: host or host:port; port 443 when omitted.
    PortalBody:
      type: object
      required:
      - intent
      properties:
        intent:
          type: string
    Preview:
      type: object
      required:
      - id
      - org_id
      - org_name
      - role
      - expires_at
      - accepted
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        role:
          type: string
        expires_at:
          type: string
          format: date-time
        accepted:
          type: boolean
        app_name:
          type: string
        app_role:
          type: string
    Product:
      type: string
      enum:
      - egress
      - ingress
      description: The billed line.
    Quotas:
      type: object
      description: Every quota registry key with its value. A new key is a registry entry, not a schema change.
      additionalProperties:
        type: integer
        format: int64
    RegionView:
      type: object
      required:
      - id
      - provider
      - provider_name
      - city
      - node_model
      - live
      properties:
        id:
          type: string
        provider:
          type: string
        provider_name:
          type: string
        city:
          type: string
        node_model:
          type: string
        live:
          type: boolean
    RemoveReply:
      type: object
      required:
      - user_id
      - revoked_keys
      properties:
        user_id:
          type: string
          format: uuid
        revoked_keys:
          type: integer
    ReplayReply:
      type: object
      required:
      - id
      properties:
        id:
          type: integer
    RoleBody:
      type: object
      required:
      - role
      properties:
        role:
          type: string
    RotatedCredential:
      type: object
      required:
      - credential
      - previous
      properties:
        credential:
          $ref: '#/components/schemas/CreatedCredential'
        previous:
          $ref: '#/components/schemas/CredentialView'
    Route:
      type: object
      required:
      - id
      - app_id
      - org_id
      - name
      - hostname
      - region
      - placement
      - placements
      - port
      - kind
      - origin
      - origin_host
      - origin_port
      - partner_cidrs
      - warning
      - state
      - state_reason
      - health
      - cert
      - created_at
      description: 'An inbound route of an app. Its IPs are placements: two shared ingress IPs of a placement group, or one dedicated IP of the app. Each placement has its own state and health, and the route''s are their roll-up. It never carries a bind address, a node id, or a token. origin carries the proto''s origin_kind, connector_id, and proxy_protocol, and origin_host and origin_port are its host and port, or a connector''s target.'
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
          description: The app the route belongs to.
        org_id:
          type: string
          format: uuid
        hostname:
          type:
          - string
          - 'null'
          description: <name>.<org-slug>.<zone>, lower case. Null for an access route, a route with an sni_pattern, and a dedicated tcp route made while hostnames were off.
        region:
          type: string
        placement:
          $ref: '#/components/schemas/RoutePlacement'
        placements:
          type: array
          description: Every IP the route is served on now. Empty for a route whose placements a plan lapse, a drain, or a teardown released.
          items:
            $ref: '#/components/schemas/RoutePlacedOn'
        port:
          type: integer
          description: The port the route holds on its IPs, 20000 to 29999 for tcp and 443 for every other kind. A route whose IP was taken off its node shows the port it held, which a reactivation gives back.
        kind:
          type: string
          enum:
          - tcp
          - tls
          - http
          - access
          description: tcp is raw TCP on a port of its own. tls is TLS on 443 routed by its hostname, or on a dedicated IP by its sni_pattern, and passed through to the origin. http is TLS on 443 that fixedmarkd terminates on the customer's machine for a plain-HTTP target. access is a route signed-in users reach through fm-access.
        sni_pattern:
          type: string
          description: An exact name or *. and a domain, lower case. Dedicated tls only.
        origin:
          $ref: '#/components/schemas/RouteOrigin'
        origin_host:
          type: string
        origin_port:
          type: integer
        partner_cidrs:
          type: array
          items:
            type: string
          description: The partner sources the route admits, IPv4 CIDRs.
        warning:
          type:
          - string
          - 'null'
          enum:
          - open_to_all
          - null
          description: open_to_all when a partner is 0.0.0.0/0 or wider than a /8, so most sources may connect.
        state:
          type: string
          enum:
          - pending
          - bound
          - active
          - disabled
          - paused
          description: The most advanced state of the route's placements. pending until a node's proxy binds a placement's listener, bound once one does, and active once that node's firewall opens the port to the route's partners. disabled is a route FixedMark staff turned off; no node serves it. paused is a bound or active route of a suspended IP, with state_reason ip_suspended; no node serves it until the IP is active again.
        state_reason:
          type:
          - string
          - 'null'
          description: Why a route is still pending, such as proxy_unsupported, ip_retired for a route whose IP was taken off its node, plan_lapsed, pool_draining, the staff reason of a disabled route, or ip_suspended for a paused one.
        health:
          type: string
          enum:
          - healthy
          - degraded
          - down
          - unknown
          description: healthy when at least one placement is healthy and every other is healthy or not yet judged, since an unknown placement is ignored while another is healthy. degraded when one is healthy and another is judged unhealthy, down when none is healthy and at least one is judged, and unknown when none is judged, for a disabled route, and for a route with no placement.
        dns:
          $ref: '#/components/schemas/RouteDNS'
        cert:
          oneOf:
          - $ref: '#/components/schemas/RouteCert'
          - type: 'null'
          description: An http route's certificate as fixedmarkd last reported it; null on every other kind.
        created_at:
          type: string
          format: date-time
        name:
          type: string
          description: The route's name, unique among the organization's live routes and its hostname's first label. A route created with none is named its kind and the first 8 hex digits of its id, such as tcp-5f0c2b1e.
        access:
          $ref: '#/components/schemas/RouteAccessLimits'
    RouteCert:
      type: object
      required:
      - state
      - serial
      - not_after
      - error
      - reported_at
      - certified_at
      description: The certificate fixedmarkd holds for an http route's hostname, on the customer's machine. The key never leaves that machine; this is the daemon's report.
      properties:
        state:
          type: string
          enum:
          - none
          - queued
          - issuing
          - valid
          - renewing
          - renewal_failing
          - failed
          description: none before any report. renewal_failing is a renewal that failed while the current certificate still serves; failed is a route with no certificate to serve.
        serial:
          type:
          - string
          - 'null'
          description: The serial of the certificate in use, in hex.
        not_after:
          type:
          - string
          - 'null'
          format: date-time
        error:
          type:
          - string
          - 'null'
          description: The last error the daemon reported.
        reported_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the daemon last reported.
        certified_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the control plane last certified the hostname on its own evidence, a challenge of the route's connector, which makes its next order a renewal.
    RouteDNS:
      type: object
      required:
      - name
      - target
      - state
      - synced_at
      - error
      description: A hostname route's record in the tunnel zone. Set on every route with a hostname. The record is a CNAME at name. A shared route points at its placement group's name while it is placed on every IP of the group, and at one IP's name otherwise; a dedicated route points at its IP's name. When some placements are active and healthy and others are not, it points at the name of a healthy one. Health never removes the record. It is absent only while the route is disabled or unplaced, and goes when the route is deleted.
      properties:
        name:
          type: string
          description: The record's name, the route's hostname.
        target:
          type: string
          nullable: true
          description: The name the CNAME points at, such as pg-0123456789ab.<zone> or ip-0123456789ab.<zone>; null when unpublished.
        state:
          type: string
          enum:
          - unpublished
          - pending
          - synced
          - refused
          description: pending until the record is written or removed, which takes about two seconds; refused when Cloudflare refused it, retried when the route changes and at the hourly reconcile.
        synced_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
          description: Why Cloudflare refused the record, as its status and its error codes and messages; null unless state is refused.
    RouteOrigin:
      type: object
      required:
      - kind
      description: 'Where a route''s partner connections go. kind host is {host, port}, dialed from the node. kind connector is {connector_id, target, proxy_protocol}: target is host:port on the connector''s side, and loopback and private targets are allowed there.'
      properties:
        kind:
          type: string
          enum:
          - host
          - connector
        host:
          type: string
        port:
          type: integer
        connector_id:
          type: string
          format: uuid
        target:
          type: string
        proxy_protocol:
          type: boolean
    CreateRouteOrigin:
      type: object
      required:
      - kind
      additionalProperties: false
      description: The origin of a new route, {kind host, host, port} or {kind connector, connector_id, target, proxy_protocol}. route.origin_conflict answers a body that mixes the two, or sends origin with origin_host. route.proxy_protocol_host_only answers proxy_protocol on a host origin.
      properties:
        kind:
          type: string
          enum:
          - host
          - connector
        host:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
        connector_id:
          type: string
          format: uuid
        target:
          type: string
          description: host:port, such as 127.0.0.1:8080. route.target_invalid answers anything else.
        proxy_protocol:
          type: boolean
          description: The connector writes a PROXY protocol v2 header naming the partner before the first byte.
    Connector:
      type: object
      required:
      - id
      - app_id
      - name
      - token_prefix
      - state
      - created_at
      - updated_at
      - last_seen_at
      - last_hostname
      - last_version
      - sessions
      description: A connector, fixedmarkd on the customer's machine. It never holds the token.
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        name:
          type: string
        token_prefix:
          type: string
          description: The 8 characters after fmc_ of the connector's token.
        state:
          type: string
          enum:
          - active
          - disabled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_seen_at:
          type:
          - string
          - 'null'
          format: date-time
        last_hostname:
          type:
          - string
          - 'null'
        last_version:
          type:
          - string
          - 'null'
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/ConnectorSession'
    ConnectorSession:
      type: object
      required:
      - ip_id
      - ip
      - connected_at
      - last_seen_at
      - remote_addr
      - hostname
      - version
      - streams_open
      - disconnected_at
      - disconnect_reason
      description: One session a node reports for a connector on one customer IP. disconnect_reason is missing_from_heartbeat, connector_unbound, connector_disabled, connector_deleted, token_rotated, or staff_disconnect.
      properties:
        ip_id:
          type: string
          format: uuid
        ip:
          type: string
          description: The customer IP's public address, the one the connector dials.
        connected_at:
          type: string
          format: date-time
        last_seen_at:
          type: string
          format: date-time
        remote_addr:
          type:
          - string
          - 'null'
        hostname:
          type:
          - string
          - 'null'
        version:
          type:
          - string
          - 'null'
        streams_open:
          type: integer
        disconnected_at:
          type:
          - string
          - 'null'
          format: date-time
        disconnect_reason:
          type:
          - string
          - 'null'
    ConnectorList:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Connector'
    CreateConnectorBody:
      type: object
      required:
      - name
      additionalProperties: false
      properties:
        name:
          type: string
          description: 1 to 64 letters, digits, dots, dashes, and underscores, starting with a letter or digit, unique in the app.
    ConnectorCreated:
      type: object
      required:
      - connector
      - token
      - install_command
      properties:
        connector:
          $ref: '#/components/schemas/Connector'
        token:
          type: string
          description: The connector token, shown this once.
        install_command:
          type: string
          description: The shell line that, with sudo, installs fixedmarkd as a service with the token read from fm-connector-<name>.token in the directory it runs from. It needs fixedmarkd on the PATH. Save the token there at mode 0600 first; fm connectors create and rotate write it there. The token is never on the line.
    ConnectorInUse:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          - route_ids
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
            route_ids:
              type: array
              items:
                type: string
                format: uuid
    ConnectorPlan:
      type: object
      required:
      - version
      - connector_id
      - app_id
      - endpoints
      - routes
      - sessions
      - refresh_after_s
      properties:
        version:
          type: integer
          enum:
          - 2
        connector_id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/ConnectorEndpoint'
        routes:
          type: array
          items:
            $ref: '#/components/schemas/ConnectorPlanRoute'
        acme:
          $ref: '#/components/schemas/ConnectorPlanACME'
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/ConnectorSession'
        refresh_after_s:
          type: integer
    ConnectorEndpoint:
      type: object
      required:
      - ip_id
      - address
      - port
      - sni
      description: One IP the connector dials, by its public address.
      properties:
        ip_id:
          type: string
          format: uuid
        address:
          type: string
        port:
          type: integer
        sni:
          type: string
    ConnectorPlanRoute:
      type: object
      required:
      - id
      - name
      - kind
      - hostname
      - tls
      - target
      - proxy_protocol
      - placements
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        kind:
          type: string
          enum:
          - tcp
          - tls
          - http
          - access
        hostname:
          type:
          - string
          - 'null'
          description: The route's hostname, null for a route that has none.
        tls:
          type: string
          enum:
          - terminate
          - passthrough
          description: terminate for http, where fixedmarkd serves the certificate; passthrough for every other kind.
        target:
          type: string
        proxy_protocol:
          type: boolean
        placements:
          type: array
          description: The IPs and ports partners reach the route on, active and healthy first.
          items:
            $ref: '#/components/schemas/ConnectorPlanPlacement'
    ConnectorPlanPlacement:
      type: object
      required:
      - ip_id
      - port
      properties:
        ip_id:
          type: string
          format: uuid
        port:
          type: integer
    ConnectorPlanACME:
      type: object
      required:
      - directory_url
      - relay_url
      description: Where fixedmarkd orders its certificates and asks for its DNS-01 records.
      properties:
        directory_url:
          type: string
        relay_url:
          type: string
          description: The DNS-01 relay. Unless the control plane names one, it is the relay on the origin the plan was fetched from.
    ConnectorChallengeCreate:
      type: object
      required:
      - hostname
      - value
      properties:
        hostname:
          type: string
        value:
          type: string
          description: The DNS-01 key authorization digest, 43 base64url characters.
    ConnectorChallenge:
      type: object
      required:
      - id
      - fqdn
      - ttl
      - visible
      properties:
        id:
          type: string
          format: uuid
        fqdn:
          type: string
          description: _acme-challenge.<hostname>.
        ttl:
          type: integer
        visible:
          type: boolean
          description: True once every authoritative nameserver of the tunnel zone answers the value.
    ConnectorCertReport:
      type: object
      required:
      - state
      properties:
        state:
          type: string
          enum:
          - queued
          - issuing
          - valid
          - renewing
          - failed
        not_after:
          type:
          - string
          - 'null'
          format: date-time
        serial:
          type: string
        issuer:
          type: string
        error:
          type: string
    RoutePlacement:
      type: object
      required:
      - mode
      description: The placement the route asked for.
      properties:
        mode:
          type: string
          enum:
          - shared
          - dedicated
        ip_id:
          type: string
          format: uuid
          description: The dedicated IP. dedicated only.
    RoutePlacedOn:
      type: object
      required:
      - ip_id
      - address
      - port
      - state
      - state_reason
      - health
      - health_since
      properties:
        ip_id:
          type: string
          format: uuid
        address:
          type: string
          description: The IP's public address.
        port:
          type: integer
        state:
          type: string
          enum:
          - pending
          - bound
          - active
          description: pending until the node serving this IP binds the route's listener there, bound once it does, and active once its firewall opens the port to the route's partners.
        state_reason:
          type:
          - string
          - 'null'
          description: Why the placement is still pending, such as proxy_unsupported, listener_rejected, or placement_changed.
        health:
          type: string
          enum:
          - unknown
          - healthy
          - no_session
          - listener_down
          - origin_down
          - ip_down
          description: no_session is a connector route whose connector holds no session on this IP, listener_down a placement FixedMark cannot reach, origin_down a host route whose origin stops answering this IP's node, and ip_down a placement whose node stopped answering.
        health_since:
          type: string
          format: date-time
    SetRoutePlacementBody:
      type: object
      required:
      - mode
      additionalProperties: false
      properties:
        mode:
          type: string
          enum:
          - shared
          - dedicated
        ip_id:
          type: string
          format: uuid
          description: A dedicated IP of the app. dedicated only.
        confirm:
          type: boolean
          description: Accepts that a tcp route with partners moves to a new IP and port, which its partners dial. A tcp route with partners needs it to move.
    RoutePlacementChanged:
      type: object
      required:
      - route
      - from
      - changed
      - port_changed
      properties:
        route:
          $ref: '#/components/schemas/Route'
        from:
          type: string
          enum:
          - shared
          - dedicated
        changed:
          type: boolean
          description: False when the route was already in the placement asked for and nothing moved.
        port_changed:
          type: boolean
          description: A tcp route whose port was taken on its new IPs and that holds another now.
    RouteList:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Route'
    RouteCreated:
      type: object
      required:
      - job_id
      - route
      properties:
        job_id:
          type: string
          format: uuid
        route:
          $ref: '#/components/schemas/Route'
    SetRoutePartnersBody:
      type: object
      required:
      - partner_cidrs
      additionalProperties: false
      properties:
        partner_cidrs:
          type: array
          description: 1 to 256 IPv4 addresses or CIDRs. More answers route.partners_too_many.
          items:
            type: string
        allow_any:
          type: boolean
          description: Confirms a 0.0.0.0/0 partner, or one wider than a /8.
    RoutePartners:
      type: object
      required:
      - route
      - before
      - warning
      properties:
        route:
          $ref: '#/components/schemas/Route'
        before:
          type: array
          items:
            type: string
          description: The list this one replaced.
        warning:
          type:
          - string
          - 'null'
          enum:
          - open_to_all
          - null
    PartnersRefusal:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          - problems
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
            problems:
              type: array
              items:
                type: object
                required:
                - code
                properties:
                  code:
                    $ref: '#/components/schemas/ErrorCode'
                  message:
                    type: string
    AccessConfig:
      type: object
      required: [dashboard_url, min_client_version, recommended_client_version, credential_ttl_secs, sni]
      properties:
        dashboard_url:
          type: string
        min_client_version:
          type: string
        recommended_client_version:
          type: string
        credential_ttl_secs:
          type: integer
        sni:
          type: string
    AccessAuthorizeBody:
      type: object
      additionalProperties: false
      required: [org_id, redirect_uri, code_challenge, code_challenge_method, device_name]
      properties:
        org_id:
          type: string
          format: uuid
        redirect_uri:
          type: string
        code_challenge:
          type: string
        code_challenge_method:
          type: string
          enum: [S256]
        device_name:
          type: string
          maxLength: 100
    AccessAuthorization:
      type: object
      required: [code, expires_in]
      properties:
        code:
          type: string
        expires_in:
          type: integer
    AccessTokenBody:
      type: object
      additionalProperties: false
      required: [grant_type]
      properties:
        grant_type:
          type: string
          enum: [authorization_code, refresh_token]
        code:
          type: string
        code_verifier:
          type: string
        redirect_uri:
          type: string
        device_name:
          type: string
        client_version:
          type: string
        client_os:
          type: string
        client_arch:
          type: string
        refresh_token:
          type: string
    AccessToken:
      type: object
      required: [credential, credential_expires_at, session_expires_at, session_id, org_id, user]
      properties:
        refresh_token:
          type: string
          description: fmar_ and 52 lowercase base32 characters, only on an authorization_code exchange.
        credential:
          type: string
          description: fma1. and the signed claims; present it to the node in the access hello.
        credential_expires_at:
          type: string
          format: date-time
        session_expires_at:
          type: string
          format: date-time
        session_id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        user:
          type: object
          required: [id, email]
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
    AccessEndpoint:
      type: object
      required: [address, port, sni]
      properties:
        address:
          type: string
        port:
          type: integer
        sni:
          type: string
    AccessRouteList:
      type: object
      required: [org_id, items]
      properties:
        org_id:
          type: string
          format: uuid
        items:
          type: array
          items:
            type: object
            required: [id, name, app_id, app_name, endpoint, endpoints, origin_kind, target, max_session_secs]
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              app_id:
                type: string
                format: uuid
              app_name:
                type: string
              endpoint:
                description: The route's first healthy placement, else its first.
                $ref: '#/components/schemas/AccessEndpoint'
              endpoints:
                description: Every live placement of the route, the healthy ones first. fm-access dials them in order.
                type: array
                items:
                  $ref: '#/components/schemas/AccessEndpoint'
              origin_kind:
                type: string
                enum: [host, connector]
              target:
                type: string
              connector_name:
                type: string
              max_session_secs:
                type: integer
    AccessLogoutBody:
      type: object
      additionalProperties: false
      required: [refresh_token]
      properties:
        refresh_token:
          type: string
    AccessSessionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [id, user_id, email, device_name, client_version, client_os, client_arch, refresh_prefix, created_at, expires_at, last_refreshed_at]
            properties:
              id:
                type: string
                format: uuid
              user_id:
                type: string
                format: uuid
              email:
                type: string
              device_name:
                type: string
              client_version:
                type: string
              client_os:
                type: string
              client_arch:
                type: string
              refresh_prefix:
                type: string
              created_at:
                type: string
                format: date-time
              expires_at:
                type: string
                format: date-time
              last_refreshed_at:
                type:
                - string
                - 'null'
                format: date-time
    AccessRevoked:
      type: object
      required: [revoked, sessions]
      properties:
        revoked:
          type: boolean
        user_id:
          type: string
          format: uuid
        sessions:
          type: integer
    RouteAccessLimits:
      type: object
      additionalProperties: false
      description: An access route's own limits. 0 is the default, no cap on one connection's life and the node's one-hour idle timer. route.access_limits_invalid answers a value out of range.
      properties:
        max_session_secs:
          type: integer
          minimum: 0
          maximum: 604800
        idle_secs:
          type: integer
          minimum: 0
          maximum: 86400
    AccessPolicy:
      type: object
      required:
      - route_id
      - users
      - teams
      properties:
        route_id:
          type: string
          format: uuid
        users:
          type: array
          items:
            type: object
            required:
            - id
            - email
            properties:
              id:
                type: string
                format: uuid
              email:
                type: string
        teams:
          type: array
          items:
            type: object
            required:
            - id
            - name
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
    SetAccessPolicyBody:
      type: object
      additionalProperties: false
      description: The whole policy. users are user ids or emails, teams are team ids, slugs, or names, all of the route's organization. At most 100 together.
      properties:
        users:
          type: array
          items:
            type: string
        teams:
          type: array
          items:
            type: string
    CreateRouteBody:
      type: object
      required:
      - kind
      additionalProperties: false
      description: The route's origin is origin, or the legacy origin_host and origin_port, which are a host origin. A partner route needs partner_cidrs. Omitted placement is shared. An access route takes name and optional access limits and no partner_cidrs, and needs FM_ACCESS_ENABLED (else route.access_disabled) and the plan's access feature (else route.access_not_entitled).
      properties:
        kind:
          type: string
          enum:
          - http
          - tls
          - tcp
          - access
        name:
          type: string
          description: The route's name and its hostname's first label, unique in the organization (route.name_taken). 1 to 40 lowercase letters, digits, and single hyphens, starting and ending with a letter or digit, not starting xn (route.name_invalid), and not a reserved name or one starting pg- or ip- (route.name_reserved). Omitted takes a generated name. An access route needs one, 1 to 63 lowercase letters, digits, and hyphens (route.access_name_invalid).
        org_slug:
          type: string
          description: The org's slug on its first hostname route, under the name rules (route.slug_invalid, route.slug_reserved) and unique (route.slug_taken). Omitted takes the org's name. Once set it is fixed, and a later org_slug is ignored.
        region:
          type: string
          description: A shared route's region. Omitted takes the first region with a shared pool (route.region_unknown).
        placement:
          type: object
          additionalProperties: false
          description: '{mode: shared}, the default, or {mode: dedicated, ip_id} for a dedicated IP of this app, which needs inbound_ip.'
          properties:
            mode:
              type: string
              enum:
              - shared
              - dedicated
            ip_id:
              type: string
              format: uuid
        origin:
          $ref: '#/components/schemas/CreateRouteOrigin'
        origin_host:
          type: string
          description: A host name with an A record, or an IPv4 address. Private, loopback, link-local, CGNAT, multicast, the metadata address, and FixedMark's own addresses are refused. Dedicated only.
        origin_port:
          type: integer
          minimum: 1
          maximum: 65535
        proxy_protocol:
          type: boolean
          description: Asks the connector for a PROXY v2 header. Not for http.
        partner_cidrs:
          type: array
          maxItems: 256
          items:
            type: string
        allow_any:
          type: boolean
          description: Confirms a 0.0.0.0/0 partner, or one wider than a /8, which admits most sources. Without it such a partner answers route.partner_open_to_all.
        sni_pattern:
          type: string
          description: Dedicated tls only. A shared tls route is routed by its hostname.
        port:
          type: integer
          description: A port from 20000 to 29999 for a dedicated tcp route. Omitted picks the lowest free one.
        access:
          $ref: '#/components/schemas/RouteAccessLimits'
    SeatsView:
      type: object
      required:
      - used
      - included
      - max
      - open_invites
      - billable
      - price
      properties:
        used:
          type: integer
        included:
          type: integer
        max:
          type:
          - integer
          - 'null'
        open_invites:
          type: integer
        billable:
          type: boolean
        price:
          oneOf:
          - $ref: '#/components/schemas/Object'
          - type: 'null'
        next_price:
          oneOf:
          - $ref: '#/components/schemas/NextPrice'
          - type: 'null'
        plan_id:
          type: string
        channel:
          type: string
    State:
      type: object
      required:
      - memberships
      - active_org_id
      - active_role
      - pending_invitations
      properties:
        memberships:
          type: array
          items:
            $ref: '#/components/schemas/Membership'
        active_org_id:
          type:
          - string
          - 'null'
          format: uuid
        active_role:
          type:
          - string
          - 'null'
        pending_invitations:
          type: array
          items:
            $ref: '#/components/schemas/PendingInvitation'
    Status:
      type: object
      required:
      - status
      additionalProperties:
        type: string
      properties:
        status:
          type: string
      description: What changed, by id, with the new status.
    TeamCreateBody:
      type: object
      required:
      - name
      properties:
        name:
          type: string
    TeamDetailView:
      type: object
      required:
      - id
      - org_id
      - name
      - slug
      - idp_group
      - members
      - projects
      - created_at
      - managed
      - can_manage
      - people
      - grants
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        idp_group:
          type:
          - string
          - 'null'
        members:
          type: integer
        projects:
          type: integer
        created_at:
          type: string
          format: date-time
        managed:
          type: boolean
        can_manage:
          type: boolean
        people:
          type: array
          items:
            $ref: '#/components/schemas/TeamMember'
        grants:
          type: array
          items:
            $ref: '#/components/schemas/TeamGrant'
    TeamGrant:
      type: object
      required:
      - app_id
      - app_name
      - role
      - granted_at
      properties:
        app_id:
          type: string
          format: uuid
        app_name:
          type: string
        role:
          type: string
        granted_at:
          type: string
          format: date-time
    TeamGrantView:
      type: object
      required:
      - team_id
      - app_id
      - app_role
      - from
      properties:
        team_id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        app_role:
          type: string
        from:
          type:
          - string
          - 'null'
    TeamMember:
      type: object
      required:
      - user_id
      - email
      - name
      - org_role
      - synced_group
      - added_at
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
        name:
          type: string
        org_role:
          type: string
        synced_group:
          type:
          - string
          - 'null'
        added_at:
          type: string
          format: date-time
    TeamPatchBody:
      type: object
      required:
      - name
      - idp_group
      - confirm
      properties:
        name:
          type:
          - string
          - 'null'
        idp_group:
          $ref: '#/components/schemas/OptString'
        confirm:
          type: boolean
    TeamView:
      type: object
      required:
      - id
      - org_id
      - name
      - slug
      - idp_group
      - members
      - projects
      - created_at
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        idp_group:
          type:
          - string
          - 'null'
        members:
          type: integer
        projects:
          type: integer
        created_at:
          type: string
          format: date-time
    TransferBody:
      type: object
      required:
      - user_id
      properties:
        user_id:
          type: string
          format: uuid
    TransferReply:
      type: object
      required:
      - owner
      - previous_owner
      properties:
        owner:
          type: string
          format: uuid
        previous_owner:
          type: string
          format: uuid
    UsageView:
      type: object
      additionalProperties: false
      required:
      - direction
      - granularity
      - series
      properties:
        direction:
          type: string
          enum:
          - egress
          - ingress
          - all
        granularity:
          type: string
          enum:
          - hour
          - day
        series:
          type: array
          items:
            $ref: '#/components/schemas/UsageSeries'
    UsageSeries:
      type: object
      additionalProperties: false
      required:
      - direction
      - label
      - rows
      properties:
        direction:
          $ref: '#/components/schemas/Direction'
        label:
          type: string
          description: Outbound for egress, Inbound for ingress.
        rows:
          type: array
          items:
            $ref: '#/components/schemas/UsageRow'
    UsageRow:
      type: object
      additionalProperties: false
      required:
      - bucket
      - direction
      - bytes_up
      - bytes_down
      - conns
      - reqs
      properties:
        bucket:
          type: string
          format: date-time
          description: The UTC start of the hour or day.
        direction:
          $ref: '#/components/schemas/Direction'
        bytes_up:
          type: integer
          format: int64
          minimum: 0
          description: Bytes the FixedMark IP sent.
        bytes_down:
          type: integer
          format: int64
          minimum: 0
          description: Bytes the FixedMark IP received.
        conns:
          type: integer
          format: int64
          minimum: 0
        reqs:
          type: integer
          format: int64
          minimum: 0
    View:
      type: object
      required:
      - org_id
      - status
      - enforced
      - default_role
      - domains
      - prices
      - users_this_period
      - period_start
      - enabled_at
      - disabled_at
      properties:
        org_id:
          type: string
          format: uuid
        status:
          type: string
        enforced:
          type: boolean
        default_role:
          type: string
        domains:
          type: array
          items:
            $ref: '#/components/schemas/DomainView'
        prices:
          $ref: '#/components/schemas/Object'
        users_this_period:
          type: integer
        period_start:
          type: string
          format: date-time
        enabled_at:
          type:
          - string
          - 'null'
          format: date-time
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
        refusal:
          type: string
    WebhookBody:
      type: object
      required:
      - url
      - events
      properties:
        url:
          type:
          - string
          - 'null'
        events:
          type: array
          items:
            type: string
        enabled:
          type:
          - boolean
          - 'null'
